servcraft 0.2.0 → 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/.github/workflows/ci.yml +9 -4
- package/README.md +44 -2
- package/ROADMAP.md +72 -34
- package/dist/cli/index.cjs +901 -289
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +984 -387
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +116 -1
- package/src/cli/commands/generate.ts +30 -0
- package/src/cli/commands/list.ts +1 -1
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/templates/controller-test.ts +110 -0
- package/src/cli/templates/integration-test.ts +139 -0
- package/src/cli/templates/service-test.ts +100 -0
- package/tests/cli/add.test.ts +32 -0
- package/tests/cli/completion.test.ts +35 -0
- package/tests/cli/doctor.test.ts +23 -0
- package/tests/cli/dry-run.test.ts +39 -0
- package/tests/cli/errors.test.ts +29 -0
- package/tests/cli/generate.test.ts +39 -0
- package/tests/cli/init.test.ts +63 -0
- package/tests/cli/list.test.ts +25 -0
- package/tests/cli/remove.test.ts +28 -0
- package/tests/cli/update.test.ts +34 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,221 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
2
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
7
3
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
8
4
|
}) : x)(function(x) {
|
|
9
5
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
10
6
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
11
7
|
});
|
|
12
|
-
var __esm = (fn, res) => function __init() {
|
|
13
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
|
-
};
|
|
15
|
-
var __export = (target, all) => {
|
|
16
|
-
for (var name in all)
|
|
17
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
-
};
|
|
19
|
-
var __copyProps = (to, from, except, desc) => {
|
|
20
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
-
for (let key of __getOwnPropNames(from))
|
|
22
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
-
}
|
|
25
|
-
return to;
|
|
26
|
-
};
|
|
27
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
-
|
|
29
|
-
// node_modules/tsup/assets/esm_shims.js
|
|
30
|
-
import path from "path";
|
|
31
|
-
import { fileURLToPath } from "url";
|
|
32
|
-
var init_esm_shims = __esm({
|
|
33
|
-
"node_modules/tsup/assets/esm_shims.js"() {
|
|
34
|
-
"use strict";
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// src/cli/utils/error-handler.ts
|
|
39
|
-
var error_handler_exports = {};
|
|
40
|
-
__export(error_handler_exports, {
|
|
41
|
-
ErrorTypes: () => ErrorTypes,
|
|
42
|
-
ServCraftError: () => ServCraftError,
|
|
43
|
-
displayError: () => displayError,
|
|
44
|
-
handleSystemError: () => handleSystemError,
|
|
45
|
-
validateProject: () => validateProject
|
|
46
|
-
});
|
|
47
|
-
import chalk4 from "chalk";
|
|
48
|
-
function displayError(error2) {
|
|
49
|
-
console.error("\n" + chalk4.red.bold("\u2717 Error: ") + chalk4.red(error2.message));
|
|
50
|
-
if (error2 instanceof ServCraftError) {
|
|
51
|
-
if (error2.suggestions.length > 0) {
|
|
52
|
-
console.log("\n" + chalk4.yellow.bold("\u{1F4A1} Suggestions:"));
|
|
53
|
-
error2.suggestions.forEach((suggestion) => {
|
|
54
|
-
console.log(chalk4.yellow(" \u2022 ") + suggestion);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
if (error2.docsLink) {
|
|
58
|
-
console.log("\n" + chalk4.blue.bold("\u{1F4DA} Documentation: ") + chalk4.blue.underline(error2.docsLink));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
console.log();
|
|
62
|
-
}
|
|
63
|
-
function handleSystemError(err) {
|
|
64
|
-
switch (err.code) {
|
|
65
|
-
case "ENOENT":
|
|
66
|
-
return new ServCraftError(
|
|
67
|
-
`File or directory not found: ${err.path}`,
|
|
68
|
-
[`Check if the path exists`, `Create the directory first`]
|
|
69
|
-
);
|
|
70
|
-
case "EACCES":
|
|
71
|
-
case "EPERM":
|
|
72
|
-
return new ServCraftError(
|
|
73
|
-
`Permission denied: ${err.path}`,
|
|
74
|
-
[
|
|
75
|
-
`Check file permissions`,
|
|
76
|
-
`Try running with elevated privileges (not recommended)`,
|
|
77
|
-
`Change ownership of the directory`
|
|
78
|
-
]
|
|
79
|
-
);
|
|
80
|
-
case "EEXIST":
|
|
81
|
-
return new ServCraftError(
|
|
82
|
-
`File or directory already exists: ${err.path}`,
|
|
83
|
-
[`Use a different name`, `Remove the existing file first`, `Use ${chalk4.cyan("--force")} to overwrite`]
|
|
84
|
-
);
|
|
85
|
-
case "ENOTDIR":
|
|
86
|
-
return new ServCraftError(
|
|
87
|
-
`Not a directory: ${err.path}`,
|
|
88
|
-
[`Check the path`, `A file exists where a directory is expected`]
|
|
89
|
-
);
|
|
90
|
-
case "EISDIR":
|
|
91
|
-
return new ServCraftError(
|
|
92
|
-
`Is a directory: ${err.path}`,
|
|
93
|
-
[`Cannot perform this operation on a directory`, `Did you mean to target a file?`]
|
|
94
|
-
);
|
|
95
|
-
default:
|
|
96
|
-
return new ServCraftError(
|
|
97
|
-
err.message,
|
|
98
|
-
[`Check system error code: ${err.code}`, `Review the error details above`]
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function validateProject() {
|
|
103
|
-
try {
|
|
104
|
-
const fs10 = __require("fs");
|
|
105
|
-
const path12 = __require("path");
|
|
106
|
-
if (!fs10.existsSync("package.json")) {
|
|
107
|
-
return ErrorTypes.NOT_IN_PROJECT();
|
|
108
|
-
}
|
|
109
|
-
const packageJson = JSON.parse(fs10.readFileSync("package.json", "utf-8"));
|
|
110
|
-
if (!packageJson.dependencies?.fastify) {
|
|
111
|
-
return new ServCraftError(
|
|
112
|
-
"This does not appear to be a ServCraft project",
|
|
113
|
-
[
|
|
114
|
-
`ServCraft projects require Fastify`,
|
|
115
|
-
`Run ${chalk4.cyan("servcraft init")} to create a new project`
|
|
116
|
-
]
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
} catch (err) {
|
|
121
|
-
return new ServCraftError(
|
|
122
|
-
"Failed to validate project",
|
|
123
|
-
[`Ensure you are in the project root directory`, `Check if ${chalk4.yellow("package.json")} is valid`]
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
var ServCraftError, ErrorTypes;
|
|
128
|
-
var init_error_handler = __esm({
|
|
129
|
-
"src/cli/utils/error-handler.ts"() {
|
|
130
|
-
"use strict";
|
|
131
|
-
init_esm_shims();
|
|
132
|
-
ServCraftError = class extends Error {
|
|
133
|
-
suggestions;
|
|
134
|
-
docsLink;
|
|
135
|
-
constructor(message, suggestions = [], docsLink) {
|
|
136
|
-
super(message);
|
|
137
|
-
this.name = "ServCraftError";
|
|
138
|
-
this.suggestions = suggestions;
|
|
139
|
-
this.docsLink = docsLink;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
ErrorTypes = {
|
|
143
|
-
MODULE_NOT_FOUND: (moduleName) => new ServCraftError(
|
|
144
|
-
`Module "${moduleName}" not found`,
|
|
145
|
-
[
|
|
146
|
-
`Run ${chalk4.cyan("servcraft list")} to see available modules`,
|
|
147
|
-
`Check the spelling of the module name`,
|
|
148
|
-
`Visit ${chalk4.blue("https://github.com/Le-Sourcier/servcraft#modules")} for module list`
|
|
149
|
-
],
|
|
150
|
-
"https://github.com/Le-Sourcier/servcraft#add-pre-built-modules"
|
|
151
|
-
),
|
|
152
|
-
MODULE_ALREADY_EXISTS: (moduleName) => new ServCraftError(
|
|
153
|
-
`Module "${moduleName}" already exists`,
|
|
154
|
-
[
|
|
155
|
-
`Use ${chalk4.cyan("servcraft add " + moduleName + " --force")} to overwrite`,
|
|
156
|
-
`Use ${chalk4.cyan("servcraft add " + moduleName + " --update")} to update`,
|
|
157
|
-
`Use ${chalk4.cyan("servcraft add " + moduleName + " --skip-existing")} to skip`
|
|
158
|
-
]
|
|
159
|
-
),
|
|
160
|
-
NOT_IN_PROJECT: () => new ServCraftError(
|
|
161
|
-
"Not in a ServCraft project directory",
|
|
162
|
-
[
|
|
163
|
-
`Run ${chalk4.cyan("servcraft init")} to create a new project`,
|
|
164
|
-
`Navigate to your ServCraft project directory`,
|
|
165
|
-
`Check if ${chalk4.yellow("package.json")} exists`
|
|
166
|
-
],
|
|
167
|
-
"https://github.com/Le-Sourcier/servcraft#initialize-project"
|
|
168
|
-
),
|
|
169
|
-
FILE_ALREADY_EXISTS: (fileName) => new ServCraftError(
|
|
170
|
-
`File "${fileName}" already exists`,
|
|
171
|
-
[
|
|
172
|
-
`Use ${chalk4.cyan("--force")} flag to overwrite`,
|
|
173
|
-
`Choose a different name`,
|
|
174
|
-
`Delete the existing file first`
|
|
175
|
-
]
|
|
176
|
-
),
|
|
177
|
-
INVALID_DATABASE: (database) => new ServCraftError(
|
|
178
|
-
`Invalid database type: "${database}"`,
|
|
179
|
-
[
|
|
180
|
-
`Valid options: ${chalk4.cyan("postgresql, mysql, sqlite, mongodb, none")}`,
|
|
181
|
-
`Use ${chalk4.cyan("servcraft init --db postgresql")} for PostgreSQL`
|
|
182
|
-
]
|
|
183
|
-
),
|
|
184
|
-
INVALID_VALIDATOR: (validator) => new ServCraftError(
|
|
185
|
-
`Invalid validator type: "${validator}"`,
|
|
186
|
-
[`Valid options: ${chalk4.cyan("zod, joi, yup")}`, `Default is ${chalk4.cyan("zod")}`]
|
|
187
|
-
),
|
|
188
|
-
MISSING_DEPENDENCY: (dependency, command) => new ServCraftError(
|
|
189
|
-
`Missing dependency: "${dependency}"`,
|
|
190
|
-
[`Run ${chalk4.cyan(command)} to install`, `Check your ${chalk4.yellow("package.json")}`]
|
|
191
|
-
),
|
|
192
|
-
INVALID_FIELD_FORMAT: (field) => new ServCraftError(
|
|
193
|
-
`Invalid field format: "${field}"`,
|
|
194
|
-
[
|
|
195
|
-
`Expected format: ${chalk4.cyan("name:type")}`,
|
|
196
|
-
`Example: ${chalk4.cyan("name:string age:number isActive:boolean")}`,
|
|
197
|
-
`Supported types: string, number, boolean, date`
|
|
198
|
-
]
|
|
199
|
-
),
|
|
200
|
-
GIT_NOT_INITIALIZED: () => new ServCraftError(
|
|
201
|
-
"Git repository not initialized",
|
|
202
|
-
[
|
|
203
|
-
`Run ${chalk4.cyan("git init")} to initialize git`,
|
|
204
|
-
`This is required for some ServCraft features`
|
|
205
|
-
]
|
|
206
|
-
)
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
8
|
|
|
211
9
|
// src/cli/index.ts
|
|
212
|
-
|
|
213
|
-
import { Command as Command9 } from "commander";
|
|
10
|
+
import { Command as Command11 } from "commander";
|
|
214
11
|
|
|
215
12
|
// src/cli/commands/init.ts
|
|
216
|
-
init_esm_shims();
|
|
217
13
|
import { Command } from "commander";
|
|
218
|
-
import
|
|
14
|
+
import path3 from "path";
|
|
219
15
|
import fs2 from "fs/promises";
|
|
220
16
|
import ora from "ora";
|
|
221
17
|
import inquirer from "inquirer";
|
|
@@ -223,15 +19,13 @@ import chalk3 from "chalk";
|
|
|
223
19
|
import { execSync } from "child_process";
|
|
224
20
|
|
|
225
21
|
// src/cli/utils/helpers.ts
|
|
226
|
-
init_esm_shims();
|
|
227
22
|
import fs from "fs/promises";
|
|
228
|
-
import
|
|
23
|
+
import path2 from "path";
|
|
229
24
|
import chalk2 from "chalk";
|
|
230
25
|
|
|
231
26
|
// src/cli/utils/dry-run.ts
|
|
232
|
-
init_esm_shims();
|
|
233
27
|
import chalk from "chalk";
|
|
234
|
-
import
|
|
28
|
+
import path from "path";
|
|
235
29
|
var DryRunManager = class _DryRunManager {
|
|
236
30
|
static instance;
|
|
237
31
|
enabled = false;
|
|
@@ -306,7 +100,7 @@ var DryRunManager = class _DryRunManager {
|
|
|
306
100
|
}
|
|
307
101
|
// Helper to format file path relative to cwd
|
|
308
102
|
relativePath(filePath) {
|
|
309
|
-
return
|
|
103
|
+
return path.relative(process.cwd(), filePath);
|
|
310
104
|
}
|
|
311
105
|
};
|
|
312
106
|
|
|
@@ -352,7 +146,7 @@ async function writeFile(filePath, content) {
|
|
|
352
146
|
});
|
|
353
147
|
return;
|
|
354
148
|
}
|
|
355
|
-
await ensureDir(
|
|
149
|
+
await ensureDir(path2.dirname(filePath));
|
|
356
150
|
await fs.writeFile(filePath, content, "utf-8");
|
|
357
151
|
}
|
|
358
152
|
function success(message) {
|
|
@@ -371,10 +165,10 @@ function getProjectRoot() {
|
|
|
371
165
|
return process.cwd();
|
|
372
166
|
}
|
|
373
167
|
function getSourceDir() {
|
|
374
|
-
return
|
|
168
|
+
return path2.join(getProjectRoot(), "src");
|
|
375
169
|
}
|
|
376
170
|
function getModulesDir() {
|
|
377
|
-
return
|
|
171
|
+
return path2.join(getSourceDir(), "modules");
|
|
378
172
|
}
|
|
379
173
|
|
|
380
174
|
// src/cli/commands/init.ts
|
|
@@ -484,7 +278,7 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
484
278
|
orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
|
|
485
279
|
};
|
|
486
280
|
}
|
|
487
|
-
const projectDir =
|
|
281
|
+
const projectDir = path3.resolve(process.cwd(), options.name);
|
|
488
282
|
const spinner = ora("Creating project...").start();
|
|
489
283
|
try {
|
|
490
284
|
try {
|
|
@@ -498,21 +292,21 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
498
292
|
spinner.text = "Generating project files...";
|
|
499
293
|
const packageJson = generatePackageJson(options);
|
|
500
294
|
await writeFile(
|
|
501
|
-
|
|
295
|
+
path3.join(projectDir, "package.json"),
|
|
502
296
|
JSON.stringify(packageJson, null, 2)
|
|
503
297
|
);
|
|
504
298
|
if (options.language === "typescript") {
|
|
505
|
-
await writeFile(
|
|
506
|
-
await writeFile(
|
|
299
|
+
await writeFile(path3.join(projectDir, "tsconfig.json"), generateTsConfig(options));
|
|
300
|
+
await writeFile(path3.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
|
|
507
301
|
} else {
|
|
508
|
-
await writeFile(
|
|
302
|
+
await writeFile(path3.join(projectDir, "jsconfig.json"), generateJsConfig(options));
|
|
509
303
|
}
|
|
510
|
-
await writeFile(
|
|
511
|
-
await writeFile(
|
|
512
|
-
await writeFile(
|
|
513
|
-
await writeFile(
|
|
304
|
+
await writeFile(path3.join(projectDir, ".env.example"), generateEnvExample(options));
|
|
305
|
+
await writeFile(path3.join(projectDir, ".env"), generateEnvExample(options));
|
|
306
|
+
await writeFile(path3.join(projectDir, ".gitignore"), generateGitignore());
|
|
307
|
+
await writeFile(path3.join(projectDir, "Dockerfile"), generateDockerfile(options));
|
|
514
308
|
await writeFile(
|
|
515
|
-
|
|
309
|
+
path3.join(projectDir, "docker-compose.yml"),
|
|
516
310
|
generateDockerCompose(options)
|
|
517
311
|
);
|
|
518
312
|
const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
|
|
@@ -533,45 +327,45 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
533
327
|
dirs.push("src/database/models");
|
|
534
328
|
}
|
|
535
329
|
for (const dir of dirs) {
|
|
536
|
-
await ensureDir(
|
|
330
|
+
await ensureDir(path3.join(projectDir, dir));
|
|
537
331
|
}
|
|
538
|
-
await writeFile(
|
|
332
|
+
await writeFile(path3.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
|
|
539
333
|
await writeFile(
|
|
540
|
-
|
|
334
|
+
path3.join(projectDir, `src/core/server.${ext}`),
|
|
541
335
|
generateServerFile(options)
|
|
542
336
|
);
|
|
543
337
|
await writeFile(
|
|
544
|
-
|
|
338
|
+
path3.join(projectDir, `src/core/logger.${ext}`),
|
|
545
339
|
generateLoggerFile(options)
|
|
546
340
|
);
|
|
547
341
|
await writeFile(
|
|
548
|
-
|
|
342
|
+
path3.join(projectDir, `src/config/index.${ext}`),
|
|
549
343
|
generateConfigFile(options)
|
|
550
344
|
);
|
|
551
345
|
await writeFile(
|
|
552
|
-
|
|
346
|
+
path3.join(projectDir, `src/middleware/index.${ext}`),
|
|
553
347
|
generateMiddlewareFile(options)
|
|
554
348
|
);
|
|
555
349
|
await writeFile(
|
|
556
|
-
|
|
350
|
+
path3.join(projectDir, `src/utils/index.${ext}`),
|
|
557
351
|
generateUtilsFile(options)
|
|
558
352
|
);
|
|
559
353
|
await writeFile(
|
|
560
|
-
|
|
354
|
+
path3.join(projectDir, `src/types/index.${ext}`),
|
|
561
355
|
generateTypesFile(options)
|
|
562
356
|
);
|
|
563
357
|
if (options.orm === "prisma") {
|
|
564
358
|
await writeFile(
|
|
565
|
-
|
|
359
|
+
path3.join(projectDir, "prisma/schema.prisma"),
|
|
566
360
|
generatePrismaSchema(options)
|
|
567
361
|
);
|
|
568
362
|
} else if (options.orm === "mongoose") {
|
|
569
363
|
await writeFile(
|
|
570
|
-
|
|
364
|
+
path3.join(projectDir, `src/database/connection.${ext}`),
|
|
571
365
|
generateMongooseConnection(options)
|
|
572
366
|
);
|
|
573
367
|
await writeFile(
|
|
574
|
-
|
|
368
|
+
path3.join(projectDir, `src/database/models/user.model.${ext}`),
|
|
575
369
|
generateMongooseUserModel(options)
|
|
576
370
|
);
|
|
577
371
|
}
|
|
@@ -1410,15 +1204,13 @@ declare module 'fastify' {
|
|
|
1410
1204
|
}
|
|
1411
1205
|
|
|
1412
1206
|
// src/cli/commands/generate.ts
|
|
1413
|
-
init_esm_shims();
|
|
1414
1207
|
import { Command as Command2 } from "commander";
|
|
1415
|
-
import
|
|
1208
|
+
import path4 from "path";
|
|
1416
1209
|
import ora2 from "ora";
|
|
1417
1210
|
import inquirer2 from "inquirer";
|
|
1418
1211
|
import chalk5 from "chalk";
|
|
1419
1212
|
|
|
1420
1213
|
// src/cli/utils/field-parser.ts
|
|
1421
|
-
init_esm_shims();
|
|
1422
1214
|
var tsTypeMap = {
|
|
1423
1215
|
string: "string",
|
|
1424
1216
|
number: "number",
|
|
@@ -1560,11 +1352,109 @@ function parseFields(fieldsStr) {
|
|
|
1560
1352
|
return fieldsStr.split(/\s+/).filter(Boolean).map(parseField);
|
|
1561
1353
|
}
|
|
1562
1354
|
|
|
1563
|
-
// src/cli/
|
|
1564
|
-
|
|
1355
|
+
// src/cli/utils/error-handler.ts
|
|
1356
|
+
import chalk4 from "chalk";
|
|
1357
|
+
var ServCraftError = class extends Error {
|
|
1358
|
+
suggestions;
|
|
1359
|
+
docsLink;
|
|
1360
|
+
constructor(message, suggestions = [], docsLink) {
|
|
1361
|
+
super(message);
|
|
1362
|
+
this.name = "ServCraftError";
|
|
1363
|
+
this.suggestions = suggestions;
|
|
1364
|
+
this.docsLink = docsLink;
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
var ErrorTypes = {
|
|
1368
|
+
MODULE_NOT_FOUND: (moduleName) => new ServCraftError(
|
|
1369
|
+
`Module "${moduleName}" not found`,
|
|
1370
|
+
[
|
|
1371
|
+
`Run ${chalk4.cyan("servcraft list")} to see available modules`,
|
|
1372
|
+
`Check the spelling of the module name`,
|
|
1373
|
+
`Visit ${chalk4.blue("https://github.com/Le-Sourcier/servcraft#modules")} for module list`
|
|
1374
|
+
],
|
|
1375
|
+
"https://github.com/Le-Sourcier/servcraft#add-pre-built-modules"
|
|
1376
|
+
),
|
|
1377
|
+
MODULE_ALREADY_EXISTS: (moduleName) => new ServCraftError(`Module "${moduleName}" already exists`, [
|
|
1378
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --force")} to overwrite`,
|
|
1379
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --update")} to update`,
|
|
1380
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --skip-existing")} to skip`
|
|
1381
|
+
]),
|
|
1382
|
+
NOT_IN_PROJECT: () => new ServCraftError(
|
|
1383
|
+
"Not in a ServCraft project directory",
|
|
1384
|
+
[
|
|
1385
|
+
`Run ${chalk4.cyan("servcraft init")} to create a new project`,
|
|
1386
|
+
`Navigate to your ServCraft project directory`,
|
|
1387
|
+
`Check if ${chalk4.yellow("package.json")} exists`
|
|
1388
|
+
],
|
|
1389
|
+
"https://github.com/Le-Sourcier/servcraft#initialize-project"
|
|
1390
|
+
),
|
|
1391
|
+
FILE_ALREADY_EXISTS: (fileName) => new ServCraftError(`File "${fileName}" already exists`, [
|
|
1392
|
+
`Use ${chalk4.cyan("--force")} flag to overwrite`,
|
|
1393
|
+
`Choose a different name`,
|
|
1394
|
+
`Delete the existing file first`
|
|
1395
|
+
]),
|
|
1396
|
+
INVALID_DATABASE: (database) => new ServCraftError(`Invalid database type: "${database}"`, [
|
|
1397
|
+
`Valid options: ${chalk4.cyan("postgresql, mysql, sqlite, mongodb, none")}`,
|
|
1398
|
+
`Use ${chalk4.cyan("servcraft init --db postgresql")} for PostgreSQL`
|
|
1399
|
+
]),
|
|
1400
|
+
INVALID_VALIDATOR: (validator) => new ServCraftError(`Invalid validator type: "${validator}"`, [
|
|
1401
|
+
`Valid options: ${chalk4.cyan("zod, joi, yup")}`,
|
|
1402
|
+
`Default is ${chalk4.cyan("zod")}`
|
|
1403
|
+
]),
|
|
1404
|
+
MISSING_DEPENDENCY: (dependency, command) => new ServCraftError(`Missing dependency: "${dependency}"`, [
|
|
1405
|
+
`Run ${chalk4.cyan(command)} to install`,
|
|
1406
|
+
`Check your ${chalk4.yellow("package.json")}`
|
|
1407
|
+
]),
|
|
1408
|
+
INVALID_FIELD_FORMAT: (field) => new ServCraftError(`Invalid field format: "${field}"`, [
|
|
1409
|
+
`Expected format: ${chalk4.cyan("name:type")}`,
|
|
1410
|
+
`Example: ${chalk4.cyan("name:string age:number isActive:boolean")}`,
|
|
1411
|
+
`Supported types: string, number, boolean, date`
|
|
1412
|
+
]),
|
|
1413
|
+
GIT_NOT_INITIALIZED: () => new ServCraftError("Git repository not initialized", [
|
|
1414
|
+
`Run ${chalk4.cyan("git init")} to initialize git`,
|
|
1415
|
+
`This is required for some ServCraft features`
|
|
1416
|
+
])
|
|
1417
|
+
};
|
|
1418
|
+
function displayError(error2) {
|
|
1419
|
+
console.error("\n" + chalk4.red.bold("\u2717 Error: ") + chalk4.red(error2.message));
|
|
1420
|
+
if (error2 instanceof ServCraftError) {
|
|
1421
|
+
if (error2.suggestions.length > 0) {
|
|
1422
|
+
console.log("\n" + chalk4.yellow.bold("\u{1F4A1} Suggestions:"));
|
|
1423
|
+
error2.suggestions.forEach((suggestion) => {
|
|
1424
|
+
console.log(chalk4.yellow(" \u2022 ") + suggestion);
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
if (error2.docsLink) {
|
|
1428
|
+
console.log(
|
|
1429
|
+
"\n" + chalk4.blue.bold("\u{1F4DA} Documentation: ") + chalk4.blue.underline(error2.docsLink)
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
console.log();
|
|
1434
|
+
}
|
|
1435
|
+
function validateProject() {
|
|
1436
|
+
try {
|
|
1437
|
+
const fs12 = __require("fs");
|
|
1438
|
+
if (!fs12.existsSync("package.json")) {
|
|
1439
|
+
return ErrorTypes.NOT_IN_PROJECT();
|
|
1440
|
+
}
|
|
1441
|
+
const packageJson = JSON.parse(fs12.readFileSync("package.json", "utf-8"));
|
|
1442
|
+
if (!packageJson.dependencies?.fastify) {
|
|
1443
|
+
return new ServCraftError("This does not appear to be a ServCraft project", [
|
|
1444
|
+
`ServCraft projects require Fastify`,
|
|
1445
|
+
`Run ${chalk4.cyan("servcraft init")} to create a new project`
|
|
1446
|
+
]);
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return new ServCraftError("Failed to validate project", [
|
|
1451
|
+
`Ensure you are in the project root directory`,
|
|
1452
|
+
`Check if ${chalk4.yellow("package.json")} is valid`
|
|
1453
|
+
]);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1565
1456
|
|
|
1566
1457
|
// src/cli/templates/controller.ts
|
|
1567
|
-
init_esm_shims();
|
|
1568
1458
|
function controllerTemplate(name, pascalName, camelName) {
|
|
1569
1459
|
return `import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
1570
1460
|
import type { ${pascalName}Service } from './${name}.service.js';
|
|
@@ -1634,7 +1524,6 @@ export function create${pascalName}Controller(${camelName}Service: ${pascalName}
|
|
|
1634
1524
|
}
|
|
1635
1525
|
|
|
1636
1526
|
// src/cli/templates/service.ts
|
|
1637
|
-
init_esm_shims();
|
|
1638
1527
|
function serviceTemplate(name, pascalName, camelName) {
|
|
1639
1528
|
return `import type { PaginatedResult, PaginationParams } from '../../types/index.js';
|
|
1640
1529
|
import { NotFoundError, ConflictError } from '../../utils/errors.js';
|
|
@@ -1695,7 +1584,6 @@ export function create${pascalName}Service(repository?: ${pascalName}Repository)
|
|
|
1695
1584
|
}
|
|
1696
1585
|
|
|
1697
1586
|
// src/cli/templates/repository.ts
|
|
1698
|
-
init_esm_shims();
|
|
1699
1587
|
function repositoryTemplate(name, pascalName, camelName, pluralName) {
|
|
1700
1588
|
return `import { randomUUID } from 'crypto';
|
|
1701
1589
|
import type { PaginatedResult, PaginationParams } from '../../types/index.js';
|
|
@@ -1802,7 +1690,6 @@ export function create${pascalName}Repository(): ${pascalName}Repository {
|
|
|
1802
1690
|
}
|
|
1803
1691
|
|
|
1804
1692
|
// src/cli/templates/types.ts
|
|
1805
|
-
init_esm_shims();
|
|
1806
1693
|
function typesTemplate(name, pascalName) {
|
|
1807
1694
|
return `import type { BaseEntity } from '../../types/index.js';
|
|
1808
1695
|
|
|
@@ -1832,7 +1719,6 @@ export interface ${pascalName}Filters {
|
|
|
1832
1719
|
}
|
|
1833
1720
|
|
|
1834
1721
|
// src/cli/templates/schemas.ts
|
|
1835
|
-
init_esm_shims();
|
|
1836
1722
|
function schemasTemplate(name, pascalName, camelName) {
|
|
1837
1723
|
return `import { z } from 'zod';
|
|
1838
1724
|
|
|
@@ -1861,7 +1747,6 @@ export type ${pascalName}QueryInput = z.infer<typeof ${camelName}QuerySchema>;
|
|
|
1861
1747
|
}
|
|
1862
1748
|
|
|
1863
1749
|
// src/cli/templates/routes.ts
|
|
1864
|
-
init_esm_shims();
|
|
1865
1750
|
function routesTemplate(name, pascalName, camelName, pluralName) {
|
|
1866
1751
|
return `import type { FastifyInstance } from 'fastify';
|
|
1867
1752
|
import type { ${pascalName}Controller } from './${name}.controller.js';
|
|
@@ -1914,7 +1799,6 @@ export function register${pascalName}Routes(
|
|
|
1914
1799
|
}
|
|
1915
1800
|
|
|
1916
1801
|
// src/cli/templates/module-index.ts
|
|
1917
|
-
init_esm_shims();
|
|
1918
1802
|
function moduleIndexTemplate(name, pascalName, camelName) {
|
|
1919
1803
|
return `import type { FastifyInstance } from 'fastify';
|
|
1920
1804
|
import { logger } from '../../core/logger.js';
|
|
@@ -1950,7 +1834,6 @@ export * from './${name}.schemas.js';
|
|
|
1950
1834
|
}
|
|
1951
1835
|
|
|
1952
1836
|
// src/cli/templates/prisma-model.ts
|
|
1953
|
-
init_esm_shims();
|
|
1954
1837
|
function prismaModelTemplate(name, pascalName, tableName) {
|
|
1955
1838
|
return `
|
|
1956
1839
|
// Add this model to your prisma/schema.prisma file
|
|
@@ -1970,7 +1853,6 @@ model ${pascalName} {
|
|
|
1970
1853
|
}
|
|
1971
1854
|
|
|
1972
1855
|
// src/cli/templates/dynamic-types.ts
|
|
1973
|
-
init_esm_shims();
|
|
1974
1856
|
function dynamicTypesTemplate(name, pascalName, fields) {
|
|
1975
1857
|
const fieldLines = fields.map((field) => {
|
|
1976
1858
|
const tsType = tsTypeMap[field.type];
|
|
@@ -2015,7 +1897,6 @@ ${fields.filter((f) => ["string", "enum", "boolean"].includes(f.type)).map((f) =
|
|
|
2015
1897
|
}
|
|
2016
1898
|
|
|
2017
1899
|
// src/cli/templates/dynamic-schemas.ts
|
|
2018
|
-
init_esm_shims();
|
|
2019
1900
|
function dynamicSchemasTemplate(name, pascalName, camelName, fields, validator = "zod") {
|
|
2020
1901
|
switch (validator) {
|
|
2021
1902
|
case "joi":
|
|
@@ -2194,7 +2075,6 @@ function getJsType(field) {
|
|
|
2194
2075
|
}
|
|
2195
2076
|
|
|
2196
2077
|
// src/cli/templates/dynamic-prisma.ts
|
|
2197
|
-
init_esm_shims();
|
|
2198
2078
|
function dynamicPrismaTemplate(modelName, tableName, fields) {
|
|
2199
2079
|
const fieldLines = [];
|
|
2200
2080
|
for (const field of fields) {
|
|
@@ -2262,6 +2142,353 @@ ${indexLines.join("\n")}
|
|
|
2262
2142
|
`;
|
|
2263
2143
|
}
|
|
2264
2144
|
|
|
2145
|
+
// src/cli/templates/controller-test.ts
|
|
2146
|
+
function controllerTestTemplate(name, pascalName, camelName) {
|
|
2147
|
+
return `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2148
|
+
import { build } from '../../app.js';
|
|
2149
|
+
import { FastifyInstance } from 'fastify';
|
|
2150
|
+
|
|
2151
|
+
describe('${pascalName}Controller', () => {
|
|
2152
|
+
let app: FastifyInstance;
|
|
2153
|
+
|
|
2154
|
+
beforeAll(async () => {
|
|
2155
|
+
app = await build();
|
|
2156
|
+
await app.ready();
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
afterAll(async () => {
|
|
2160
|
+
await app.close();
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
describe('GET /${name}', () => {
|
|
2164
|
+
it('should return list of ${name}', async () => {
|
|
2165
|
+
const response = await app.inject({
|
|
2166
|
+
method: 'GET',
|
|
2167
|
+
url: '/${name}',
|
|
2168
|
+
});
|
|
2169
|
+
|
|
2170
|
+
expect(response.statusCode).toBe(200);
|
|
2171
|
+
expect(response.json()).toHaveProperty('data');
|
|
2172
|
+
});
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
describe('GET /${name}/:id', () => {
|
|
2176
|
+
it('should return a single ${camelName}', async () => {
|
|
2177
|
+
// TODO: Create test ${camelName} first
|
|
2178
|
+
const response = await app.inject({
|
|
2179
|
+
method: 'GET',
|
|
2180
|
+
url: '/${name}/1',
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
expect(response.statusCode).toBe(200);
|
|
2184
|
+
expect(response.json()).toHaveProperty('data');
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
it('should return 404 for non-existent ${camelName}', async () => {
|
|
2188
|
+
const response = await app.inject({
|
|
2189
|
+
method: 'GET',
|
|
2190
|
+
url: '/${name}/999999',
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
expect(response.statusCode).toBe(404);
|
|
2194
|
+
});
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
describe('POST /${name}', () => {
|
|
2198
|
+
it('should create a new ${camelName}', async () => {
|
|
2199
|
+
const response = await app.inject({
|
|
2200
|
+
method: 'POST',
|
|
2201
|
+
url: '/${name}',
|
|
2202
|
+
payload: {
|
|
2203
|
+
// TODO: Add required fields
|
|
2204
|
+
},
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
expect(response.statusCode).toBe(201);
|
|
2208
|
+
expect(response.json()).toHaveProperty('data');
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
it('should return 400 for invalid data', async () => {
|
|
2212
|
+
const response = await app.inject({
|
|
2213
|
+
method: 'POST',
|
|
2214
|
+
url: '/${name}',
|
|
2215
|
+
payload: {},
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
expect(response.statusCode).toBe(400);
|
|
2219
|
+
});
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
describe('PUT /${name}/:id', () => {
|
|
2223
|
+
it('should update a ${camelName}', async () => {
|
|
2224
|
+
// TODO: Create test ${camelName} first
|
|
2225
|
+
const response = await app.inject({
|
|
2226
|
+
method: 'PUT',
|
|
2227
|
+
url: '/${name}/1',
|
|
2228
|
+
payload: {
|
|
2229
|
+
// TODO: Add fields to update
|
|
2230
|
+
},
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
expect(response.statusCode).toBe(200);
|
|
2234
|
+
expect(response.json()).toHaveProperty('data');
|
|
2235
|
+
});
|
|
2236
|
+
});
|
|
2237
|
+
|
|
2238
|
+
describe('DELETE /${name}/:id', () => {
|
|
2239
|
+
it('should delete a ${camelName}', async () => {
|
|
2240
|
+
// TODO: Create test ${camelName} first
|
|
2241
|
+
const response = await app.inject({
|
|
2242
|
+
method: 'DELETE',
|
|
2243
|
+
url: '/${name}/1',
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
expect(response.statusCode).toBe(204);
|
|
2247
|
+
});
|
|
2248
|
+
});
|
|
2249
|
+
});
|
|
2250
|
+
`;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// src/cli/templates/service-test.ts
|
|
2254
|
+
function serviceTestTemplate(name, pascalName, camelName) {
|
|
2255
|
+
return `import { describe, it, expect, beforeEach } from 'vitest';
|
|
2256
|
+
import { ${pascalName}Service } from '../${name}.service.js';
|
|
2257
|
+
|
|
2258
|
+
describe('${pascalName}Service', () => {
|
|
2259
|
+
let service: ${pascalName}Service;
|
|
2260
|
+
|
|
2261
|
+
beforeEach(() => {
|
|
2262
|
+
service = new ${pascalName}Service();
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
describe('getAll', () => {
|
|
2266
|
+
it('should return all ${name}', async () => {
|
|
2267
|
+
const result = await service.getAll();
|
|
2268
|
+
|
|
2269
|
+
expect(result).toBeDefined();
|
|
2270
|
+
expect(Array.isArray(result)).toBe(true);
|
|
2271
|
+
});
|
|
2272
|
+
|
|
2273
|
+
it('should apply pagination', async () => {
|
|
2274
|
+
const result = await service.getAll({ page: 1, limit: 10 });
|
|
2275
|
+
|
|
2276
|
+
expect(result).toBeDefined();
|
|
2277
|
+
expect(result.length).toBeLessThanOrEqual(10);
|
|
2278
|
+
});
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
describe('getById', () => {
|
|
2282
|
+
it('should return a ${camelName} by id', async () => {
|
|
2283
|
+
// TODO: Create test ${camelName} first
|
|
2284
|
+
const id = '1';
|
|
2285
|
+
const result = await service.getById(id);
|
|
2286
|
+
|
|
2287
|
+
expect(result).toBeDefined();
|
|
2288
|
+
expect(result.id).toBe(id);
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
it('should return null for non-existent id', async () => {
|
|
2292
|
+
const result = await service.getById('999999');
|
|
2293
|
+
|
|
2294
|
+
expect(result).toBeNull();
|
|
2295
|
+
});
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
describe('create', () => {
|
|
2299
|
+
it('should create a new ${camelName}', async () => {
|
|
2300
|
+
const data = {
|
|
2301
|
+
// TODO: Add required fields
|
|
2302
|
+
};
|
|
2303
|
+
|
|
2304
|
+
const result = await service.create(data);
|
|
2305
|
+
|
|
2306
|
+
expect(result).toBeDefined();
|
|
2307
|
+
expect(result.id).toBeDefined();
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
it('should throw error for invalid data', async () => {
|
|
2311
|
+
await expect(service.create({} as any)).rejects.toThrow();
|
|
2312
|
+
});
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
describe('update', () => {
|
|
2316
|
+
it('should update a ${camelName}', async () => {
|
|
2317
|
+
// TODO: Create test ${camelName} first
|
|
2318
|
+
const id = '1';
|
|
2319
|
+
const updates = {
|
|
2320
|
+
// TODO: Add fields to update
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
const result = await service.update(id, updates);
|
|
2324
|
+
|
|
2325
|
+
expect(result).toBeDefined();
|
|
2326
|
+
expect(result.id).toBe(id);
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
it('should return null for non-existent id', async () => {
|
|
2330
|
+
const result = await service.update('999999', {});
|
|
2331
|
+
|
|
2332
|
+
expect(result).toBeNull();
|
|
2333
|
+
});
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
describe('delete', () => {
|
|
2337
|
+
it('should delete a ${camelName}', async () => {
|
|
2338
|
+
// TODO: Create test ${camelName} first
|
|
2339
|
+
const id = '1';
|
|
2340
|
+
const result = await service.delete(id);
|
|
2341
|
+
|
|
2342
|
+
expect(result).toBe(true);
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
it('should return false for non-existent id', async () => {
|
|
2346
|
+
const result = await service.delete('999999');
|
|
2347
|
+
|
|
2348
|
+
expect(result).toBe(false);
|
|
2349
|
+
});
|
|
2350
|
+
});
|
|
2351
|
+
});
|
|
2352
|
+
`;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// src/cli/templates/integration-test.ts
|
|
2356
|
+
function integrationTestTemplate(name, pascalName, camelName) {
|
|
2357
|
+
return `import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2358
|
+
import { build } from '../../app.js';
|
|
2359
|
+
import { FastifyInstance } from 'fastify';
|
|
2360
|
+
import { prisma } from '../../lib/prisma.js';
|
|
2361
|
+
|
|
2362
|
+
describe('${pascalName} Integration Tests', () => {
|
|
2363
|
+
let app: FastifyInstance;
|
|
2364
|
+
|
|
2365
|
+
beforeAll(async () => {
|
|
2366
|
+
app = await build();
|
|
2367
|
+
await app.ready();
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
afterAll(async () => {
|
|
2371
|
+
await app.close();
|
|
2372
|
+
await prisma.$disconnect();
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
beforeEach(async () => {
|
|
2376
|
+
// Clean up test data
|
|
2377
|
+
// await prisma.${camelName}.deleteMany();
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
describe('Full CRUD workflow', () => {
|
|
2381
|
+
it('should create, read, update, and delete a ${camelName}', async () => {
|
|
2382
|
+
// Create
|
|
2383
|
+
const createResponse = await app.inject({
|
|
2384
|
+
method: 'POST',
|
|
2385
|
+
url: '/${name}',
|
|
2386
|
+
payload: {
|
|
2387
|
+
// TODO: Add required fields
|
|
2388
|
+
},
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
expect(createResponse.statusCode).toBe(201);
|
|
2392
|
+
const created = createResponse.json().data;
|
|
2393
|
+
expect(created.id).toBeDefined();
|
|
2394
|
+
|
|
2395
|
+
// Read
|
|
2396
|
+
const readResponse = await app.inject({
|
|
2397
|
+
method: 'GET',
|
|
2398
|
+
url: \`/${name}/\${created.id}\`,
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
expect(readResponse.statusCode).toBe(200);
|
|
2402
|
+
const read = readResponse.json().data;
|
|
2403
|
+
expect(read.id).toBe(created.id);
|
|
2404
|
+
|
|
2405
|
+
// Update
|
|
2406
|
+
const updateResponse = await app.inject({
|
|
2407
|
+
method: 'PUT',
|
|
2408
|
+
url: \`/${name}/\${created.id}\`,
|
|
2409
|
+
payload: {
|
|
2410
|
+
// TODO: Add fields to update
|
|
2411
|
+
},
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
expect(updateResponse.statusCode).toBe(200);
|
|
2415
|
+
const updated = updateResponse.json().data;
|
|
2416
|
+
expect(updated.id).toBe(created.id);
|
|
2417
|
+
|
|
2418
|
+
// Delete
|
|
2419
|
+
const deleteResponse = await app.inject({
|
|
2420
|
+
method: 'DELETE',
|
|
2421
|
+
url: \`/${name}/\${created.id}\`,
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
expect(deleteResponse.statusCode).toBe(204);
|
|
2425
|
+
|
|
2426
|
+
// Verify deletion
|
|
2427
|
+
const verifyResponse = await app.inject({
|
|
2428
|
+
method: 'GET',
|
|
2429
|
+
url: \`/${name}/\${created.id}\`,
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
expect(verifyResponse.statusCode).toBe(404);
|
|
2433
|
+
});
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
describe('List and pagination', () => {
|
|
2437
|
+
it('should list ${name} with pagination', async () => {
|
|
2438
|
+
// Create multiple ${name}
|
|
2439
|
+
const count = 5;
|
|
2440
|
+
for (let i = 0; i < count; i++) {
|
|
2441
|
+
await app.inject({
|
|
2442
|
+
method: 'POST',
|
|
2443
|
+
url: '/${name}',
|
|
2444
|
+
payload: {
|
|
2445
|
+
// TODO: Add required fields
|
|
2446
|
+
},
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Test pagination
|
|
2451
|
+
const response = await app.inject({
|
|
2452
|
+
method: 'GET',
|
|
2453
|
+
url: '/${name}?page=1&limit=3',
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
expect(response.statusCode).toBe(200);
|
|
2457
|
+
const result = response.json();
|
|
2458
|
+
expect(result.data).toBeDefined();
|
|
2459
|
+
expect(result.data.length).toBeLessThanOrEqual(3);
|
|
2460
|
+
expect(result.total).toBeGreaterThanOrEqual(count);
|
|
2461
|
+
});
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
describe('Validation', () => {
|
|
2465
|
+
it('should validate required fields on create', async () => {
|
|
2466
|
+
const response = await app.inject({
|
|
2467
|
+
method: 'POST',
|
|
2468
|
+
url: '/${name}',
|
|
2469
|
+
payload: {},
|
|
2470
|
+
});
|
|
2471
|
+
|
|
2472
|
+
expect(response.statusCode).toBe(400);
|
|
2473
|
+
expect(response.json()).toHaveProperty('error');
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
it('should validate data types', async () => {
|
|
2477
|
+
const response = await app.inject({
|
|
2478
|
+
method: 'POST',
|
|
2479
|
+
url: '/${name}',
|
|
2480
|
+
payload: {
|
|
2481
|
+
// TODO: Add invalid field types
|
|
2482
|
+
},
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
expect(response.statusCode).toBe(400);
|
|
2486
|
+
});
|
|
2487
|
+
});
|
|
2488
|
+
});
|
|
2489
|
+
`;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2265
2492
|
// src/cli/commands/generate.ts
|
|
2266
2493
|
function enableDryRunIfNeeded(options) {
|
|
2267
2494
|
const dryRun = DryRunManager.getInstance();
|
|
@@ -2278,7 +2505,7 @@ function showDryRunSummary(options) {
|
|
|
2278
2505
|
var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
|
|
2279
2506
|
generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
2280
2507
|
"Generate a complete module with controller, service, repository, types, schemas, and routes"
|
|
2281
|
-
).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").option("--dry-run", "Preview changes without writing files").action(async (name, fieldsArgs, options) => {
|
|
2508
|
+
).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").option("--with-tests", "Generate test files (__tests__ directory)").option("--dry-run", "Preview changes without writing files").action(async (name, fieldsArgs, options) => {
|
|
2282
2509
|
enableDryRunIfNeeded(options);
|
|
2283
2510
|
let fields = [];
|
|
2284
2511
|
if (options.interactive) {
|
|
@@ -2294,7 +2521,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2294
2521
|
const pluralName = pluralize(kebabName);
|
|
2295
2522
|
const tableName = pluralize(kebabName.replace(/-/g, "_"));
|
|
2296
2523
|
const validatorType = options.validator || "zod";
|
|
2297
|
-
const moduleDir =
|
|
2524
|
+
const moduleDir = path4.join(getModulesDir(), kebabName);
|
|
2298
2525
|
if (await fileExists(moduleDir)) {
|
|
2299
2526
|
spinner.stop();
|
|
2300
2527
|
error(`Module "${kebabName}" already exists`);
|
|
@@ -2333,7 +2560,22 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2333
2560
|
});
|
|
2334
2561
|
}
|
|
2335
2562
|
for (const file of files) {
|
|
2336
|
-
await writeFile(
|
|
2563
|
+
await writeFile(path4.join(moduleDir, file.name), file.content);
|
|
2564
|
+
}
|
|
2565
|
+
if (options.withTests) {
|
|
2566
|
+
const testDir = path4.join(moduleDir, "__tests__");
|
|
2567
|
+
await writeFile(
|
|
2568
|
+
path4.join(testDir, `${kebabName}.controller.test.ts`),
|
|
2569
|
+
controllerTestTemplate(kebabName, pascalName, camelName)
|
|
2570
|
+
);
|
|
2571
|
+
await writeFile(
|
|
2572
|
+
path4.join(testDir, `${kebabName}.service.test.ts`),
|
|
2573
|
+
serviceTestTemplate(kebabName, pascalName, camelName)
|
|
2574
|
+
);
|
|
2575
|
+
await writeFile(
|
|
2576
|
+
path4.join(testDir, `${kebabName}.integration.test.ts`),
|
|
2577
|
+
integrationTestTemplate(kebabName, pascalName, camelName)
|
|
2578
|
+
);
|
|
2337
2579
|
}
|
|
2338
2580
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
2339
2581
|
if (options.prisma || hasFields) {
|
|
@@ -2358,6 +2600,11 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2358
2600
|
}
|
|
2359
2601
|
console.log("\n\u{1F4C1} Files created:");
|
|
2360
2602
|
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
2603
|
+
if (options.withTests) {
|
|
2604
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
2605
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
2606
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
2607
|
+
}
|
|
2361
2608
|
console.log("\n\u{1F4CC} Next steps:");
|
|
2362
2609
|
if (!hasFields) {
|
|
2363
2610
|
info(` 1. Update the types in ${kebabName}.types.ts`);
|
|
@@ -2385,8 +2632,8 @@ generateCommand.command("controller <name>").alias("c").description("Generate a
|
|
|
2385
2632
|
const pascalName = toPascalCase(name);
|
|
2386
2633
|
const camelName = toCamelCase(name);
|
|
2387
2634
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2388
|
-
const moduleDir =
|
|
2389
|
-
const filePath =
|
|
2635
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2636
|
+
const filePath = path4.join(moduleDir, `${kebabName}.controller.ts`);
|
|
2390
2637
|
if (await fileExists(filePath)) {
|
|
2391
2638
|
spinner.stop();
|
|
2392
2639
|
displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
|
|
@@ -2409,8 +2656,8 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
|
|
|
2409
2656
|
const pascalName = toPascalCase(name);
|
|
2410
2657
|
const camelName = toCamelCase(name);
|
|
2411
2658
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2412
|
-
const moduleDir =
|
|
2413
|
-
const filePath =
|
|
2659
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2660
|
+
const filePath = path4.join(moduleDir, `${kebabName}.service.ts`);
|
|
2414
2661
|
if (await fileExists(filePath)) {
|
|
2415
2662
|
spinner.stop();
|
|
2416
2663
|
error(`Service "${kebabName}" already exists`);
|
|
@@ -2434,8 +2681,8 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
2434
2681
|
const camelName = toCamelCase(name);
|
|
2435
2682
|
const pluralName = pluralize(kebabName);
|
|
2436
2683
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2437
|
-
const moduleDir =
|
|
2438
|
-
const filePath =
|
|
2684
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2685
|
+
const filePath = path4.join(moduleDir, `${kebabName}.repository.ts`);
|
|
2439
2686
|
if (await fileExists(filePath)) {
|
|
2440
2687
|
spinner.stop();
|
|
2441
2688
|
error(`Repository "${kebabName}" already exists`);
|
|
@@ -2457,8 +2704,8 @@ generateCommand.command("types <name>").alias("t").description("Generate types/i
|
|
|
2457
2704
|
const kebabName = toKebabCase(name);
|
|
2458
2705
|
const pascalName = toPascalCase(name);
|
|
2459
2706
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2460
|
-
const moduleDir =
|
|
2461
|
-
const filePath =
|
|
2707
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2708
|
+
const filePath = path4.join(moduleDir, `${kebabName}.types.ts`);
|
|
2462
2709
|
if (await fileExists(filePath)) {
|
|
2463
2710
|
spinner.stop();
|
|
2464
2711
|
error(`Types file "${kebabName}.types.ts" already exists`);
|
|
@@ -2481,8 +2728,8 @@ generateCommand.command("schema <name>").alias("v").description("Generate valida
|
|
|
2481
2728
|
const pascalName = toPascalCase(name);
|
|
2482
2729
|
const camelName = toCamelCase(name);
|
|
2483
2730
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2484
|
-
const moduleDir =
|
|
2485
|
-
const filePath =
|
|
2731
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2732
|
+
const filePath = path4.join(moduleDir, `${kebabName}.schemas.ts`);
|
|
2486
2733
|
if (await fileExists(filePath)) {
|
|
2487
2734
|
spinner.stop();
|
|
2488
2735
|
error(`Schemas file "${kebabName}.schemas.ts" already exists`);
|
|
@@ -2506,8 +2753,8 @@ generateCommand.command("routes <name>").description("Generate routes").option("
|
|
|
2506
2753
|
const camelName = toCamelCase(name);
|
|
2507
2754
|
const pluralName = pluralize(kebabName);
|
|
2508
2755
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2509
|
-
const moduleDir =
|
|
2510
|
-
const filePath =
|
|
2756
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2757
|
+
const filePath = path4.join(moduleDir, `${kebabName}.routes.ts`);
|
|
2511
2758
|
if (await fileExists(filePath)) {
|
|
2512
2759
|
spinner.stop();
|
|
2513
2760
|
error(`Routes file "${kebabName}.routes.ts" already exists`);
|
|
@@ -2594,24 +2841,22 @@ async function promptForFields() {
|
|
|
2594
2841
|
}
|
|
2595
2842
|
|
|
2596
2843
|
// src/cli/commands/add-module.ts
|
|
2597
|
-
init_esm_shims();
|
|
2598
2844
|
import { Command as Command3 } from "commander";
|
|
2599
|
-
import
|
|
2845
|
+
import path7 from "path";
|
|
2600
2846
|
import ora3 from "ora";
|
|
2601
2847
|
import chalk7 from "chalk";
|
|
2602
2848
|
import * as fs5 from "fs/promises";
|
|
2603
2849
|
|
|
2604
2850
|
// src/cli/utils/env-manager.ts
|
|
2605
|
-
init_esm_shims();
|
|
2606
2851
|
import * as fs3 from "fs/promises";
|
|
2607
|
-
import * as
|
|
2852
|
+
import * as path5 from "path";
|
|
2608
2853
|
import { existsSync } from "fs";
|
|
2609
2854
|
var EnvManager = class {
|
|
2610
2855
|
envPath;
|
|
2611
2856
|
envExamplePath;
|
|
2612
2857
|
constructor(projectRoot) {
|
|
2613
|
-
this.envPath =
|
|
2614
|
-
this.envExamplePath =
|
|
2858
|
+
this.envPath = path5.join(projectRoot, ".env");
|
|
2859
|
+
this.envExamplePath = path5.join(projectRoot, ".env.example");
|
|
2615
2860
|
}
|
|
2616
2861
|
/**
|
|
2617
2862
|
* Add environment variables to .env file
|
|
@@ -3261,17 +3506,16 @@ var EnvManager = class {
|
|
|
3261
3506
|
};
|
|
3262
3507
|
|
|
3263
3508
|
// src/cli/utils/template-manager.ts
|
|
3264
|
-
init_esm_shims();
|
|
3265
3509
|
import * as fs4 from "fs/promises";
|
|
3266
|
-
import * as
|
|
3510
|
+
import * as path6 from "path";
|
|
3267
3511
|
import { createHash } from "crypto";
|
|
3268
3512
|
import { existsSync as existsSync2 } from "fs";
|
|
3269
3513
|
var TemplateManager = class {
|
|
3270
3514
|
templatesDir;
|
|
3271
3515
|
manifestsDir;
|
|
3272
3516
|
constructor(projectRoot) {
|
|
3273
|
-
this.templatesDir =
|
|
3274
|
-
this.manifestsDir =
|
|
3517
|
+
this.templatesDir = path6.join(projectRoot, ".servcraft", "templates");
|
|
3518
|
+
this.manifestsDir = path6.join(projectRoot, ".servcraft", "manifests");
|
|
3275
3519
|
}
|
|
3276
3520
|
/**
|
|
3277
3521
|
* Initialize template system
|
|
@@ -3285,10 +3529,10 @@ var TemplateManager = class {
|
|
|
3285
3529
|
*/
|
|
3286
3530
|
async saveTemplate(moduleName, files) {
|
|
3287
3531
|
await this.initialize();
|
|
3288
|
-
const moduleTemplateDir =
|
|
3532
|
+
const moduleTemplateDir = path6.join(this.templatesDir, moduleName);
|
|
3289
3533
|
await fs4.mkdir(moduleTemplateDir, { recursive: true });
|
|
3290
3534
|
for (const [fileName, content] of Object.entries(files)) {
|
|
3291
|
-
const filePath =
|
|
3535
|
+
const filePath = path6.join(moduleTemplateDir, fileName);
|
|
3292
3536
|
await fs4.writeFile(filePath, content, "utf-8");
|
|
3293
3537
|
}
|
|
3294
3538
|
}
|
|
@@ -3297,7 +3541,7 @@ var TemplateManager = class {
|
|
|
3297
3541
|
*/
|
|
3298
3542
|
async getTemplate(moduleName, fileName) {
|
|
3299
3543
|
try {
|
|
3300
|
-
const filePath =
|
|
3544
|
+
const filePath = path6.join(this.templatesDir, moduleName, fileName);
|
|
3301
3545
|
return await fs4.readFile(filePath, "utf-8");
|
|
3302
3546
|
} catch {
|
|
3303
3547
|
return null;
|
|
@@ -3322,7 +3566,7 @@ var TemplateManager = class {
|
|
|
3322
3566
|
installedAt: /* @__PURE__ */ new Date(),
|
|
3323
3567
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3324
3568
|
};
|
|
3325
|
-
const manifestPath =
|
|
3569
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
3326
3570
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
3327
3571
|
}
|
|
3328
3572
|
/**
|
|
@@ -3330,7 +3574,7 @@ var TemplateManager = class {
|
|
|
3330
3574
|
*/
|
|
3331
3575
|
async getManifest(moduleName) {
|
|
3332
3576
|
try {
|
|
3333
|
-
const manifestPath =
|
|
3577
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
3334
3578
|
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
3335
3579
|
return JSON.parse(content);
|
|
3336
3580
|
} catch {
|
|
@@ -3359,7 +3603,7 @@ var TemplateManager = class {
|
|
|
3359
3603
|
}
|
|
3360
3604
|
const results = [];
|
|
3361
3605
|
for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
|
|
3362
|
-
const filePath =
|
|
3606
|
+
const filePath = path6.join(moduleDir, fileName);
|
|
3363
3607
|
if (!existsSync2(filePath)) {
|
|
3364
3608
|
results.push({
|
|
3365
3609
|
fileName,
|
|
@@ -3385,7 +3629,7 @@ var TemplateManager = class {
|
|
|
3385
3629
|
*/
|
|
3386
3630
|
async createBackup(moduleName, moduleDir) {
|
|
3387
3631
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
3388
|
-
const backupDir =
|
|
3632
|
+
const backupDir = path6.join(path6.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
|
|
3389
3633
|
await this.copyDirectory(moduleDir, backupDir);
|
|
3390
3634
|
return backupDir;
|
|
3391
3635
|
}
|
|
@@ -3396,8 +3640,8 @@ var TemplateManager = class {
|
|
|
3396
3640
|
await fs4.mkdir(dest, { recursive: true });
|
|
3397
3641
|
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
3398
3642
|
for (const entry of entries) {
|
|
3399
|
-
const srcPath =
|
|
3400
|
-
const destPath =
|
|
3643
|
+
const srcPath = path6.join(src, entry.name);
|
|
3644
|
+
const destPath = path6.join(dest, entry.name);
|
|
3401
3645
|
if (entry.isDirectory()) {
|
|
3402
3646
|
await this.copyDirectory(srcPath, destPath);
|
|
3403
3647
|
} else {
|
|
@@ -3498,13 +3742,12 @@ var TemplateManager = class {
|
|
|
3498
3742
|
}
|
|
3499
3743
|
manifest.files = fileHashes;
|
|
3500
3744
|
manifest.updatedAt = /* @__PURE__ */ new Date();
|
|
3501
|
-
const manifestPath =
|
|
3745
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
3502
3746
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
3503
3747
|
}
|
|
3504
3748
|
};
|
|
3505
3749
|
|
|
3506
3750
|
// src/cli/utils/interactive-prompt.ts
|
|
3507
|
-
init_esm_shims();
|
|
3508
3751
|
import inquirer3 from "inquirer";
|
|
3509
3752
|
import chalk6 from "chalk";
|
|
3510
3753
|
var InteractivePrompt = class {
|
|
@@ -3690,7 +3933,6 @@ var InteractivePrompt = class {
|
|
|
3690
3933
|
};
|
|
3691
3934
|
|
|
3692
3935
|
// src/cli/commands/add-module.ts
|
|
3693
|
-
init_error_handler();
|
|
3694
3936
|
var AVAILABLE_MODULES = {
|
|
3695
3937
|
auth: {
|
|
3696
3938
|
name: "Authentication",
|
|
@@ -3858,7 +4100,7 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
|
|
|
3858
4100
|
}
|
|
3859
4101
|
const spinner = ora3(`Adding ${module.name} module...`).start();
|
|
3860
4102
|
try {
|
|
3861
|
-
const moduleDir =
|
|
4103
|
+
const moduleDir = path7.join(getModulesDir(), moduleName);
|
|
3862
4104
|
const templateManager = new TemplateManager(process.cwd());
|
|
3863
4105
|
const moduleExists = await fileExists(moduleDir);
|
|
3864
4106
|
if (moduleExists) {
|
|
@@ -4002,7 +4244,7 @@ export * from './auth.schemas.js';
|
|
|
4002
4244
|
`
|
|
4003
4245
|
};
|
|
4004
4246
|
for (const [name, content] of Object.entries(files)) {
|
|
4005
|
-
await writeFile(
|
|
4247
|
+
await writeFile(path7.join(dir, name), content);
|
|
4006
4248
|
}
|
|
4007
4249
|
}
|
|
4008
4250
|
async function generateUsersModule(dir) {
|
|
@@ -4042,7 +4284,7 @@ export * from './user.schemas.js';
|
|
|
4042
4284
|
`
|
|
4043
4285
|
};
|
|
4044
4286
|
for (const [name, content] of Object.entries(files)) {
|
|
4045
|
-
await writeFile(
|
|
4287
|
+
await writeFile(path7.join(dir, name), content);
|
|
4046
4288
|
}
|
|
4047
4289
|
}
|
|
4048
4290
|
async function generateEmailModule(dir) {
|
|
@@ -4099,7 +4341,7 @@ export { EmailService, emailService } from './email.service.js';
|
|
|
4099
4341
|
`
|
|
4100
4342
|
};
|
|
4101
4343
|
for (const [name, content] of Object.entries(files)) {
|
|
4102
|
-
await writeFile(
|
|
4344
|
+
await writeFile(path7.join(dir, name), content);
|
|
4103
4345
|
}
|
|
4104
4346
|
}
|
|
4105
4347
|
async function generateAuditModule(dir) {
|
|
@@ -4143,7 +4385,7 @@ export { AuditService, auditService } from './audit.service.js';
|
|
|
4143
4385
|
`
|
|
4144
4386
|
};
|
|
4145
4387
|
for (const [name, content] of Object.entries(files)) {
|
|
4146
|
-
await writeFile(
|
|
4388
|
+
await writeFile(path7.join(dir, name), content);
|
|
4147
4389
|
}
|
|
4148
4390
|
}
|
|
4149
4391
|
async function generateUploadModule(dir) {
|
|
@@ -4169,7 +4411,7 @@ export interface UploadOptions {
|
|
|
4169
4411
|
`
|
|
4170
4412
|
};
|
|
4171
4413
|
for (const [name, content] of Object.entries(files)) {
|
|
4172
|
-
await writeFile(
|
|
4414
|
+
await writeFile(path7.join(dir, name), content);
|
|
4173
4415
|
}
|
|
4174
4416
|
}
|
|
4175
4417
|
async function generateCacheModule(dir) {
|
|
@@ -4215,7 +4457,7 @@ export { CacheService, cacheService } from './cache.service.js';
|
|
|
4215
4457
|
`
|
|
4216
4458
|
};
|
|
4217
4459
|
for (const [name, content] of Object.entries(files)) {
|
|
4218
|
-
await writeFile(
|
|
4460
|
+
await writeFile(path7.join(dir, name), content);
|
|
4219
4461
|
}
|
|
4220
4462
|
}
|
|
4221
4463
|
async function generateGenericModule(dir, name) {
|
|
@@ -4229,18 +4471,18 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
|
|
|
4229
4471
|
`
|
|
4230
4472
|
};
|
|
4231
4473
|
for (const [fileName, content] of Object.entries(files)) {
|
|
4232
|
-
await writeFile(
|
|
4474
|
+
await writeFile(path7.join(dir, fileName), content);
|
|
4233
4475
|
}
|
|
4234
4476
|
}
|
|
4235
4477
|
async function findServercraftModules() {
|
|
4236
|
-
const scriptDir =
|
|
4478
|
+
const scriptDir = path7.dirname(new URL(import.meta.url).pathname);
|
|
4237
4479
|
const possiblePaths = [
|
|
4238
4480
|
// Local node_modules (when servcraft is a dependency)
|
|
4239
|
-
|
|
4481
|
+
path7.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
|
|
4240
4482
|
// From dist/cli/index.js -> src/modules (npx or global install)
|
|
4241
|
-
|
|
4483
|
+
path7.resolve(scriptDir, "..", "..", "src", "modules"),
|
|
4242
4484
|
// From src/cli/commands/add-module.ts -> src/modules (development)
|
|
4243
|
-
|
|
4485
|
+
path7.resolve(scriptDir, "..", "..", "modules")
|
|
4244
4486
|
];
|
|
4245
4487
|
for (const p of possiblePaths) {
|
|
4246
4488
|
try {
|
|
@@ -4264,7 +4506,7 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
4264
4506
|
const sourceDirName = moduleNameMap[moduleName] || moduleName;
|
|
4265
4507
|
const servercraftModulesDir = await findServercraftModules();
|
|
4266
4508
|
if (servercraftModulesDir) {
|
|
4267
|
-
const sourceModuleDir =
|
|
4509
|
+
const sourceModuleDir = path7.join(servercraftModulesDir, sourceDirName);
|
|
4268
4510
|
if (await fileExists(sourceModuleDir)) {
|
|
4269
4511
|
await copyModuleFromSource(sourceModuleDir, moduleDir);
|
|
4270
4512
|
return;
|
|
@@ -4296,8 +4538,8 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
4296
4538
|
async function copyModuleFromSource(sourceDir, targetDir) {
|
|
4297
4539
|
const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
|
|
4298
4540
|
for (const entry of entries) {
|
|
4299
|
-
const sourcePath =
|
|
4300
|
-
const targetPath =
|
|
4541
|
+
const sourcePath = path7.join(sourceDir, entry.name);
|
|
4542
|
+
const targetPath = path7.join(targetDir, entry.name);
|
|
4301
4543
|
if (entry.isDirectory()) {
|
|
4302
4544
|
await fs5.mkdir(targetPath, { recursive: true });
|
|
4303
4545
|
await copyModuleFromSource(sourcePath, targetPath);
|
|
@@ -4310,7 +4552,7 @@ async function getModuleFiles(moduleName, moduleDir) {
|
|
|
4310
4552
|
const files = {};
|
|
4311
4553
|
const entries = await fs5.readdir(moduleDir);
|
|
4312
4554
|
for (const entry of entries) {
|
|
4313
|
-
const filePath =
|
|
4555
|
+
const filePath = path7.join(moduleDir, entry);
|
|
4314
4556
|
const stat2 = await fs5.stat(filePath);
|
|
4315
4557
|
if (stat2.isFile() && entry.endsWith(".ts")) {
|
|
4316
4558
|
const content = await fs5.readFile(filePath, "utf-8");
|
|
@@ -4328,7 +4570,7 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
|
4328
4570
|
if (file.isModified) {
|
|
4329
4571
|
console.log(chalk7.yellow(`
|
|
4330
4572
|
\u{1F4C4} ${file.fileName}:`));
|
|
4331
|
-
const currentPath =
|
|
4573
|
+
const currentPath = path7.join(moduleDir, file.fileName);
|
|
4332
4574
|
const currentContent = await fs5.readFile(currentPath, "utf-8");
|
|
4333
4575
|
const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
|
|
4334
4576
|
if (originalContent) {
|
|
@@ -4341,11 +4583,11 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
|
4341
4583
|
async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
|
|
4342
4584
|
const spinner = ora3("Analyzing files for merge...").start();
|
|
4343
4585
|
const newFiles = {};
|
|
4344
|
-
const templateDir =
|
|
4586
|
+
const templateDir = path7.join(templateManager["templatesDir"], moduleName);
|
|
4345
4587
|
try {
|
|
4346
4588
|
const entries = await fs5.readdir(templateDir);
|
|
4347
4589
|
for (const entry of entries) {
|
|
4348
|
-
const content = await fs5.readFile(
|
|
4590
|
+
const content = await fs5.readFile(path7.join(templateDir, entry), "utf-8");
|
|
4349
4591
|
newFiles[entry] = content;
|
|
4350
4592
|
}
|
|
4351
4593
|
} catch {
|
|
@@ -4363,7 +4605,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4363
4605
|
};
|
|
4364
4606
|
for (const fileInfo of modifiedFiles) {
|
|
4365
4607
|
const fileName = fileInfo.fileName;
|
|
4366
|
-
const filePath =
|
|
4608
|
+
const filePath = path7.join(moduleDir, fileName);
|
|
4367
4609
|
const newContent = newFiles[fileName];
|
|
4368
4610
|
if (!newContent) {
|
|
4369
4611
|
continue;
|
|
@@ -4432,7 +4674,6 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4432
4674
|
}
|
|
4433
4675
|
|
|
4434
4676
|
// src/cli/commands/db.ts
|
|
4435
|
-
init_esm_shims();
|
|
4436
4677
|
import { Command as Command4 } from "commander";
|
|
4437
4678
|
import { execSync as execSync2, spawn } from "child_process";
|
|
4438
4679
|
import ora4 from "ora";
|
|
@@ -4526,25 +4767,21 @@ dbCommand.command("status").description("Show migration status").action(async ()
|
|
|
4526
4767
|
});
|
|
4527
4768
|
|
|
4528
4769
|
// src/cli/commands/docs.ts
|
|
4529
|
-
init_esm_shims();
|
|
4530
4770
|
import { Command as Command5 } from "commander";
|
|
4531
|
-
import
|
|
4771
|
+
import path9 from "path";
|
|
4532
4772
|
import fs7 from "fs/promises";
|
|
4533
4773
|
import ora6 from "ora";
|
|
4534
4774
|
import chalk9 from "chalk";
|
|
4535
4775
|
|
|
4536
4776
|
// src/cli/utils/docs-generator.ts
|
|
4537
|
-
init_esm_shims();
|
|
4538
4777
|
import fs6 from "fs/promises";
|
|
4539
|
-
import
|
|
4778
|
+
import path8 from "path";
|
|
4540
4779
|
import ora5 from "ora";
|
|
4541
4780
|
|
|
4542
4781
|
// src/core/server.ts
|
|
4543
|
-
init_esm_shims();
|
|
4544
4782
|
import Fastify from "fastify";
|
|
4545
4783
|
|
|
4546
4784
|
// src/core/logger.ts
|
|
4547
|
-
init_esm_shims();
|
|
4548
4785
|
import pino from "pino";
|
|
4549
4786
|
var defaultConfig = {
|
|
4550
4787
|
level: process.env.LOG_LEVEL || "info",
|
|
@@ -4677,14 +4914,7 @@ function createServer(config2 = {}) {
|
|
|
4677
4914
|
return new Server(config2);
|
|
4678
4915
|
}
|
|
4679
4916
|
|
|
4680
|
-
// src/middleware/index.ts
|
|
4681
|
-
init_esm_shims();
|
|
4682
|
-
|
|
4683
|
-
// src/middleware/error-handler.ts
|
|
4684
|
-
init_esm_shims();
|
|
4685
|
-
|
|
4686
4917
|
// src/utils/errors.ts
|
|
4687
|
-
init_esm_shims();
|
|
4688
4918
|
var AppError = class _AppError extends Error {
|
|
4689
4919
|
statusCode;
|
|
4690
4920
|
isOperational;
|
|
@@ -4738,11 +4968,7 @@ function isAppError(error2) {
|
|
|
4738
4968
|
return error2 instanceof AppError;
|
|
4739
4969
|
}
|
|
4740
4970
|
|
|
4741
|
-
// src/config/index.ts
|
|
4742
|
-
init_esm_shims();
|
|
4743
|
-
|
|
4744
4971
|
// src/config/env.ts
|
|
4745
|
-
init_esm_shims();
|
|
4746
4972
|
import { z } from "zod";
|
|
4747
4973
|
import dotenv from "dotenv";
|
|
4748
4974
|
dotenv.config();
|
|
@@ -4888,7 +5114,6 @@ function registerErrorHandler(app) {
|
|
|
4888
5114
|
}
|
|
4889
5115
|
|
|
4890
5116
|
// src/middleware/security.ts
|
|
4891
|
-
init_esm_shims();
|
|
4892
5117
|
import helmet from "@fastify/helmet";
|
|
4893
5118
|
import cors from "@fastify/cors";
|
|
4894
5119
|
import rateLimit from "@fastify/rate-limit";
|
|
@@ -4948,11 +5173,7 @@ async function registerSecurity(app, options = {}) {
|
|
|
4948
5173
|
}
|
|
4949
5174
|
}
|
|
4950
5175
|
|
|
4951
|
-
// src/modules/swagger/index.ts
|
|
4952
|
-
init_esm_shims();
|
|
4953
|
-
|
|
4954
5176
|
// src/modules/swagger/swagger.service.ts
|
|
4955
|
-
init_esm_shims();
|
|
4956
5177
|
import swagger from "@fastify/swagger";
|
|
4957
5178
|
import swaggerUi from "@fastify/swagger-ui";
|
|
4958
5179
|
var defaultConfig3 = {
|
|
@@ -5012,16 +5233,11 @@ async function registerSwagger(app, customConfig) {
|
|
|
5012
5233
|
logger.info("Swagger documentation registered at /docs");
|
|
5013
5234
|
}
|
|
5014
5235
|
|
|
5015
|
-
// src/modules/swagger/schema-builder.ts
|
|
5016
|
-
init_esm_shims();
|
|
5017
|
-
|
|
5018
5236
|
// src/modules/auth/index.ts
|
|
5019
|
-
init_esm_shims();
|
|
5020
5237
|
import jwt from "@fastify/jwt";
|
|
5021
5238
|
import cookie from "@fastify/cookie";
|
|
5022
5239
|
|
|
5023
5240
|
// src/modules/auth/auth.service.ts
|
|
5024
|
-
init_esm_shims();
|
|
5025
5241
|
import bcrypt from "bcryptjs";
|
|
5026
5242
|
import { Redis } from "ioredis";
|
|
5027
5243
|
var AuthService = class {
|
|
@@ -5220,11 +5436,7 @@ function createAuthService(app) {
|
|
|
5220
5436
|
return new AuthService(app);
|
|
5221
5437
|
}
|
|
5222
5438
|
|
|
5223
|
-
// src/modules/auth/auth.controller.ts
|
|
5224
|
-
init_esm_shims();
|
|
5225
|
-
|
|
5226
5439
|
// src/modules/auth/schemas.ts
|
|
5227
|
-
init_esm_shims();
|
|
5228
5440
|
import { z as z2 } from "zod";
|
|
5229
5441
|
var loginSchema = z2.object({
|
|
5230
5442
|
email: z2.string().email("Invalid email address"),
|
|
@@ -5251,7 +5463,6 @@ var changePasswordSchema = z2.object({
|
|
|
5251
5463
|
});
|
|
5252
5464
|
|
|
5253
5465
|
// src/utils/response.ts
|
|
5254
|
-
init_esm_shims();
|
|
5255
5466
|
function success2(reply, data, statusCode = 200) {
|
|
5256
5467
|
const response = {
|
|
5257
5468
|
success: true,
|
|
@@ -5267,7 +5478,6 @@ function noContent(reply) {
|
|
|
5267
5478
|
}
|
|
5268
5479
|
|
|
5269
5480
|
// src/modules/validation/validator.ts
|
|
5270
|
-
init_esm_shims();
|
|
5271
5481
|
import { z as z3 } from "zod";
|
|
5272
5482
|
function validateBody(schema, data) {
|
|
5273
5483
|
const result = schema.safeParse(data);
|
|
@@ -5438,11 +5648,7 @@ function createAuthController(authService, userService) {
|
|
|
5438
5648
|
return new AuthController(authService, userService);
|
|
5439
5649
|
}
|
|
5440
5650
|
|
|
5441
|
-
// src/modules/auth/auth.routes.ts
|
|
5442
|
-
init_esm_shims();
|
|
5443
|
-
|
|
5444
5651
|
// src/modules/auth/auth.middleware.ts
|
|
5445
|
-
init_esm_shims();
|
|
5446
5652
|
function createAuthMiddleware(authService) {
|
|
5447
5653
|
return async function authenticate(request, _reply) {
|
|
5448
5654
|
const authHeader = request.headers.authorization;
|
|
@@ -5485,14 +5691,7 @@ function registerAuthRoutes(app, controller, authService) {
|
|
|
5485
5691
|
);
|
|
5486
5692
|
}
|
|
5487
5693
|
|
|
5488
|
-
// src/modules/user/user.service.ts
|
|
5489
|
-
init_esm_shims();
|
|
5490
|
-
|
|
5491
|
-
// src/modules/user/user.repository.ts
|
|
5492
|
-
init_esm_shims();
|
|
5493
|
-
|
|
5494
5694
|
// src/database/prisma.ts
|
|
5495
|
-
init_esm_shims();
|
|
5496
5695
|
import { PrismaClient } from "@prisma/client";
|
|
5497
5696
|
var prismaClientSingleton = () => {
|
|
5498
5697
|
return new PrismaClient({
|
|
@@ -5506,7 +5705,6 @@ if (!isProduction()) {
|
|
|
5506
5705
|
}
|
|
5507
5706
|
|
|
5508
5707
|
// src/utils/pagination.ts
|
|
5509
|
-
init_esm_shims();
|
|
5510
5708
|
var DEFAULT_PAGE = 1;
|
|
5511
5709
|
var DEFAULT_LIMIT = 20;
|
|
5512
5710
|
var MAX_LIMIT = 100;
|
|
@@ -5771,7 +5969,6 @@ function createUserRepository() {
|
|
|
5771
5969
|
}
|
|
5772
5970
|
|
|
5773
5971
|
// src/modules/user/types.ts
|
|
5774
|
-
init_esm_shims();
|
|
5775
5972
|
var DEFAULT_ROLE_PERMISSIONS = {
|
|
5776
5973
|
user: ["profile:read", "profile:update"],
|
|
5777
5974
|
moderator: [
|
|
@@ -5902,9 +6099,6 @@ function createUserService(repository) {
|
|
|
5902
6099
|
return new UserService(repository || createUserRepository());
|
|
5903
6100
|
}
|
|
5904
6101
|
|
|
5905
|
-
// src/modules/auth/types.ts
|
|
5906
|
-
init_esm_shims();
|
|
5907
|
-
|
|
5908
6102
|
// src/modules/auth/index.ts
|
|
5909
6103
|
async function registerAuthModule(app) {
|
|
5910
6104
|
await app.register(jwt, {
|
|
@@ -5924,14 +6118,7 @@ async function registerAuthModule(app) {
|
|
|
5924
6118
|
logger.info("Auth module registered");
|
|
5925
6119
|
}
|
|
5926
6120
|
|
|
5927
|
-
// src/modules/user/index.ts
|
|
5928
|
-
init_esm_shims();
|
|
5929
|
-
|
|
5930
|
-
// src/modules/user/user.controller.ts
|
|
5931
|
-
init_esm_shims();
|
|
5932
|
-
|
|
5933
6121
|
// src/modules/user/schemas.ts
|
|
5934
|
-
init_esm_shims();
|
|
5935
6122
|
import { z as z4 } from "zod";
|
|
5936
6123
|
var userStatusEnum = z4.enum(["active", "inactive", "suspended", "banned"]);
|
|
5937
6124
|
var userRoleEnum = z4.enum(["user", "admin", "moderator", "super_admin"]);
|
|
@@ -6058,7 +6245,6 @@ function createUserController(userService) {
|
|
|
6058
6245
|
}
|
|
6059
6246
|
|
|
6060
6247
|
// src/modules/user/user.routes.ts
|
|
6061
|
-
init_esm_shims();
|
|
6062
6248
|
var idParamsSchema = {
|
|
6063
6249
|
type: "object",
|
|
6064
6250
|
properties: {
|
|
@@ -6147,8 +6333,8 @@ async function generateDocs(outputPath = "openapi.json", silent = false) {
|
|
|
6147
6333
|
await registerUserModule(app, authService);
|
|
6148
6334
|
await app.ready();
|
|
6149
6335
|
const spec = app.swagger();
|
|
6150
|
-
const absoluteOutput =
|
|
6151
|
-
await fs6.mkdir(
|
|
6336
|
+
const absoluteOutput = path8.resolve(outputPath);
|
|
6337
|
+
await fs6.mkdir(path8.dirname(absoluteOutput), { recursive: true });
|
|
6152
6338
|
await fs6.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
|
|
6153
6339
|
spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
|
|
6154
6340
|
await app.close();
|
|
@@ -6193,7 +6379,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
6193
6379
|
const spinner = ora6("Exporting documentation...").start();
|
|
6194
6380
|
try {
|
|
6195
6381
|
const projectRoot = getProjectRoot();
|
|
6196
|
-
const specPath =
|
|
6382
|
+
const specPath = path9.join(projectRoot, "openapi.json");
|
|
6197
6383
|
try {
|
|
6198
6384
|
await fs7.access(specPath);
|
|
6199
6385
|
} catch {
|
|
@@ -6220,7 +6406,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
6220
6406
|
default:
|
|
6221
6407
|
throw new Error(`Unknown format: ${options.format}`);
|
|
6222
6408
|
}
|
|
6223
|
-
const outPath =
|
|
6409
|
+
const outPath = path9.join(projectRoot, options.output || defaultName);
|
|
6224
6410
|
await fs7.writeFile(outPath, output);
|
|
6225
6411
|
spinner.succeed(`Exported to: ${options.output || defaultName}`);
|
|
6226
6412
|
if (options.format === "postman") {
|
|
@@ -6234,7 +6420,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
6234
6420
|
docsCommand.command("status").description("Show documentation status").action(async () => {
|
|
6235
6421
|
const projectRoot = getProjectRoot();
|
|
6236
6422
|
console.log(chalk9.bold("\n\u{1F4CA} Documentation Status\n"));
|
|
6237
|
-
const specPath =
|
|
6423
|
+
const specPath = path9.join(projectRoot, "openapi.json");
|
|
6238
6424
|
try {
|
|
6239
6425
|
const stat2 = await fs7.stat(specPath);
|
|
6240
6426
|
success(
|
|
@@ -6350,7 +6536,6 @@ function formatDate(date) {
|
|
|
6350
6536
|
}
|
|
6351
6537
|
|
|
6352
6538
|
// src/cli/commands/list.ts
|
|
6353
|
-
init_esm_shims();
|
|
6354
6539
|
import { Command as Command6 } from "commander";
|
|
6355
6540
|
import chalk10 from "chalk";
|
|
6356
6541
|
import fs8 from "fs/promises";
|
|
@@ -6508,7 +6693,7 @@ var listCommand = new Command6("list").alias("ls").description("List available a
|
|
|
6508
6693
|
if (!byCategory[mod.category]) {
|
|
6509
6694
|
byCategory[mod.category] = [];
|
|
6510
6695
|
}
|
|
6511
|
-
byCategory[mod.category]
|
|
6696
|
+
byCategory[mod.category]?.push({
|
|
6512
6697
|
id: key,
|
|
6513
6698
|
name: mod.name,
|
|
6514
6699
|
description: mod.description,
|
|
@@ -6576,14 +6761,12 @@ var listCommand = new Command6("list").alias("ls").description("List available a
|
|
|
6576
6761
|
);
|
|
6577
6762
|
|
|
6578
6763
|
// src/cli/commands/remove.ts
|
|
6579
|
-
init_esm_shims();
|
|
6580
6764
|
import { Command as Command7 } from "commander";
|
|
6581
|
-
import
|
|
6765
|
+
import path10 from "path";
|
|
6582
6766
|
import ora7 from "ora";
|
|
6583
6767
|
import chalk11 from "chalk";
|
|
6584
6768
|
import fs9 from "fs/promises";
|
|
6585
6769
|
import inquirer4 from "inquirer";
|
|
6586
|
-
init_error_handler();
|
|
6587
6770
|
var removeCommand = new Command7("remove").alias("rm").description("Remove an installed module from your project").argument("<module>", "Module to remove").option("-y, --yes", "Skip confirmation prompt").option("--keep-env", "Keep environment variables").action(async (moduleName, options) => {
|
|
6588
6771
|
const projectError = validateProject();
|
|
6589
6772
|
if (projectError) {
|
|
@@ -6591,18 +6774,15 @@ var removeCommand = new Command7("remove").alias("rm").description("Remove an in
|
|
|
6591
6774
|
return;
|
|
6592
6775
|
}
|
|
6593
6776
|
console.log(chalk11.bold.cyan("\n\u{1F5D1}\uFE0F ServCraft Module Removal\n"));
|
|
6594
|
-
const moduleDir =
|
|
6777
|
+
const moduleDir = path10.join(getModulesDir(), moduleName);
|
|
6595
6778
|
try {
|
|
6596
6779
|
const exists = await fs9.access(moduleDir).then(() => true).catch(() => false);
|
|
6597
6780
|
if (!exists) {
|
|
6598
6781
|
displayError(
|
|
6599
|
-
new (
|
|
6600
|
-
`
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
`Check the spelling of the module name`
|
|
6604
|
-
]
|
|
6605
|
-
)
|
|
6782
|
+
new ServCraftError(`Module "${moduleName}" is not installed`, [
|
|
6783
|
+
`Run ${chalk11.cyan("servcraft list --installed")} to see installed modules`,
|
|
6784
|
+
`Check the spelling of the module name`
|
|
6785
|
+
])
|
|
6606
6786
|
);
|
|
6607
6787
|
return;
|
|
6608
6788
|
}
|
|
@@ -6651,15 +6831,430 @@ var removeCommand = new Command7("remove").alias("rm").description("Remove an in
|
|
|
6651
6831
|
});
|
|
6652
6832
|
|
|
6653
6833
|
// src/cli/commands/doctor.ts
|
|
6654
|
-
init_esm_shims();
|
|
6655
6834
|
import { Command as Command8 } from "commander";
|
|
6656
6835
|
import chalk12 from "chalk";
|
|
6836
|
+
import fs10 from "fs/promises";
|
|
6837
|
+
async function checkNodeVersion() {
|
|
6838
|
+
const version = process.version;
|
|
6839
|
+
const major = parseInt(version.slice(1).split(".")[0] || "0", 10);
|
|
6840
|
+
if (major >= 18) {
|
|
6841
|
+
return { name: "Node.js", status: "pass", message: `${version} \u2713` };
|
|
6842
|
+
}
|
|
6843
|
+
return {
|
|
6844
|
+
name: "Node.js",
|
|
6845
|
+
status: "fail",
|
|
6846
|
+
message: `${version} (< 18)`,
|
|
6847
|
+
suggestion: "Upgrade to Node.js 18+"
|
|
6848
|
+
};
|
|
6849
|
+
}
|
|
6850
|
+
async function checkPackageJson() {
|
|
6851
|
+
const checks = [];
|
|
6852
|
+
try {
|
|
6853
|
+
const content = await fs10.readFile("package.json", "utf-8");
|
|
6854
|
+
const pkg = JSON.parse(content);
|
|
6855
|
+
checks.push({ name: "package.json", status: "pass", message: "Found" });
|
|
6856
|
+
if (pkg.dependencies?.fastify) {
|
|
6857
|
+
checks.push({ name: "Fastify", status: "pass", message: "Installed" });
|
|
6858
|
+
} else {
|
|
6859
|
+
checks.push({
|
|
6860
|
+
name: "Fastify",
|
|
6861
|
+
status: "fail",
|
|
6862
|
+
message: "Missing",
|
|
6863
|
+
suggestion: "npm install fastify"
|
|
6864
|
+
});
|
|
6865
|
+
}
|
|
6866
|
+
} catch {
|
|
6867
|
+
checks.push({
|
|
6868
|
+
name: "package.json",
|
|
6869
|
+
status: "fail",
|
|
6870
|
+
message: "Not found",
|
|
6871
|
+
suggestion: "Run servcraft init"
|
|
6872
|
+
});
|
|
6873
|
+
}
|
|
6874
|
+
return checks;
|
|
6875
|
+
}
|
|
6876
|
+
async function checkDirectories() {
|
|
6877
|
+
const checks = [];
|
|
6878
|
+
const dirs = ["src", "node_modules", ".git", ".env"];
|
|
6879
|
+
for (const dir of dirs) {
|
|
6880
|
+
try {
|
|
6881
|
+
await fs10.access(dir);
|
|
6882
|
+
checks.push({ name: dir, status: "pass", message: "Exists" });
|
|
6883
|
+
} catch {
|
|
6884
|
+
const isCritical = dir === "src" || dir === "node_modules";
|
|
6885
|
+
checks.push({
|
|
6886
|
+
name: dir,
|
|
6887
|
+
status: isCritical ? "fail" : "warn",
|
|
6888
|
+
message: "Not found",
|
|
6889
|
+
suggestion: dir === "node_modules" ? "npm install" : dir === ".env" ? "Create .env file" : void 0
|
|
6890
|
+
});
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
return checks;
|
|
6894
|
+
}
|
|
6657
6895
|
var doctorCommand = new Command8("doctor").description("Diagnose project configuration and dependencies").action(async () => {
|
|
6658
|
-
console.log(chalk12.bold.cyan("\
|
|
6896
|
+
console.log(chalk12.bold.cyan("\n\u{1F50D} ServCraft Doctor\n"));
|
|
6897
|
+
const allChecks = [];
|
|
6898
|
+
allChecks.push(await checkNodeVersion());
|
|
6899
|
+
allChecks.push(...await checkPackageJson());
|
|
6900
|
+
allChecks.push(...await checkDirectories());
|
|
6901
|
+
allChecks.forEach((check) => {
|
|
6902
|
+
const icon = check.status === "pass" ? chalk12.green("\u2713") : check.status === "warn" ? chalk12.yellow("\u26A0") : chalk12.red("\u2717");
|
|
6903
|
+
const color = check.status === "pass" ? chalk12.green : check.status === "warn" ? chalk12.yellow : chalk12.red;
|
|
6904
|
+
console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
|
|
6905
|
+
if (check.suggestion) {
|
|
6906
|
+
console.log(chalk12.gray(` \u2192 ${check.suggestion}`));
|
|
6907
|
+
}
|
|
6908
|
+
});
|
|
6909
|
+
const pass = allChecks.filter((c) => c.status === "pass").length;
|
|
6910
|
+
const warn2 = allChecks.filter((c) => c.status === "warn").length;
|
|
6911
|
+
const fail = allChecks.filter((c) => c.status === "fail").length;
|
|
6912
|
+
console.log(chalk12.gray("\n" + "\u2500".repeat(60)));
|
|
6913
|
+
console.log(
|
|
6914
|
+
`
|
|
6915
|
+
${chalk12.green(pass + " passed")} | ${chalk12.yellow(warn2 + " warnings")} | ${chalk12.red(fail + " failed")}
|
|
6916
|
+
`
|
|
6917
|
+
);
|
|
6918
|
+
if (fail === 0 && warn2 === 0) {
|
|
6919
|
+
console.log(chalk12.green.bold("\u2728 Everything looks good!\n"));
|
|
6920
|
+
} else if (fail > 0) {
|
|
6921
|
+
console.log(chalk12.red.bold("\u2717 Fix critical issues before using ServCraft.\n"));
|
|
6922
|
+
} else {
|
|
6923
|
+
console.log(chalk12.yellow.bold("\u26A0 Some warnings, but should work.\n"));
|
|
6924
|
+
}
|
|
6925
|
+
});
|
|
6926
|
+
|
|
6927
|
+
// src/cli/commands/update.ts
|
|
6928
|
+
import { Command as Command9 } from "commander";
|
|
6929
|
+
import chalk13 from "chalk";
|
|
6930
|
+
import fs11 from "fs/promises";
|
|
6931
|
+
import path11 from "path";
|
|
6932
|
+
import { fileURLToPath } from "url";
|
|
6933
|
+
import inquirer5 from "inquirer";
|
|
6934
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
6935
|
+
var __dirname2 = path11.dirname(__filename2);
|
|
6936
|
+
var AVAILABLE_MODULES3 = [
|
|
6937
|
+
"auth",
|
|
6938
|
+
"users",
|
|
6939
|
+
"email",
|
|
6940
|
+
"mfa",
|
|
6941
|
+
"oauth",
|
|
6942
|
+
"rate-limit",
|
|
6943
|
+
"cache",
|
|
6944
|
+
"upload",
|
|
6945
|
+
"search",
|
|
6946
|
+
"notification",
|
|
6947
|
+
"webhook",
|
|
6948
|
+
"websocket",
|
|
6949
|
+
"queue",
|
|
6950
|
+
"payment",
|
|
6951
|
+
"i18n",
|
|
6952
|
+
"feature-flag",
|
|
6953
|
+
"analytics",
|
|
6954
|
+
"media-processing",
|
|
6955
|
+
"api-versioning",
|
|
6956
|
+
"audit",
|
|
6957
|
+
"swagger",
|
|
6958
|
+
"validation"
|
|
6959
|
+
];
|
|
6960
|
+
async function getInstalledModules2() {
|
|
6961
|
+
try {
|
|
6962
|
+
const modulesDir = getModulesDir();
|
|
6963
|
+
const entries = await fs11.readdir(modulesDir, { withFileTypes: true });
|
|
6964
|
+
const installedModules = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => AVAILABLE_MODULES3.includes(name));
|
|
6965
|
+
return installedModules;
|
|
6966
|
+
} catch {
|
|
6967
|
+
return [];
|
|
6968
|
+
}
|
|
6969
|
+
}
|
|
6970
|
+
async function copyModuleFiles(moduleName, _projectRoot) {
|
|
6971
|
+
const cliRoot = path11.resolve(__dirname2, "../../../");
|
|
6972
|
+
const sourceModulePath = path11.join(cliRoot, "src", "modules", moduleName);
|
|
6973
|
+
const targetModulesDir = getModulesDir();
|
|
6974
|
+
const targetModulePath = path11.join(targetModulesDir, moduleName);
|
|
6975
|
+
try {
|
|
6976
|
+
await fs11.access(sourceModulePath);
|
|
6977
|
+
} catch {
|
|
6978
|
+
throw new Error(`Module source not found: ${moduleName}`);
|
|
6979
|
+
}
|
|
6980
|
+
await fs11.cp(sourceModulePath, targetModulePath, { recursive: true });
|
|
6981
|
+
}
|
|
6982
|
+
async function updateModule(moduleName, options) {
|
|
6983
|
+
const projectError = validateProject();
|
|
6984
|
+
if (projectError) {
|
|
6985
|
+
displayError(projectError);
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
const projectRoot = getProjectRoot();
|
|
6989
|
+
const installedModules = await getInstalledModules2();
|
|
6990
|
+
if (!installedModules.includes(moduleName)) {
|
|
6991
|
+
console.log(chalk13.yellow(`
|
|
6992
|
+
\u26A0 Module "${moduleName}" is not installed
|
|
6993
|
+
`));
|
|
6994
|
+
console.log(
|
|
6995
|
+
chalk13.gray(`Run ${chalk13.cyan(`servcraft add ${moduleName}`)} to install it first.
|
|
6996
|
+
`)
|
|
6997
|
+
);
|
|
6998
|
+
return;
|
|
6999
|
+
}
|
|
7000
|
+
if (options.check) {
|
|
7001
|
+
console.log(chalk13.cyan(`
|
|
7002
|
+
\u{1F4E6} Checking updates for "${moduleName}"...
|
|
7003
|
+
`));
|
|
7004
|
+
console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
|
|
7005
|
+
console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7006
|
+
return;
|
|
7007
|
+
}
|
|
7008
|
+
const { confirmed } = await inquirer5.prompt([
|
|
7009
|
+
{
|
|
7010
|
+
type: "confirm",
|
|
7011
|
+
name: "confirmed",
|
|
7012
|
+
message: `Update "${moduleName}" module? This will overwrite existing files.`,
|
|
7013
|
+
default: false
|
|
7014
|
+
}
|
|
7015
|
+
]);
|
|
7016
|
+
if (!confirmed) {
|
|
7017
|
+
console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
console.log(chalk13.cyan(`
|
|
7021
|
+
\u{1F504} Updating "${moduleName}" module...
|
|
7022
|
+
`));
|
|
7023
|
+
try {
|
|
7024
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7025
|
+
console.log(chalk13.green(`\u2714 Module "${moduleName}" updated successfully!
|
|
7026
|
+
`));
|
|
7027
|
+
console.log(
|
|
7028
|
+
chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7029
|
+
);
|
|
7030
|
+
} catch (error2) {
|
|
7031
|
+
if (error2 instanceof Error) {
|
|
7032
|
+
console.error(chalk13.red(`
|
|
7033
|
+
\u2717 Failed to update module: ${error2.message}
|
|
7034
|
+
`));
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
}
|
|
7038
|
+
async function updateAllModules(options) {
|
|
7039
|
+
const projectError = validateProject();
|
|
7040
|
+
if (projectError) {
|
|
7041
|
+
displayError(projectError);
|
|
7042
|
+
return;
|
|
7043
|
+
}
|
|
7044
|
+
const installedModules = await getInstalledModules2();
|
|
7045
|
+
if (installedModules.length === 0) {
|
|
7046
|
+
console.log(chalk13.yellow("\n\u26A0 No modules installed\n"));
|
|
7047
|
+
console.log(chalk13.gray(`Run ${chalk13.cyan("servcraft list")} to see available modules.
|
|
7048
|
+
`));
|
|
7049
|
+
return;
|
|
7050
|
+
}
|
|
7051
|
+
if (options.check) {
|
|
7052
|
+
console.log(chalk13.cyan("\n\u{1F4E6} Checking updates for all modules...\n"));
|
|
7053
|
+
console.log(chalk13.bold("Installed modules:"));
|
|
7054
|
+
installedModules.forEach((mod) => {
|
|
7055
|
+
console.log(` \u2022 ${chalk13.cyan(mod)}`);
|
|
7056
|
+
});
|
|
7057
|
+
console.log();
|
|
7058
|
+
console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
|
|
7059
|
+
console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7060
|
+
return;
|
|
7061
|
+
}
|
|
7062
|
+
console.log(chalk13.cyan(`
|
|
7063
|
+
\u{1F4E6} Found ${installedModules.length} installed module(s):
|
|
7064
|
+
`));
|
|
7065
|
+
installedModules.forEach((mod) => {
|
|
7066
|
+
console.log(` \u2022 ${chalk13.cyan(mod)}`);
|
|
7067
|
+
});
|
|
7068
|
+
console.log();
|
|
7069
|
+
const { confirmed } = await inquirer5.prompt([
|
|
7070
|
+
{
|
|
7071
|
+
type: "confirm",
|
|
7072
|
+
name: "confirmed",
|
|
7073
|
+
message: "Update all modules? This will overwrite existing files.",
|
|
7074
|
+
default: false
|
|
7075
|
+
}
|
|
7076
|
+
]);
|
|
7077
|
+
if (!confirmed) {
|
|
7078
|
+
console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
|
|
7079
|
+
return;
|
|
7080
|
+
}
|
|
7081
|
+
console.log(chalk13.cyan("\n\u{1F504} Updating all modules...\n"));
|
|
7082
|
+
const projectRoot = getProjectRoot();
|
|
7083
|
+
let successCount = 0;
|
|
7084
|
+
let failCount = 0;
|
|
7085
|
+
for (const moduleName of installedModules) {
|
|
7086
|
+
try {
|
|
7087
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7088
|
+
console.log(chalk13.green(`\u2714 Updated: ${moduleName}`));
|
|
7089
|
+
successCount++;
|
|
7090
|
+
} catch {
|
|
7091
|
+
console.error(chalk13.red(`\u2717 Failed: ${moduleName}`));
|
|
7092
|
+
failCount++;
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
console.log();
|
|
7096
|
+
console.log(
|
|
7097
|
+
chalk13.bold(
|
|
7098
|
+
`
|
|
7099
|
+
\u2714 Update complete: ${chalk13.green(successCount)} succeeded, ${chalk13.red(failCount)} failed
|
|
7100
|
+
`
|
|
7101
|
+
)
|
|
7102
|
+
);
|
|
7103
|
+
if (successCount > 0) {
|
|
7104
|
+
console.log(
|
|
7105
|
+
chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7106
|
+
);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
var updateCommand = new Command9("update").description("Update installed modules to latest version").argument("[module]", "Specific module to update").option("--check", "Check for updates without applying").option("-y, --yes", "Skip confirmation prompt").action(async (moduleName, options) => {
|
|
7110
|
+
if (moduleName) {
|
|
7111
|
+
await updateModule(moduleName, { check: options?.check });
|
|
7112
|
+
} else {
|
|
7113
|
+
await updateAllModules({ check: options?.check });
|
|
7114
|
+
}
|
|
7115
|
+
});
|
|
7116
|
+
|
|
7117
|
+
// src/cli/commands/completion.ts
|
|
7118
|
+
import { Command as Command10 } from "commander";
|
|
7119
|
+
var bashScript = `
|
|
7120
|
+
# servcraft bash completion script
|
|
7121
|
+
_servcraft_completions() {
|
|
7122
|
+
local cur prev words cword
|
|
7123
|
+
_init_completion || return
|
|
7124
|
+
|
|
7125
|
+
# Main commands
|
|
7126
|
+
local commands="init add generate list remove doctor update completion docs --version --help"
|
|
7127
|
+
|
|
7128
|
+
# Generate subcommands
|
|
7129
|
+
local generate_subcommands="module controller service repository types schema routes m c s r t"
|
|
7130
|
+
|
|
7131
|
+
case "\${words[1]}" in
|
|
7132
|
+
generate|g)
|
|
7133
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7134
|
+
COMPREPLY=( $(compgen -W "\${generate_subcommands}" -- "\${cur}") )
|
|
7135
|
+
fi
|
|
7136
|
+
;;
|
|
7137
|
+
add|remove|rm|update)
|
|
7138
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7139
|
+
# Get available modules
|
|
7140
|
+
local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
|
|
7141
|
+
COMPREPLY=( $(compgen -W "\${modules}" -- "\${cur}") )
|
|
7142
|
+
fi
|
|
7143
|
+
;;
|
|
7144
|
+
completion)
|
|
7145
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7146
|
+
COMPREPLY=( $(compgen -W "bash zsh" -- "\${cur}") )
|
|
7147
|
+
fi
|
|
7148
|
+
;;
|
|
7149
|
+
*)
|
|
7150
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
7151
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
7152
|
+
fi
|
|
7153
|
+
;;
|
|
7154
|
+
esac
|
|
7155
|
+
}
|
|
7156
|
+
|
|
7157
|
+
complete -F _servcraft_completions servcraft
|
|
7158
|
+
`;
|
|
7159
|
+
var zshScript = `
|
|
7160
|
+
#compdef servcraft
|
|
7161
|
+
|
|
7162
|
+
_servcraft() {
|
|
7163
|
+
local context state state_descr line
|
|
7164
|
+
typeset -A opt_args
|
|
7165
|
+
|
|
7166
|
+
_arguments -C \\
|
|
7167
|
+
'1: :_servcraft_commands' \\
|
|
7168
|
+
'*::arg:->args'
|
|
7169
|
+
|
|
7170
|
+
case $state in
|
|
7171
|
+
args)
|
|
7172
|
+
case $line[1] in
|
|
7173
|
+
generate|g)
|
|
7174
|
+
_servcraft_generate
|
|
7175
|
+
;;
|
|
7176
|
+
add|remove|rm|update)
|
|
7177
|
+
_servcraft_modules
|
|
7178
|
+
;;
|
|
7179
|
+
completion)
|
|
7180
|
+
_arguments '1: :(bash zsh)'
|
|
7181
|
+
;;
|
|
7182
|
+
esac
|
|
7183
|
+
;;
|
|
7184
|
+
esac
|
|
7185
|
+
}
|
|
7186
|
+
|
|
7187
|
+
_servcraft_commands() {
|
|
7188
|
+
local commands
|
|
7189
|
+
commands=(
|
|
7190
|
+
'init:Initialize a new ServCraft project'
|
|
7191
|
+
'add:Add a pre-built module to your project'
|
|
7192
|
+
'generate:Generate code files (controller, service, etc.)'
|
|
7193
|
+
'list:List available and installed modules'
|
|
7194
|
+
'remove:Remove an installed module'
|
|
7195
|
+
'doctor:Diagnose project configuration'
|
|
7196
|
+
'update:Update installed modules'
|
|
7197
|
+
'completion:Generate shell completion scripts'
|
|
7198
|
+
'docs:Open documentation'
|
|
7199
|
+
'--version:Show version'
|
|
7200
|
+
'--help:Show help'
|
|
7201
|
+
)
|
|
7202
|
+
_describe 'command' commands
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
_servcraft_generate() {
|
|
7206
|
+
local subcommands
|
|
7207
|
+
subcommands=(
|
|
7208
|
+
'module:Generate a complete module (controller + service + routes)'
|
|
7209
|
+
'controller:Generate a controller'
|
|
7210
|
+
'service:Generate a service'
|
|
7211
|
+
'repository:Generate a repository'
|
|
7212
|
+
'types:Generate TypeScript types'
|
|
7213
|
+
'schema:Generate validation schema'
|
|
7214
|
+
'routes:Generate routes file'
|
|
7215
|
+
'm:Alias for module'
|
|
7216
|
+
'c:Alias for controller'
|
|
7217
|
+
's:Alias for service'
|
|
7218
|
+
'r:Alias for repository'
|
|
7219
|
+
't:Alias for types'
|
|
7220
|
+
)
|
|
7221
|
+
_describe 'subcommand' subcommands
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
_servcraft_modules() {
|
|
7225
|
+
local modules
|
|
7226
|
+
modules=(
|
|
7227
|
+
'auth:Authentication & Authorization'
|
|
7228
|
+
'cache:Redis caching'
|
|
7229
|
+
'rate-limit:Rate limiting'
|
|
7230
|
+
'notification:Email/SMS notifications'
|
|
7231
|
+
'payment:Payment integration'
|
|
7232
|
+
'oauth:OAuth providers'
|
|
7233
|
+
'mfa:Multi-factor authentication'
|
|
7234
|
+
'queue:Background jobs'
|
|
7235
|
+
'websocket:WebSocket support'
|
|
7236
|
+
'upload:File upload handling'
|
|
7237
|
+
)
|
|
7238
|
+
_describe 'module' modules
|
|
7239
|
+
}
|
|
7240
|
+
|
|
7241
|
+
_servcraft "$@"
|
|
7242
|
+
`;
|
|
7243
|
+
var completionCommand = new Command10("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
|
|
7244
|
+
const shellLower = shell.toLowerCase();
|
|
7245
|
+
if (shellLower === "bash") {
|
|
7246
|
+
console.log(bashScript);
|
|
7247
|
+
} else if (shellLower === "zsh") {
|
|
7248
|
+
console.log(zshScript);
|
|
7249
|
+
} else {
|
|
7250
|
+
console.error(`Unsupported shell: ${shell}`);
|
|
7251
|
+
console.error("Supported shells: bash, zsh");
|
|
7252
|
+
process.exit(1);
|
|
7253
|
+
}
|
|
6659
7254
|
});
|
|
6660
7255
|
|
|
6661
7256
|
// src/cli/index.ts
|
|
6662
|
-
var program = new
|
|
7257
|
+
var program = new Command11();
|
|
6663
7258
|
program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
|
|
6664
7259
|
program.addCommand(initCommand);
|
|
6665
7260
|
program.addCommand(generateCommand);
|
|
@@ -6669,5 +7264,7 @@ program.addCommand(docsCommand);
|
|
|
6669
7264
|
program.addCommand(listCommand);
|
|
6670
7265
|
program.addCommand(removeCommand);
|
|
6671
7266
|
program.addCommand(doctorCommand);
|
|
7267
|
+
program.addCommand(updateCommand);
|
|
7268
|
+
program.addCommand(completionCommand);
|
|
6672
7269
|
program.parse();
|
|
6673
7270
|
//# sourceMappingURL=index.js.map
|