servcraft 0.1.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.
Files changed (106) hide show
  1. package/.dockerignore +45 -0
  2. package/.env.example +46 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +11 -0
  7. package/Dockerfile +76 -0
  8. package/Dockerfile.dev +31 -0
  9. package/README.md +232 -0
  10. package/commitlint.config.js +24 -0
  11. package/dist/cli/index.cjs +3968 -0
  12. package/dist/cli/index.cjs.map +1 -0
  13. package/dist/cli/index.d.cts +1 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +3945 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/index.cjs +2458 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +828 -0
  20. package/dist/index.d.ts +828 -0
  21. package/dist/index.js +2332 -0
  22. package/dist/index.js.map +1 -0
  23. package/docker-compose.prod.yml +118 -0
  24. package/docker-compose.yml +147 -0
  25. package/eslint.config.js +27 -0
  26. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  27. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  28. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  29. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  30. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
  31. package/npm-cache/_update-notifier-last-checked +0 -0
  32. package/package.json +112 -0
  33. package/prisma/schema.prisma +157 -0
  34. package/src/cli/commands/add-module.ts +422 -0
  35. package/src/cli/commands/db.ts +137 -0
  36. package/src/cli/commands/docs.ts +16 -0
  37. package/src/cli/commands/generate.ts +459 -0
  38. package/src/cli/commands/init.ts +640 -0
  39. package/src/cli/index.ts +32 -0
  40. package/src/cli/templates/controller.ts +67 -0
  41. package/src/cli/templates/dynamic-prisma.ts +89 -0
  42. package/src/cli/templates/dynamic-schemas.ts +232 -0
  43. package/src/cli/templates/dynamic-types.ts +60 -0
  44. package/src/cli/templates/module-index.ts +33 -0
  45. package/src/cli/templates/prisma-model.ts +17 -0
  46. package/src/cli/templates/repository.ts +104 -0
  47. package/src/cli/templates/routes.ts +70 -0
  48. package/src/cli/templates/schemas.ts +26 -0
  49. package/src/cli/templates/service.ts +58 -0
  50. package/src/cli/templates/types.ts +27 -0
  51. package/src/cli/utils/docs-generator.ts +47 -0
  52. package/src/cli/utils/field-parser.ts +315 -0
  53. package/src/cli/utils/helpers.ts +89 -0
  54. package/src/config/env.ts +80 -0
  55. package/src/config/index.ts +97 -0
  56. package/src/core/index.ts +5 -0
  57. package/src/core/logger.ts +43 -0
  58. package/src/core/server.ts +132 -0
  59. package/src/database/index.ts +7 -0
  60. package/src/database/prisma.ts +54 -0
  61. package/src/database/seed.ts +59 -0
  62. package/src/index.ts +63 -0
  63. package/src/middleware/error-handler.ts +73 -0
  64. package/src/middleware/index.ts +3 -0
  65. package/src/middleware/security.ts +116 -0
  66. package/src/modules/audit/audit.service.ts +192 -0
  67. package/src/modules/audit/index.ts +2 -0
  68. package/src/modules/audit/types.ts +37 -0
  69. package/src/modules/auth/auth.controller.ts +182 -0
  70. package/src/modules/auth/auth.middleware.ts +87 -0
  71. package/src/modules/auth/auth.routes.ts +123 -0
  72. package/src/modules/auth/auth.service.ts +142 -0
  73. package/src/modules/auth/index.ts +49 -0
  74. package/src/modules/auth/schemas.ts +52 -0
  75. package/src/modules/auth/types.ts +69 -0
  76. package/src/modules/email/email.service.ts +212 -0
  77. package/src/modules/email/index.ts +10 -0
  78. package/src/modules/email/templates.ts +213 -0
  79. package/src/modules/email/types.ts +57 -0
  80. package/src/modules/swagger/index.ts +3 -0
  81. package/src/modules/swagger/schema-builder.ts +263 -0
  82. package/src/modules/swagger/swagger.service.ts +169 -0
  83. package/src/modules/swagger/types.ts +68 -0
  84. package/src/modules/user/index.ts +30 -0
  85. package/src/modules/user/schemas.ts +49 -0
  86. package/src/modules/user/types.ts +78 -0
  87. package/src/modules/user/user.controller.ts +139 -0
  88. package/src/modules/user/user.repository.ts +156 -0
  89. package/src/modules/user/user.routes.ts +199 -0
  90. package/src/modules/user/user.service.ts +145 -0
  91. package/src/modules/validation/index.ts +18 -0
  92. package/src/modules/validation/validator.ts +104 -0
  93. package/src/types/common.ts +61 -0
  94. package/src/types/index.ts +10 -0
  95. package/src/utils/errors.ts +66 -0
  96. package/src/utils/index.ts +33 -0
  97. package/src/utils/pagination.ts +38 -0
  98. package/src/utils/response.ts +63 -0
  99. package/tests/integration/auth.test.ts +59 -0
  100. package/tests/setup.ts +17 -0
  101. package/tests/unit/modules/validation.test.ts +88 -0
  102. package/tests/unit/utils/errors.test.ts +113 -0
  103. package/tests/unit/utils/pagination.test.ts +82 -0
  104. package/tsconfig.json +33 -0
  105. package/tsup.config.ts +14 -0
  106. package/vitest.config.ts +34 -0
@@ -0,0 +1,58 @@
1
+ export function serviceTemplate(name: string, pascalName: string, camelName: string): string {
2
+ return `import type { PaginatedResult, PaginationParams } from '../../types/index.js';
3
+ import { NotFoundError, ConflictError } from '../../utils/errors.js';
4
+ import { ${pascalName}Repository, create${pascalName}Repository } from './${name}.repository.js';
5
+ import type { ${pascalName}, Create${pascalName}Data, Update${pascalName}Data, ${pascalName}Filters } from './${name}.types.js';
6
+ import { logger } from '../../core/logger.js';
7
+
8
+ export class ${pascalName}Service {
9
+ constructor(private repository: ${pascalName}Repository) {}
10
+
11
+ async findById(id: string): Promise<${pascalName} | null> {
12
+ return this.repository.findById(id);
13
+ }
14
+
15
+ async findMany(
16
+ params: PaginationParams,
17
+ filters?: ${pascalName}Filters
18
+ ): Promise<PaginatedResult<${pascalName}>> {
19
+ return this.repository.findMany(params, filters);
20
+ }
21
+
22
+ async create(data: Create${pascalName}Data): Promise<${pascalName}> {
23
+ const item = await this.repository.create(data);
24
+ logger.info({ ${camelName}Id: item.id }, '${pascalName} created');
25
+ return item;
26
+ }
27
+
28
+ async update(id: string, data: Update${pascalName}Data): Promise<${pascalName}> {
29
+ const existing = await this.repository.findById(id);
30
+ if (!existing) {
31
+ throw new NotFoundError('${pascalName}');
32
+ }
33
+
34
+ const updated = await this.repository.update(id, data);
35
+ if (!updated) {
36
+ throw new NotFoundError('${pascalName}');
37
+ }
38
+
39
+ logger.info({ ${camelName}Id: id }, '${pascalName} updated');
40
+ return updated;
41
+ }
42
+
43
+ async delete(id: string): Promise<void> {
44
+ const existing = await this.repository.findById(id);
45
+ if (!existing) {
46
+ throw new NotFoundError('${pascalName}');
47
+ }
48
+
49
+ await this.repository.delete(id);
50
+ logger.info({ ${camelName}Id: id }, '${pascalName} deleted');
51
+ }
52
+ }
53
+
54
+ export function create${pascalName}Service(repository?: ${pascalName}Repository): ${pascalName}Service {
55
+ return new ${pascalName}Service(repository || create${pascalName}Repository());
56
+ }
57
+ `;
58
+ }
@@ -0,0 +1,27 @@
1
+ export function typesTemplate(name: string, pascalName: string): string {
2
+ return `import type { BaseEntity } from '../../types/index.js';
3
+
4
+ export interface ${pascalName} extends BaseEntity {
5
+ // Add your ${pascalName} specific fields here
6
+ name: string;
7
+ description?: string;
8
+ // status?: string;
9
+ // metadata?: Record<string, unknown>;
10
+ }
11
+
12
+ export interface Create${pascalName}Data {
13
+ name: string;
14
+ description?: string;
15
+ }
16
+
17
+ export interface Update${pascalName}Data {
18
+ name?: string;
19
+ description?: string;
20
+ }
21
+
22
+ export interface ${pascalName}Filters {
23
+ search?: string;
24
+ // Add more filters as needed
25
+ }
26
+ `;
27
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import ora from 'ora';
4
+ import { createServer } from '../../core/server.js';
5
+ import { registerErrorHandler, registerSecurity } from '../../middleware/index.js';
6
+ import { registerSwagger } from '../../modules/swagger/index.js';
7
+ import { registerAuthModule } from '../../modules/auth/index.js';
8
+ import { registerUserModule } from '../../modules/user/index.js';
9
+ import { config } from '../../config/index.js';
10
+
11
+ export async function generateDocs(outputPath = 'openapi.json', silent = false): Promise<string> {
12
+ const spinner = silent ? null : ora('Generating OpenAPI documentation...').start();
13
+ try {
14
+ const server = createServer({
15
+ port: config.server.port,
16
+ host: config.server.host,
17
+ });
18
+ const app = server.instance;
19
+
20
+ registerErrorHandler(app);
21
+ await registerSecurity(app);
22
+ await registerSwagger(app, {
23
+ enabled: true,
24
+ route: config.swagger.route,
25
+ title: config.swagger.title,
26
+ description: config.swagger.description,
27
+ version: config.swagger.version,
28
+ });
29
+ const authService = await registerAuthModule(app);
30
+ await registerUserModule(app, authService);
31
+
32
+ await app.ready();
33
+ const spec = app.swagger();
34
+
35
+ const absoluteOutput = path.resolve(outputPath);
36
+ await fs.mkdir(path.dirname(absoluteOutput), { recursive: true });
37
+ await fs.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), 'utf8');
38
+
39
+ spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
40
+
41
+ await app.close();
42
+ return absoluteOutput;
43
+ } catch (error) {
44
+ spinner?.fail('Failed to generate OpenAPI documentation');
45
+ throw error;
46
+ }
47
+ }
@@ -0,0 +1,315 @@
1
+ export interface FieldDefinition {
2
+ name: string;
3
+ type: FieldType;
4
+ isOptional: boolean;
5
+ isArray: boolean;
6
+ isUnique: boolean;
7
+ defaultValue?: string;
8
+ relation?: {
9
+ model: string;
10
+ type: 'one-to-one' | 'one-to-many' | 'many-to-one';
11
+ };
12
+ }
13
+
14
+ export type FieldType =
15
+ | 'string'
16
+ | 'number'
17
+ | 'boolean'
18
+ | 'date'
19
+ | 'datetime'
20
+ | 'text'
21
+ | 'json'
22
+ | 'email'
23
+ | 'url'
24
+ | 'uuid'
25
+ | 'int'
26
+ | 'float'
27
+ | 'decimal'
28
+ | 'enum';
29
+
30
+ // Map field types to TypeScript types
31
+ export const tsTypeMap: Record<FieldType, string> = {
32
+ string: 'string',
33
+ number: 'number',
34
+ boolean: 'boolean',
35
+ date: 'Date',
36
+ datetime: 'Date',
37
+ text: 'string',
38
+ json: 'Record<string, unknown>',
39
+ email: 'string',
40
+ url: 'string',
41
+ uuid: 'string',
42
+ int: 'number',
43
+ float: 'number',
44
+ decimal: 'number',
45
+ enum: 'string',
46
+ };
47
+
48
+ // Map field types to Prisma types
49
+ export const prismaTypeMap: Record<FieldType, string> = {
50
+ string: 'String',
51
+ number: 'Int',
52
+ boolean: 'Boolean',
53
+ date: 'DateTime',
54
+ datetime: 'DateTime',
55
+ text: 'String',
56
+ json: 'Json',
57
+ email: 'String',
58
+ url: 'String',
59
+ uuid: 'String',
60
+ int: 'Int',
61
+ float: 'Float',
62
+ decimal: 'Decimal',
63
+ enum: 'String',
64
+ };
65
+
66
+ // Map field types to Zod validators
67
+ export const zodTypeMap: Record<FieldType, string> = {
68
+ string: 'z.string()',
69
+ number: 'z.number()',
70
+ boolean: 'z.boolean()',
71
+ date: 'z.coerce.date()',
72
+ datetime: 'z.coerce.date()',
73
+ text: 'z.string()',
74
+ json: 'z.record(z.unknown())',
75
+ email: 'z.string().email()',
76
+ url: 'z.string().url()',
77
+ uuid: 'z.string().uuid()',
78
+ int: 'z.number().int()',
79
+ float: 'z.number()',
80
+ decimal: 'z.number()',
81
+ enum: 'z.string()',
82
+ };
83
+
84
+ // Map field types to Joi validators
85
+ export const joiTypeMap: Record<FieldType, string> = {
86
+ string: 'Joi.string()',
87
+ number: 'Joi.number()',
88
+ boolean: 'Joi.boolean()',
89
+ date: 'Joi.date()',
90
+ datetime: 'Joi.date()',
91
+ text: 'Joi.string()',
92
+ json: 'Joi.object()',
93
+ email: 'Joi.string().email()',
94
+ url: 'Joi.string().uri()',
95
+ uuid: 'Joi.string().uuid()',
96
+ int: 'Joi.number().integer()',
97
+ float: 'Joi.number()',
98
+ decimal: 'Joi.number()',
99
+ enum: 'Joi.string()',
100
+ };
101
+
102
+ // Map field types to Yup validators
103
+ export const yupTypeMap: Record<FieldType, string> = {
104
+ string: 'yup.string()',
105
+ number: 'yup.number()',
106
+ boolean: 'yup.boolean()',
107
+ date: 'yup.date()',
108
+ datetime: 'yup.date()',
109
+ text: 'yup.string()',
110
+ json: 'yup.object()',
111
+ email: 'yup.string().email()',
112
+ url: 'yup.string().url()',
113
+ uuid: 'yup.string().uuid()',
114
+ int: 'yup.number().integer()',
115
+ float: 'yup.number()',
116
+ decimal: 'yup.number()',
117
+ enum: 'yup.string()',
118
+ };
119
+
120
+ /**
121
+ * Parse field definition string
122
+ * Format: name:type[:modifiers]
123
+ * Examples:
124
+ * - title:string
125
+ * - price:number
126
+ * - email:email:unique
127
+ * - description:text?
128
+ * - tags:string[]
129
+ * - isActive:boolean:default=true
130
+ * - category:relation:Category
131
+ */
132
+ export function parseField(fieldStr: string): FieldDefinition {
133
+ const parts = fieldStr.split(':');
134
+ let name = parts[0] || '';
135
+ let typeStr = parts[1] || 'string';
136
+ const modifiers = parts.slice(2);
137
+
138
+ // Check for optional (?)
139
+ const isOptional = name.endsWith('?') || typeStr.endsWith('?');
140
+ name = name.replace('?', '');
141
+ typeStr = typeStr.replace('?', '');
142
+
143
+ // Check for array ([])
144
+ const isArray = typeStr.endsWith('[]');
145
+ typeStr = typeStr.replace('[]', '');
146
+
147
+ // Validate type
148
+ const validTypes: FieldType[] = [
149
+ 'string', 'number', 'boolean', 'date', 'datetime',
150
+ 'text', 'json', 'email', 'url', 'uuid', 'int', 'float', 'decimal', 'enum'
151
+ ];
152
+
153
+ let type: FieldType = 'string';
154
+ if (validTypes.includes(typeStr as FieldType)) {
155
+ type = typeStr as FieldType;
156
+ }
157
+
158
+ // Parse modifiers
159
+ let isUnique = false;
160
+ let defaultValue: string | undefined;
161
+ let relation: FieldDefinition['relation'];
162
+
163
+ for (const mod of modifiers) {
164
+ if (mod === 'unique') {
165
+ isUnique = true;
166
+ } else if (mod.startsWith('default=')) {
167
+ defaultValue = mod.replace('default=', '');
168
+ } else if (typeStr === 'relation') {
169
+ relation = {
170
+ model: mod,
171
+ type: 'many-to-one',
172
+ };
173
+ type = 'string'; // Relations use string IDs
174
+ }
175
+ }
176
+
177
+ return {
178
+ name,
179
+ type,
180
+ isOptional,
181
+ isArray,
182
+ isUnique,
183
+ defaultValue,
184
+ relation,
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Parse multiple fields from command line
190
+ * Example: "title:string price:number description:text?"
191
+ */
192
+ export function parseFields(fieldsStr: string): FieldDefinition[] {
193
+ if (!fieldsStr) return [];
194
+
195
+ return fieldsStr
196
+ .split(/\s+/)
197
+ .filter(Boolean)
198
+ .map(parseField);
199
+ }
200
+
201
+ /**
202
+ * Generate TypeScript interface from fields
203
+ */
204
+ export function generateTypeInterface(
205
+ name: string,
206
+ fields: FieldDefinition[],
207
+ includeBase = true
208
+ ): string {
209
+ const lines: string[] = [];
210
+
211
+ if (includeBase) {
212
+ lines.push(`import type { BaseEntity } from '../../types/index.js';`);
213
+ lines.push('');
214
+ lines.push(`export interface ${name} extends BaseEntity {`);
215
+ } else {
216
+ lines.push(`export interface ${name} {`);
217
+ }
218
+
219
+ for (const field of fields) {
220
+ const tsType = tsTypeMap[field.type];
221
+ const arrayMark = field.isArray ? '[]' : '';
222
+ const optionalMark = field.isOptional ? '?' : '';
223
+ lines.push(` ${field.name}${optionalMark}: ${tsType}${arrayMark};`);
224
+ }
225
+
226
+ lines.push('}');
227
+
228
+ return lines.join('\n');
229
+ }
230
+
231
+ /**
232
+ * Generate Zod schema from fields
233
+ */
234
+ export function generateZodSchema(
235
+ name: string,
236
+ fields: FieldDefinition[],
237
+ schemaType: 'create' | 'update' = 'create'
238
+ ): string {
239
+ const lines: string[] = [];
240
+
241
+ lines.push(`export const ${schemaType}${name}Schema = z.object({`);
242
+
243
+ for (const field of fields) {
244
+ let validator = zodTypeMap[field.type];
245
+
246
+ if (field.isArray) {
247
+ validator = `z.array(${validator})`;
248
+ }
249
+
250
+ if (field.isOptional || schemaType === 'update') {
251
+ validator += '.optional()';
252
+ }
253
+
254
+ if (field.defaultValue && schemaType === 'create') {
255
+ validator += `.default(${field.defaultValue})`;
256
+ }
257
+
258
+ lines.push(` ${field.name}: ${validator},`);
259
+ }
260
+
261
+ lines.push('});');
262
+
263
+ return lines.join('\n');
264
+ }
265
+
266
+ /**
267
+ * Generate Prisma model from fields
268
+ */
269
+ export function generatePrismaModel(
270
+ modelName: string,
271
+ tableName: string,
272
+ fields: FieldDefinition[]
273
+ ): string {
274
+ const lines: string[] = [];
275
+
276
+ lines.push(`model ${modelName} {`);
277
+ lines.push(' id String @id @default(uuid())');
278
+
279
+ for (const field of fields) {
280
+ const prismaType = prismaTypeMap[field.type];
281
+ const optionalMark = field.isOptional ? '?' : '';
282
+ const annotations: string[] = [];
283
+
284
+ if (field.isUnique) {
285
+ annotations.push('@unique');
286
+ }
287
+
288
+ if (field.defaultValue) {
289
+ annotations.push(`@default(${field.defaultValue})`);
290
+ }
291
+
292
+ if (field.type === 'text') {
293
+ annotations.push('@db.Text');
294
+ }
295
+
296
+ const annotationStr = annotations.length > 0 ? ' ' + annotations.join(' ') : '';
297
+ lines.push(` ${field.name.padEnd(11)} ${prismaType}${optionalMark}${annotationStr}`);
298
+ }
299
+
300
+ lines.push('');
301
+ lines.push(' createdAt DateTime @default(now())');
302
+ lines.push(' updatedAt DateTime @updatedAt');
303
+ lines.push('');
304
+
305
+ // Add indexes for unique fields
306
+ const uniqueFields = fields.filter(f => f.isUnique);
307
+ for (const field of uniqueFields) {
308
+ lines.push(` @@index([${field.name}])`);
309
+ }
310
+
311
+ lines.push(` @@map("${tableName}")`);
312
+ lines.push('}');
313
+
314
+ return lines.join('\n');
315
+ }
@@ -0,0 +1,89 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export function toPascalCase(str: string): string {
6
+ return str
7
+ .split(/[-_\s]+/)
8
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
9
+ .join('');
10
+ }
11
+
12
+ export function toCamelCase(str: string): string {
13
+ const pascal = toPascalCase(str);
14
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
15
+ }
16
+
17
+ export function toKebabCase(str: string): string {
18
+ return str
19
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
20
+ .replace(/[\s_]+/g, '-')
21
+ .toLowerCase();
22
+ }
23
+
24
+ export function toSnakeCase(str: string): string {
25
+ return str
26
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
27
+ .replace(/[\s-]+/g, '_')
28
+ .toLowerCase();
29
+ }
30
+
31
+ export function pluralize(str: string): string {
32
+ if (str.endsWith('y')) {
33
+ return str.slice(0, -1) + 'ies';
34
+ }
35
+ if (str.endsWith('s') || str.endsWith('x') || str.endsWith('ch') || str.endsWith('sh')) {
36
+ return str + 'es';
37
+ }
38
+ return str + 's';
39
+ }
40
+
41
+ export async function fileExists(filePath: string): Promise<boolean> {
42
+ try {
43
+ await fs.access(filePath);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ export async function ensureDir(dirPath: string): Promise<void> {
51
+ await fs.mkdir(dirPath, { recursive: true });
52
+ }
53
+
54
+ export async function writeFile(filePath: string, content: string): Promise<void> {
55
+ await ensureDir(path.dirname(filePath));
56
+ await fs.writeFile(filePath, content, 'utf-8');
57
+ }
58
+
59
+ export function log(message: string): void {
60
+ console.log(message);
61
+ }
62
+
63
+ export function success(message: string): void {
64
+ console.log(chalk.green('✓'), message);
65
+ }
66
+
67
+ export function error(message: string): void {
68
+ console.error(chalk.red('✗'), message);
69
+ }
70
+
71
+ export function warn(message: string): void {
72
+ console.log(chalk.yellow('⚠'), message);
73
+ }
74
+
75
+ export function info(message: string): void {
76
+ console.log(chalk.blue('ℹ'), message);
77
+ }
78
+
79
+ export function getProjectRoot(): string {
80
+ return process.cwd();
81
+ }
82
+
83
+ export function getSourceDir(): string {
84
+ return path.join(getProjectRoot(), 'src');
85
+ }
86
+
87
+ export function getModulesDir(): string {
88
+ return path.join(getSourceDir(), 'modules');
89
+ }
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import dotenv from 'dotenv';
3
+ import { logger } from '../core/logger.js';
4
+
5
+ // Load .env file
6
+ dotenv.config();
7
+
8
+ const envSchema = z.object({
9
+ // Server
10
+ NODE_ENV: z.enum(['development', 'staging', 'production', 'test']).default('development'),
11
+ PORT: z.string().transform(Number).default('3000'),
12
+ HOST: z.string().default('0.0.0.0'),
13
+
14
+ // Database
15
+ DATABASE_URL: z.string().optional(),
16
+
17
+ // JWT
18
+ JWT_SECRET: z.string().min(32).optional(),
19
+ JWT_ACCESS_EXPIRES_IN: z.string().default('15m'),
20
+ JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
21
+
22
+ // Security
23
+ CORS_ORIGIN: z.string().default('*'),
24
+ RATE_LIMIT_MAX: z.string().transform(Number).default('100'),
25
+ RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default('60000'),
26
+
27
+ // Email
28
+ SMTP_HOST: z.string().optional(),
29
+ SMTP_PORT: z.string().transform(Number).optional(),
30
+ SMTP_USER: z.string().optional(),
31
+ SMTP_PASS: z.string().optional(),
32
+ SMTP_FROM: z.string().optional(),
33
+
34
+ // Redis (optional)
35
+ REDIS_URL: z.string().optional(),
36
+
37
+ // Swagger/OpenAPI
38
+ SWAGGER_ENABLED: z
39
+ .union([z.literal('true'), z.literal('false')])
40
+ .default('true')
41
+ .transform((val) => val === 'true'),
42
+ SWAGGER_ROUTE: z.string().default('/docs'),
43
+ SWAGGER_TITLE: z.string().default('Servcraft API'),
44
+ SWAGGER_DESCRIPTION: z.string().default('API documentation'),
45
+ SWAGGER_VERSION: z.string().default('1.0.0'),
46
+
47
+ // Logging
48
+ LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
49
+ });
50
+
51
+ export type Env = z.infer<typeof envSchema>;
52
+
53
+ function validateEnv(): Env {
54
+ const parsed = envSchema.safeParse(process.env);
55
+
56
+ if (!parsed.success) {
57
+ logger.error({ errors: parsed.error.flatten().fieldErrors }, 'Invalid environment variables');
58
+ throw new Error('Invalid environment variables');
59
+ }
60
+
61
+ return parsed.data;
62
+ }
63
+
64
+ export const env = validateEnv();
65
+
66
+ export function isDevelopment(): boolean {
67
+ return env.NODE_ENV === 'development';
68
+ }
69
+
70
+ export function isProduction(): boolean {
71
+ return env.NODE_ENV === 'production';
72
+ }
73
+
74
+ export function isTest(): boolean {
75
+ return env.NODE_ENV === 'test';
76
+ }
77
+
78
+ export function isStaging(): boolean {
79
+ return env.NODE_ENV === 'staging';
80
+ }