servcraft 0.2.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +9 -4
- package/README.md +70 -2
- package/ROADMAP.md +124 -47
- package/dist/cli/index.cjs +1331 -407
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1298 -389
- 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 +52 -7
- package/src/cli/commands/list.ts +1 -1
- package/src/cli/commands/scaffold.ts +211 -0
- package/src/cli/commands/templates.ts +147 -0
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +16 -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/src/cli/utils/template-loader.ts +80 -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 Command13 } 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,7 +1204,6 @@ 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
1208
|
import path5 from "path";
|
|
1416
1209
|
import ora2 from "ora";
|
|
@@ -1418,7 +1211,6 @@ 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 fs14 = __require("fs");
|
|
1438
|
+
if (!fs14.existsSync("package.json")) {
|
|
1439
|
+
return ErrorTypes.NOT_IN_PROJECT();
|
|
1440
|
+
}
|
|
1441
|
+
const packageJson = JSON.parse(fs14.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,386 @@ ${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
|
+
|
|
2492
|
+
// src/cli/utils/template-loader.ts
|
|
2493
|
+
import fs3 from "fs/promises";
|
|
2494
|
+
import path4 from "path";
|
|
2495
|
+
async function loadCustomTemplate(templateType) {
|
|
2496
|
+
const projectRoot = getProjectRoot();
|
|
2497
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
2498
|
+
const locations = [
|
|
2499
|
+
path4.join(projectRoot, ".servcraft", "templates", `${templateType}.ts`),
|
|
2500
|
+
path4.join(homeDir, ".servcraft", "templates", `${templateType}.ts`)
|
|
2501
|
+
];
|
|
2502
|
+
for (const location of locations) {
|
|
2503
|
+
try {
|
|
2504
|
+
await fs3.access(location);
|
|
2505
|
+
const templateModule = await import(`file://${location}`);
|
|
2506
|
+
const functionName = `${templateType.replace(/-/g, "")}Template`;
|
|
2507
|
+
if (templateModule[functionName]) {
|
|
2508
|
+
return templateModule;
|
|
2509
|
+
}
|
|
2510
|
+
} catch {
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
async function getTemplate(templateType, builtInTemplate) {
|
|
2517
|
+
const customTemplate = await loadCustomTemplate(templateType);
|
|
2518
|
+
if (customTemplate) {
|
|
2519
|
+
const functionName = `${templateType.replace(/-/g, "")}Template`;
|
|
2520
|
+
return customTemplate[functionName];
|
|
2521
|
+
}
|
|
2522
|
+
return builtInTemplate;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2265
2525
|
// src/cli/commands/generate.ts
|
|
2266
2526
|
function enableDryRunIfNeeded(options) {
|
|
2267
2527
|
const dryRun = DryRunManager.getInstance();
|
|
@@ -2278,7 +2538,7 @@ function showDryRunSummary(options) {
|
|
|
2278
2538
|
var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
|
|
2279
2539
|
generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
2280
2540
|
"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) => {
|
|
2541
|
+
).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
2542
|
enableDryRunIfNeeded(options);
|
|
2283
2543
|
let fields = [];
|
|
2284
2544
|
if (options.interactive) {
|
|
@@ -2301,40 +2561,65 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2301
2561
|
return;
|
|
2302
2562
|
}
|
|
2303
2563
|
const hasFields = fields.length > 0;
|
|
2564
|
+
const controllerTpl = await getTemplate("controller", controllerTemplate);
|
|
2565
|
+
const serviceTpl = await getTemplate("service", serviceTemplate);
|
|
2566
|
+
const repositoryTpl = await getTemplate("repository", repositoryTemplate);
|
|
2567
|
+
const typesTpl = await getTemplate("types", typesTemplate);
|
|
2568
|
+
const schemasTpl = await getTemplate("schemas", schemasTemplate);
|
|
2569
|
+
const routesTpl = await getTemplate("routes", routesTemplate);
|
|
2570
|
+
const moduleIndexTpl = await getTemplate("module-index", moduleIndexTemplate);
|
|
2304
2571
|
const files = [
|
|
2305
2572
|
{
|
|
2306
2573
|
name: `${kebabName}.types.ts`,
|
|
2307
|
-
content: hasFields ? dynamicTypesTemplate(kebabName, pascalName, fields) :
|
|
2574
|
+
content: hasFields ? dynamicTypesTemplate(kebabName, pascalName, fields) : typesTpl(kebabName, pascalName)
|
|
2308
2575
|
},
|
|
2309
2576
|
{
|
|
2310
2577
|
name: `${kebabName}.schemas.ts`,
|
|
2311
|
-
content: hasFields ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType) :
|
|
2578
|
+
content: hasFields ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType) : schemasTpl(kebabName, pascalName, camelName)
|
|
2312
2579
|
},
|
|
2313
2580
|
{
|
|
2314
2581
|
name: `${kebabName}.service.ts`,
|
|
2315
|
-
content:
|
|
2582
|
+
content: serviceTpl(kebabName, pascalName, camelName)
|
|
2316
2583
|
},
|
|
2317
2584
|
{
|
|
2318
2585
|
name: `${kebabName}.controller.ts`,
|
|
2319
|
-
content:
|
|
2586
|
+
content: controllerTpl(kebabName, pascalName, camelName)
|
|
2320
2587
|
},
|
|
2321
|
-
{ name: "index.ts", content:
|
|
2588
|
+
{ name: "index.ts", content: moduleIndexTpl(kebabName, pascalName, camelName) }
|
|
2322
2589
|
];
|
|
2323
2590
|
if (options.repository !== false) {
|
|
2324
2591
|
files.push({
|
|
2325
2592
|
name: `${kebabName}.repository.ts`,
|
|
2326
|
-
content:
|
|
2593
|
+
content: repositoryTpl(kebabName, pascalName, camelName, pluralName)
|
|
2327
2594
|
});
|
|
2328
2595
|
}
|
|
2329
2596
|
if (options.routes !== false) {
|
|
2330
2597
|
files.push({
|
|
2331
2598
|
name: `${kebabName}.routes.ts`,
|
|
2332
|
-
content:
|
|
2599
|
+
content: routesTpl(kebabName, pascalName, camelName, pluralName)
|
|
2333
2600
|
});
|
|
2334
2601
|
}
|
|
2335
2602
|
for (const file of files) {
|
|
2336
2603
|
await writeFile(path5.join(moduleDir, file.name), file.content);
|
|
2337
2604
|
}
|
|
2605
|
+
if (options.withTests) {
|
|
2606
|
+
const testDir = path5.join(moduleDir, "__tests__");
|
|
2607
|
+
const controllerTestTpl = await getTemplate("controller-test", controllerTestTemplate);
|
|
2608
|
+
const serviceTestTpl = await getTemplate("service-test", serviceTestTemplate);
|
|
2609
|
+
const integrationTestTpl = await getTemplate("integration-test", integrationTestTemplate);
|
|
2610
|
+
await writeFile(
|
|
2611
|
+
path5.join(testDir, `${kebabName}.controller.test.ts`),
|
|
2612
|
+
controllerTestTpl(kebabName, pascalName, camelName)
|
|
2613
|
+
);
|
|
2614
|
+
await writeFile(
|
|
2615
|
+
path5.join(testDir, `${kebabName}.service.test.ts`),
|
|
2616
|
+
serviceTestTpl(kebabName, pascalName, camelName)
|
|
2617
|
+
);
|
|
2618
|
+
await writeFile(
|
|
2619
|
+
path5.join(testDir, `${kebabName}.integration.test.ts`),
|
|
2620
|
+
integrationTestTpl(kebabName, pascalName, camelName)
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2338
2623
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
2339
2624
|
if (options.prisma || hasFields) {
|
|
2340
2625
|
console.log("\n" + "\u2500".repeat(50));
|
|
@@ -2358,6 +2643,11 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2358
2643
|
}
|
|
2359
2644
|
console.log("\n\u{1F4C1} Files created:");
|
|
2360
2645
|
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
2646
|
+
if (options.withTests) {
|
|
2647
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
2648
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
2649
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
2650
|
+
}
|
|
2361
2651
|
console.log("\n\u{1F4CC} Next steps:");
|
|
2362
2652
|
if (!hasFields) {
|
|
2363
2653
|
info(` 1. Update the types in ${kebabName}.types.ts`);
|
|
@@ -2594,16 +2884,14 @@ async function promptForFields() {
|
|
|
2594
2884
|
}
|
|
2595
2885
|
|
|
2596
2886
|
// src/cli/commands/add-module.ts
|
|
2597
|
-
init_esm_shims();
|
|
2598
2887
|
import { Command as Command3 } from "commander";
|
|
2599
2888
|
import path8 from "path";
|
|
2600
2889
|
import ora3 from "ora";
|
|
2601
2890
|
import chalk7 from "chalk";
|
|
2602
|
-
import * as
|
|
2891
|
+
import * as fs6 from "fs/promises";
|
|
2603
2892
|
|
|
2604
2893
|
// src/cli/utils/env-manager.ts
|
|
2605
|
-
|
|
2606
|
-
import * as fs3 from "fs/promises";
|
|
2894
|
+
import * as fs4 from "fs/promises";
|
|
2607
2895
|
import * as path6 from "path";
|
|
2608
2896
|
import { existsSync } from "fs";
|
|
2609
2897
|
var EnvManager = class {
|
|
@@ -2622,7 +2910,7 @@ var EnvManager = class {
|
|
|
2622
2910
|
let created2 = false;
|
|
2623
2911
|
let envContent = "";
|
|
2624
2912
|
if (existsSync(this.envPath)) {
|
|
2625
|
-
envContent = await
|
|
2913
|
+
envContent = await fs4.readFile(this.envPath, "utf-8");
|
|
2626
2914
|
} else {
|
|
2627
2915
|
created2 = true;
|
|
2628
2916
|
}
|
|
@@ -2651,7 +2939,7 @@ var EnvManager = class {
|
|
|
2651
2939
|
}
|
|
2652
2940
|
newContent += "\n";
|
|
2653
2941
|
}
|
|
2654
|
-
await
|
|
2942
|
+
await fs4.writeFile(this.envPath, newContent, "utf-8");
|
|
2655
2943
|
if (existsSync(this.envExamplePath)) {
|
|
2656
2944
|
await this.updateEnvExample(sections);
|
|
2657
2945
|
}
|
|
@@ -2663,7 +2951,7 @@ var EnvManager = class {
|
|
|
2663
2951
|
async updateEnvExample(sections) {
|
|
2664
2952
|
let exampleContent = "";
|
|
2665
2953
|
if (existsSync(this.envExamplePath)) {
|
|
2666
|
-
exampleContent = await
|
|
2954
|
+
exampleContent = await fs4.readFile(this.envExamplePath, "utf-8");
|
|
2667
2955
|
}
|
|
2668
2956
|
const existingKeys = this.parseExistingKeys(exampleContent);
|
|
2669
2957
|
let newContent = exampleContent;
|
|
@@ -2687,7 +2975,7 @@ var EnvManager = class {
|
|
|
2687
2975
|
}
|
|
2688
2976
|
newContent += "\n";
|
|
2689
2977
|
}
|
|
2690
|
-
await
|
|
2978
|
+
await fs4.writeFile(this.envExamplePath, newContent, "utf-8");
|
|
2691
2979
|
}
|
|
2692
2980
|
/**
|
|
2693
2981
|
* Parse existing environment variable keys
|
|
@@ -3261,8 +3549,7 @@ var EnvManager = class {
|
|
|
3261
3549
|
};
|
|
3262
3550
|
|
|
3263
3551
|
// src/cli/utils/template-manager.ts
|
|
3264
|
-
|
|
3265
|
-
import * as fs4 from "fs/promises";
|
|
3552
|
+
import * as fs5 from "fs/promises";
|
|
3266
3553
|
import * as path7 from "path";
|
|
3267
3554
|
import { createHash } from "crypto";
|
|
3268
3555
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -3277,8 +3564,8 @@ var TemplateManager = class {
|
|
|
3277
3564
|
* Initialize template system
|
|
3278
3565
|
*/
|
|
3279
3566
|
async initialize() {
|
|
3280
|
-
await
|
|
3281
|
-
await
|
|
3567
|
+
await fs5.mkdir(this.templatesDir, { recursive: true });
|
|
3568
|
+
await fs5.mkdir(this.manifestsDir, { recursive: true });
|
|
3282
3569
|
}
|
|
3283
3570
|
/**
|
|
3284
3571
|
* Save module template
|
|
@@ -3286,10 +3573,10 @@ var TemplateManager = class {
|
|
|
3286
3573
|
async saveTemplate(moduleName, files) {
|
|
3287
3574
|
await this.initialize();
|
|
3288
3575
|
const moduleTemplateDir = path7.join(this.templatesDir, moduleName);
|
|
3289
|
-
await
|
|
3576
|
+
await fs5.mkdir(moduleTemplateDir, { recursive: true });
|
|
3290
3577
|
for (const [fileName, content] of Object.entries(files)) {
|
|
3291
3578
|
const filePath = path7.join(moduleTemplateDir, fileName);
|
|
3292
|
-
await
|
|
3579
|
+
await fs5.writeFile(filePath, content, "utf-8");
|
|
3293
3580
|
}
|
|
3294
3581
|
}
|
|
3295
3582
|
/**
|
|
@@ -3298,7 +3585,7 @@ var TemplateManager = class {
|
|
|
3298
3585
|
async getTemplate(moduleName, fileName) {
|
|
3299
3586
|
try {
|
|
3300
3587
|
const filePath = path7.join(this.templatesDir, moduleName, fileName);
|
|
3301
|
-
return await
|
|
3588
|
+
return await fs5.readFile(filePath, "utf-8");
|
|
3302
3589
|
} catch {
|
|
3303
3590
|
return null;
|
|
3304
3591
|
}
|
|
@@ -3323,7 +3610,7 @@ var TemplateManager = class {
|
|
|
3323
3610
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3324
3611
|
};
|
|
3325
3612
|
const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
|
|
3326
|
-
await
|
|
3613
|
+
await fs5.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
3327
3614
|
}
|
|
3328
3615
|
/**
|
|
3329
3616
|
* Get module manifest
|
|
@@ -3331,7 +3618,7 @@ var TemplateManager = class {
|
|
|
3331
3618
|
async getManifest(moduleName) {
|
|
3332
3619
|
try {
|
|
3333
3620
|
const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
|
|
3334
|
-
const content = await
|
|
3621
|
+
const content = await fs5.readFile(manifestPath, "utf-8");
|
|
3335
3622
|
return JSON.parse(content);
|
|
3336
3623
|
} catch {
|
|
3337
3624
|
return null;
|
|
@@ -3369,7 +3656,7 @@ var TemplateManager = class {
|
|
|
3369
3656
|
});
|
|
3370
3657
|
continue;
|
|
3371
3658
|
}
|
|
3372
|
-
const currentContent = await
|
|
3659
|
+
const currentContent = await fs5.readFile(filePath, "utf-8");
|
|
3373
3660
|
const currentHash = this.hashContent(currentContent);
|
|
3374
3661
|
results.push({
|
|
3375
3662
|
fileName,
|
|
@@ -3393,15 +3680,15 @@ var TemplateManager = class {
|
|
|
3393
3680
|
* Copy directory recursively
|
|
3394
3681
|
*/
|
|
3395
3682
|
async copyDirectory(src, dest) {
|
|
3396
|
-
await
|
|
3397
|
-
const entries = await
|
|
3683
|
+
await fs5.mkdir(dest, { recursive: true });
|
|
3684
|
+
const entries = await fs5.readdir(src, { withFileTypes: true });
|
|
3398
3685
|
for (const entry of entries) {
|
|
3399
3686
|
const srcPath = path7.join(src, entry.name);
|
|
3400
3687
|
const destPath = path7.join(dest, entry.name);
|
|
3401
3688
|
if (entry.isDirectory()) {
|
|
3402
3689
|
await this.copyDirectory(srcPath, destPath);
|
|
3403
3690
|
} else {
|
|
3404
|
-
await
|
|
3691
|
+
await fs5.copyFile(srcPath, destPath);
|
|
3405
3692
|
}
|
|
3406
3693
|
}
|
|
3407
3694
|
}
|
|
@@ -3499,12 +3786,11 @@ var TemplateManager = class {
|
|
|
3499
3786
|
manifest.files = fileHashes;
|
|
3500
3787
|
manifest.updatedAt = /* @__PURE__ */ new Date();
|
|
3501
3788
|
const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
|
|
3502
|
-
await
|
|
3789
|
+
await fs5.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
3503
3790
|
}
|
|
3504
3791
|
};
|
|
3505
3792
|
|
|
3506
3793
|
// src/cli/utils/interactive-prompt.ts
|
|
3507
|
-
init_esm_shims();
|
|
3508
3794
|
import inquirer3 from "inquirer";
|
|
3509
3795
|
import chalk6 from "chalk";
|
|
3510
3796
|
var InteractivePrompt = class {
|
|
@@ -3690,7 +3976,6 @@ var InteractivePrompt = class {
|
|
|
3690
3976
|
};
|
|
3691
3977
|
|
|
3692
3978
|
// src/cli/commands/add-module.ts
|
|
3693
|
-
init_error_handler();
|
|
3694
3979
|
var AVAILABLE_MODULES = {
|
|
3695
3980
|
auth: {
|
|
3696
3981
|
name: "Authentication",
|
|
@@ -3891,7 +4176,7 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
|
|
|
3891
4176
|
const backupPath = await templateManager.createBackup(moduleName, moduleDir);
|
|
3892
4177
|
InteractivePrompt.showBackupCreated(backupPath);
|
|
3893
4178
|
}
|
|
3894
|
-
await
|
|
4179
|
+
await fs6.rm(moduleDir, { recursive: true, force: true });
|
|
3895
4180
|
await ensureDir(moduleDir);
|
|
3896
4181
|
await generateModuleFiles(moduleName, moduleDir);
|
|
3897
4182
|
const files = await getModuleFiles(moduleName, moduleDir);
|
|
@@ -4244,7 +4529,7 @@ async function findServercraftModules() {
|
|
|
4244
4529
|
];
|
|
4245
4530
|
for (const p of possiblePaths) {
|
|
4246
4531
|
try {
|
|
4247
|
-
const stats = await
|
|
4532
|
+
const stats = await fs6.stat(p);
|
|
4248
4533
|
if (stats.isDirectory()) {
|
|
4249
4534
|
return p;
|
|
4250
4535
|
}
|
|
@@ -4294,26 +4579,26 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
4294
4579
|
}
|
|
4295
4580
|
}
|
|
4296
4581
|
async function copyModuleFromSource(sourceDir, targetDir) {
|
|
4297
|
-
const entries = await
|
|
4582
|
+
const entries = await fs6.readdir(sourceDir, { withFileTypes: true });
|
|
4298
4583
|
for (const entry of entries) {
|
|
4299
4584
|
const sourcePath = path8.join(sourceDir, entry.name);
|
|
4300
4585
|
const targetPath = path8.join(targetDir, entry.name);
|
|
4301
4586
|
if (entry.isDirectory()) {
|
|
4302
|
-
await
|
|
4587
|
+
await fs6.mkdir(targetPath, { recursive: true });
|
|
4303
4588
|
await copyModuleFromSource(sourcePath, targetPath);
|
|
4304
4589
|
} else {
|
|
4305
|
-
await
|
|
4590
|
+
await fs6.copyFile(sourcePath, targetPath);
|
|
4306
4591
|
}
|
|
4307
4592
|
}
|
|
4308
4593
|
}
|
|
4309
4594
|
async function getModuleFiles(moduleName, moduleDir) {
|
|
4310
4595
|
const files = {};
|
|
4311
|
-
const entries = await
|
|
4596
|
+
const entries = await fs6.readdir(moduleDir);
|
|
4312
4597
|
for (const entry of entries) {
|
|
4313
4598
|
const filePath = path8.join(moduleDir, entry);
|
|
4314
|
-
const stat2 = await
|
|
4599
|
+
const stat2 = await fs6.stat(filePath);
|
|
4315
4600
|
if (stat2.isFile() && entry.endsWith(".ts")) {
|
|
4316
|
-
const content = await
|
|
4601
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
4317
4602
|
files[entry] = content;
|
|
4318
4603
|
}
|
|
4319
4604
|
}
|
|
@@ -4329,7 +4614,7 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
|
4329
4614
|
console.log(chalk7.yellow(`
|
|
4330
4615
|
\u{1F4C4} ${file.fileName}:`));
|
|
4331
4616
|
const currentPath = path8.join(moduleDir, file.fileName);
|
|
4332
|
-
const currentContent = await
|
|
4617
|
+
const currentContent = await fs6.readFile(currentPath, "utf-8");
|
|
4333
4618
|
const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
|
|
4334
4619
|
if (originalContent) {
|
|
4335
4620
|
const diff = templateManager.generateDiff(originalContent, currentContent);
|
|
@@ -4343,9 +4628,9 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4343
4628
|
const newFiles = {};
|
|
4344
4629
|
const templateDir = path8.join(templateManager["templatesDir"], moduleName);
|
|
4345
4630
|
try {
|
|
4346
|
-
const entries = await
|
|
4631
|
+
const entries = await fs6.readdir(templateDir);
|
|
4347
4632
|
for (const entry of entries) {
|
|
4348
|
-
const content = await
|
|
4633
|
+
const content = await fs6.readFile(path8.join(templateDir, entry), "utf-8");
|
|
4349
4634
|
newFiles[entry] = content;
|
|
4350
4635
|
}
|
|
4351
4636
|
} catch {
|
|
@@ -4376,7 +4661,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4376
4661
|
} else if (batchAction === "overwrite-all") {
|
|
4377
4662
|
fileAction = "overwrite";
|
|
4378
4663
|
} else {
|
|
4379
|
-
const currentContent = await
|
|
4664
|
+
const currentContent = await fs6.readFile(filePath, "utf-8");
|
|
4380
4665
|
const yourLines = currentContent.split("\n").length;
|
|
4381
4666
|
const newLines = newContent.split("\n").length;
|
|
4382
4667
|
const choice = await InteractivePrompt.askFileAction(
|
|
@@ -4400,20 +4685,20 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4400
4685
|
continue;
|
|
4401
4686
|
}
|
|
4402
4687
|
if (fileAction === "overwrite") {
|
|
4403
|
-
await
|
|
4688
|
+
await fs6.writeFile(filePath, newContent, "utf-8");
|
|
4404
4689
|
stats.overwritten++;
|
|
4405
4690
|
continue;
|
|
4406
4691
|
}
|
|
4407
4692
|
if (fileAction === "merge") {
|
|
4408
4693
|
const originalContent = await templateManager.getTemplate(moduleName, fileName);
|
|
4409
|
-
const currentContent = await
|
|
4694
|
+
const currentContent = await fs6.readFile(filePath, "utf-8");
|
|
4410
4695
|
if (originalContent) {
|
|
4411
4696
|
const mergeResult = await templateManager.mergeFiles(
|
|
4412
4697
|
originalContent,
|
|
4413
4698
|
currentContent,
|
|
4414
4699
|
newContent
|
|
4415
4700
|
);
|
|
4416
|
-
await
|
|
4701
|
+
await fs6.writeFile(filePath, mergeResult.merged, "utf-8");
|
|
4417
4702
|
if (mergeResult.hasConflicts) {
|
|
4418
4703
|
stats.conflicts++;
|
|
4419
4704
|
InteractivePrompt.displayConflicts(mergeResult.conflicts);
|
|
@@ -4421,7 +4706,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4421
4706
|
stats.merged++;
|
|
4422
4707
|
}
|
|
4423
4708
|
} else {
|
|
4424
|
-
await
|
|
4709
|
+
await fs6.writeFile(filePath, newContent, "utf-8");
|
|
4425
4710
|
stats.overwritten++;
|
|
4426
4711
|
}
|
|
4427
4712
|
}
|
|
@@ -4432,7 +4717,6 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4432
4717
|
}
|
|
4433
4718
|
|
|
4434
4719
|
// src/cli/commands/db.ts
|
|
4435
|
-
init_esm_shims();
|
|
4436
4720
|
import { Command as Command4 } from "commander";
|
|
4437
4721
|
import { execSync as execSync2, spawn } from "child_process";
|
|
4438
4722
|
import ora4 from "ora";
|
|
@@ -4526,25 +4810,21 @@ dbCommand.command("status").description("Show migration status").action(async ()
|
|
|
4526
4810
|
});
|
|
4527
4811
|
|
|
4528
4812
|
// src/cli/commands/docs.ts
|
|
4529
|
-
init_esm_shims();
|
|
4530
4813
|
import { Command as Command5 } from "commander";
|
|
4531
4814
|
import path10 from "path";
|
|
4532
|
-
import
|
|
4815
|
+
import fs8 from "fs/promises";
|
|
4533
4816
|
import ora6 from "ora";
|
|
4534
4817
|
import chalk9 from "chalk";
|
|
4535
4818
|
|
|
4536
4819
|
// src/cli/utils/docs-generator.ts
|
|
4537
|
-
|
|
4538
|
-
import fs6 from "fs/promises";
|
|
4820
|
+
import fs7 from "fs/promises";
|
|
4539
4821
|
import path9 from "path";
|
|
4540
4822
|
import ora5 from "ora";
|
|
4541
4823
|
|
|
4542
4824
|
// src/core/server.ts
|
|
4543
|
-
init_esm_shims();
|
|
4544
4825
|
import Fastify from "fastify";
|
|
4545
4826
|
|
|
4546
4827
|
// src/core/logger.ts
|
|
4547
|
-
init_esm_shims();
|
|
4548
4828
|
import pino from "pino";
|
|
4549
4829
|
var defaultConfig = {
|
|
4550
4830
|
level: process.env.LOG_LEVEL || "info",
|
|
@@ -4677,14 +4957,7 @@ function createServer(config2 = {}) {
|
|
|
4677
4957
|
return new Server(config2);
|
|
4678
4958
|
}
|
|
4679
4959
|
|
|
4680
|
-
// src/middleware/index.ts
|
|
4681
|
-
init_esm_shims();
|
|
4682
|
-
|
|
4683
|
-
// src/middleware/error-handler.ts
|
|
4684
|
-
init_esm_shims();
|
|
4685
|
-
|
|
4686
4960
|
// src/utils/errors.ts
|
|
4687
|
-
init_esm_shims();
|
|
4688
4961
|
var AppError = class _AppError extends Error {
|
|
4689
4962
|
statusCode;
|
|
4690
4963
|
isOperational;
|
|
@@ -4738,11 +5011,7 @@ function isAppError(error2) {
|
|
|
4738
5011
|
return error2 instanceof AppError;
|
|
4739
5012
|
}
|
|
4740
5013
|
|
|
4741
|
-
// src/config/index.ts
|
|
4742
|
-
init_esm_shims();
|
|
4743
|
-
|
|
4744
5014
|
// src/config/env.ts
|
|
4745
|
-
init_esm_shims();
|
|
4746
5015
|
import { z } from "zod";
|
|
4747
5016
|
import dotenv from "dotenv";
|
|
4748
5017
|
dotenv.config();
|
|
@@ -4888,7 +5157,6 @@ function registerErrorHandler(app) {
|
|
|
4888
5157
|
}
|
|
4889
5158
|
|
|
4890
5159
|
// src/middleware/security.ts
|
|
4891
|
-
init_esm_shims();
|
|
4892
5160
|
import helmet from "@fastify/helmet";
|
|
4893
5161
|
import cors from "@fastify/cors";
|
|
4894
5162
|
import rateLimit from "@fastify/rate-limit";
|
|
@@ -4948,11 +5216,7 @@ async function registerSecurity(app, options = {}) {
|
|
|
4948
5216
|
}
|
|
4949
5217
|
}
|
|
4950
5218
|
|
|
4951
|
-
// src/modules/swagger/index.ts
|
|
4952
|
-
init_esm_shims();
|
|
4953
|
-
|
|
4954
5219
|
// src/modules/swagger/swagger.service.ts
|
|
4955
|
-
init_esm_shims();
|
|
4956
5220
|
import swagger from "@fastify/swagger";
|
|
4957
5221
|
import swaggerUi from "@fastify/swagger-ui";
|
|
4958
5222
|
var defaultConfig3 = {
|
|
@@ -5012,16 +5276,11 @@ async function registerSwagger(app, customConfig) {
|
|
|
5012
5276
|
logger.info("Swagger documentation registered at /docs");
|
|
5013
5277
|
}
|
|
5014
5278
|
|
|
5015
|
-
// src/modules/swagger/schema-builder.ts
|
|
5016
|
-
init_esm_shims();
|
|
5017
|
-
|
|
5018
5279
|
// src/modules/auth/index.ts
|
|
5019
|
-
init_esm_shims();
|
|
5020
5280
|
import jwt from "@fastify/jwt";
|
|
5021
5281
|
import cookie from "@fastify/cookie";
|
|
5022
5282
|
|
|
5023
5283
|
// src/modules/auth/auth.service.ts
|
|
5024
|
-
init_esm_shims();
|
|
5025
5284
|
import bcrypt from "bcryptjs";
|
|
5026
5285
|
import { Redis } from "ioredis";
|
|
5027
5286
|
var AuthService = class {
|
|
@@ -5220,11 +5479,7 @@ function createAuthService(app) {
|
|
|
5220
5479
|
return new AuthService(app);
|
|
5221
5480
|
}
|
|
5222
5481
|
|
|
5223
|
-
// src/modules/auth/auth.controller.ts
|
|
5224
|
-
init_esm_shims();
|
|
5225
|
-
|
|
5226
5482
|
// src/modules/auth/schemas.ts
|
|
5227
|
-
init_esm_shims();
|
|
5228
5483
|
import { z as z2 } from "zod";
|
|
5229
5484
|
var loginSchema = z2.object({
|
|
5230
5485
|
email: z2.string().email("Invalid email address"),
|
|
@@ -5251,7 +5506,6 @@ var changePasswordSchema = z2.object({
|
|
|
5251
5506
|
});
|
|
5252
5507
|
|
|
5253
5508
|
// src/utils/response.ts
|
|
5254
|
-
init_esm_shims();
|
|
5255
5509
|
function success2(reply, data, statusCode = 200) {
|
|
5256
5510
|
const response = {
|
|
5257
5511
|
success: true,
|
|
@@ -5267,7 +5521,6 @@ function noContent(reply) {
|
|
|
5267
5521
|
}
|
|
5268
5522
|
|
|
5269
5523
|
// src/modules/validation/validator.ts
|
|
5270
|
-
init_esm_shims();
|
|
5271
5524
|
import { z as z3 } from "zod";
|
|
5272
5525
|
function validateBody(schema, data) {
|
|
5273
5526
|
const result = schema.safeParse(data);
|
|
@@ -5286,11 +5539,11 @@ function validateQuery(schema, data) {
|
|
|
5286
5539
|
function formatZodErrors(error2) {
|
|
5287
5540
|
const errors = {};
|
|
5288
5541
|
for (const issue of error2.issues) {
|
|
5289
|
-
const
|
|
5290
|
-
if (!errors[
|
|
5291
|
-
errors[
|
|
5542
|
+
const path15 = issue.path.join(".") || "root";
|
|
5543
|
+
if (!errors[path15]) {
|
|
5544
|
+
errors[path15] = [];
|
|
5292
5545
|
}
|
|
5293
|
-
errors[
|
|
5546
|
+
errors[path15].push(issue.message);
|
|
5294
5547
|
}
|
|
5295
5548
|
return errors;
|
|
5296
5549
|
}
|
|
@@ -5438,11 +5691,7 @@ function createAuthController(authService, userService) {
|
|
|
5438
5691
|
return new AuthController(authService, userService);
|
|
5439
5692
|
}
|
|
5440
5693
|
|
|
5441
|
-
// src/modules/auth/auth.routes.ts
|
|
5442
|
-
init_esm_shims();
|
|
5443
|
-
|
|
5444
5694
|
// src/modules/auth/auth.middleware.ts
|
|
5445
|
-
init_esm_shims();
|
|
5446
5695
|
function createAuthMiddleware(authService) {
|
|
5447
5696
|
return async function authenticate(request, _reply) {
|
|
5448
5697
|
const authHeader = request.headers.authorization;
|
|
@@ -5485,14 +5734,7 @@ function registerAuthRoutes(app, controller, authService) {
|
|
|
5485
5734
|
);
|
|
5486
5735
|
}
|
|
5487
5736
|
|
|
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
5737
|
// src/database/prisma.ts
|
|
5495
|
-
init_esm_shims();
|
|
5496
5738
|
import { PrismaClient } from "@prisma/client";
|
|
5497
5739
|
var prismaClientSingleton = () => {
|
|
5498
5740
|
return new PrismaClient({
|
|
@@ -5506,7 +5748,6 @@ if (!isProduction()) {
|
|
|
5506
5748
|
}
|
|
5507
5749
|
|
|
5508
5750
|
// src/utils/pagination.ts
|
|
5509
|
-
init_esm_shims();
|
|
5510
5751
|
var DEFAULT_PAGE = 1;
|
|
5511
5752
|
var DEFAULT_LIMIT = 20;
|
|
5512
5753
|
var MAX_LIMIT = 100;
|
|
@@ -5771,7 +6012,6 @@ function createUserRepository() {
|
|
|
5771
6012
|
}
|
|
5772
6013
|
|
|
5773
6014
|
// src/modules/user/types.ts
|
|
5774
|
-
init_esm_shims();
|
|
5775
6015
|
var DEFAULT_ROLE_PERMISSIONS = {
|
|
5776
6016
|
user: ["profile:read", "profile:update"],
|
|
5777
6017
|
moderator: [
|
|
@@ -5902,9 +6142,6 @@ function createUserService(repository) {
|
|
|
5902
6142
|
return new UserService(repository || createUserRepository());
|
|
5903
6143
|
}
|
|
5904
6144
|
|
|
5905
|
-
// src/modules/auth/types.ts
|
|
5906
|
-
init_esm_shims();
|
|
5907
|
-
|
|
5908
6145
|
// src/modules/auth/index.ts
|
|
5909
6146
|
async function registerAuthModule(app) {
|
|
5910
6147
|
await app.register(jwt, {
|
|
@@ -5924,14 +6161,7 @@ async function registerAuthModule(app) {
|
|
|
5924
6161
|
logger.info("Auth module registered");
|
|
5925
6162
|
}
|
|
5926
6163
|
|
|
5927
|
-
// src/modules/user/index.ts
|
|
5928
|
-
init_esm_shims();
|
|
5929
|
-
|
|
5930
|
-
// src/modules/user/user.controller.ts
|
|
5931
|
-
init_esm_shims();
|
|
5932
|
-
|
|
5933
6164
|
// src/modules/user/schemas.ts
|
|
5934
|
-
init_esm_shims();
|
|
5935
6165
|
import { z as z4 } from "zod";
|
|
5936
6166
|
var userStatusEnum = z4.enum(["active", "inactive", "suspended", "banned"]);
|
|
5937
6167
|
var userRoleEnum = z4.enum(["user", "admin", "moderator", "super_admin"]);
|
|
@@ -6058,7 +6288,6 @@ function createUserController(userService) {
|
|
|
6058
6288
|
}
|
|
6059
6289
|
|
|
6060
6290
|
// src/modules/user/user.routes.ts
|
|
6061
|
-
init_esm_shims();
|
|
6062
6291
|
var idParamsSchema = {
|
|
6063
6292
|
type: "object",
|
|
6064
6293
|
properties: {
|
|
@@ -6148,8 +6377,8 @@ async function generateDocs(outputPath = "openapi.json", silent = false) {
|
|
|
6148
6377
|
await app.ready();
|
|
6149
6378
|
const spec = app.swagger();
|
|
6150
6379
|
const absoluteOutput = path9.resolve(outputPath);
|
|
6151
|
-
await
|
|
6152
|
-
await
|
|
6380
|
+
await fs7.mkdir(path9.dirname(absoluteOutput), { recursive: true });
|
|
6381
|
+
await fs7.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
|
|
6153
6382
|
spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
|
|
6154
6383
|
await app.close();
|
|
6155
6384
|
return absoluteOutput;
|
|
@@ -6165,10 +6394,10 @@ docsCommand.command("generate").alias("gen").description("Generate OpenAPI/Swagg
|
|
|
6165
6394
|
try {
|
|
6166
6395
|
const outputPath = await generateDocs(options.output, false);
|
|
6167
6396
|
if (options.format === "yaml") {
|
|
6168
|
-
const jsonContent = await
|
|
6397
|
+
const jsonContent = await fs8.readFile(outputPath, "utf-8");
|
|
6169
6398
|
const spec = JSON.parse(jsonContent);
|
|
6170
6399
|
const yamlPath = outputPath.replace(".json", ".yaml");
|
|
6171
|
-
await
|
|
6400
|
+
await fs8.writeFile(yamlPath, jsonToYaml(spec));
|
|
6172
6401
|
success(`YAML documentation generated: ${yamlPath}`);
|
|
6173
6402
|
}
|
|
6174
6403
|
console.log("\n\u{1F4DA} Documentation URLs:");
|
|
@@ -6195,12 +6424,12 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
6195
6424
|
const projectRoot = getProjectRoot();
|
|
6196
6425
|
const specPath = path10.join(projectRoot, "openapi.json");
|
|
6197
6426
|
try {
|
|
6198
|
-
await
|
|
6427
|
+
await fs8.access(specPath);
|
|
6199
6428
|
} catch {
|
|
6200
6429
|
spinner.text = "Generating OpenAPI spec first...";
|
|
6201
6430
|
await generateDocs("openapi.json", true);
|
|
6202
6431
|
}
|
|
6203
|
-
const specContent = await
|
|
6432
|
+
const specContent = await fs8.readFile(specPath, "utf-8");
|
|
6204
6433
|
const spec = JSON.parse(specContent);
|
|
6205
6434
|
let output;
|
|
6206
6435
|
let defaultName;
|
|
@@ -6221,7 +6450,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
6221
6450
|
throw new Error(`Unknown format: ${options.format}`);
|
|
6222
6451
|
}
|
|
6223
6452
|
const outPath = path10.join(projectRoot, options.output || defaultName);
|
|
6224
|
-
await
|
|
6453
|
+
await fs8.writeFile(outPath, output);
|
|
6225
6454
|
spinner.succeed(`Exported to: ${options.output || defaultName}`);
|
|
6226
6455
|
if (options.format === "postman") {
|
|
6227
6456
|
info("\n Import in Postman: File > Import > Select file");
|
|
@@ -6236,11 +6465,11 @@ docsCommand.command("status").description("Show documentation status").action(as
|
|
|
6236
6465
|
console.log(chalk9.bold("\n\u{1F4CA} Documentation Status\n"));
|
|
6237
6466
|
const specPath = path10.join(projectRoot, "openapi.json");
|
|
6238
6467
|
try {
|
|
6239
|
-
const stat2 = await
|
|
6468
|
+
const stat2 = await fs8.stat(specPath);
|
|
6240
6469
|
success(
|
|
6241
6470
|
`openapi.json exists (${formatBytes(stat2.size)}, modified ${formatDate(stat2.mtime)})`
|
|
6242
6471
|
);
|
|
6243
|
-
const content = await
|
|
6472
|
+
const content = await fs8.readFile(specPath, "utf-8");
|
|
6244
6473
|
const spec = JSON.parse(content);
|
|
6245
6474
|
const pathCount = Object.keys(spec.paths || {}).length;
|
|
6246
6475
|
info(` ${pathCount} endpoints documented`);
|
|
@@ -6350,10 +6579,9 @@ function formatDate(date) {
|
|
|
6350
6579
|
}
|
|
6351
6580
|
|
|
6352
6581
|
// src/cli/commands/list.ts
|
|
6353
|
-
init_esm_shims();
|
|
6354
6582
|
import { Command as Command6 } from "commander";
|
|
6355
6583
|
import chalk10 from "chalk";
|
|
6356
|
-
import
|
|
6584
|
+
import fs9 from "fs/promises";
|
|
6357
6585
|
var AVAILABLE_MODULES2 = {
|
|
6358
6586
|
// Core
|
|
6359
6587
|
auth: {
|
|
@@ -6468,7 +6696,7 @@ var AVAILABLE_MODULES2 = {
|
|
|
6468
6696
|
async function getInstalledModules() {
|
|
6469
6697
|
try {
|
|
6470
6698
|
const modulesDir = getModulesDir();
|
|
6471
|
-
const entries = await
|
|
6699
|
+
const entries = await fs9.readdir(modulesDir, { withFileTypes: true });
|
|
6472
6700
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6473
6701
|
} catch {
|
|
6474
6702
|
return [];
|
|
@@ -6508,7 +6736,7 @@ var listCommand = new Command6("list").alias("ls").description("List available a
|
|
|
6508
6736
|
if (!byCategory[mod.category]) {
|
|
6509
6737
|
byCategory[mod.category] = [];
|
|
6510
6738
|
}
|
|
6511
|
-
byCategory[mod.category]
|
|
6739
|
+
byCategory[mod.category]?.push({
|
|
6512
6740
|
id: key,
|
|
6513
6741
|
name: mod.name,
|
|
6514
6742
|
description: mod.description,
|
|
@@ -6576,14 +6804,12 @@ var listCommand = new Command6("list").alias("ls").description("List available a
|
|
|
6576
6804
|
);
|
|
6577
6805
|
|
|
6578
6806
|
// src/cli/commands/remove.ts
|
|
6579
|
-
init_esm_shims();
|
|
6580
6807
|
import { Command as Command7 } from "commander";
|
|
6581
6808
|
import path11 from "path";
|
|
6582
6809
|
import ora7 from "ora";
|
|
6583
6810
|
import chalk11 from "chalk";
|
|
6584
|
-
import
|
|
6811
|
+
import fs10 from "fs/promises";
|
|
6585
6812
|
import inquirer4 from "inquirer";
|
|
6586
|
-
init_error_handler();
|
|
6587
6813
|
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
6814
|
const projectError = validateProject();
|
|
6589
6815
|
if (projectError) {
|
|
@@ -6593,20 +6819,17 @@ var removeCommand = new Command7("remove").alias("rm").description("Remove an in
|
|
|
6593
6819
|
console.log(chalk11.bold.cyan("\n\u{1F5D1}\uFE0F ServCraft Module Removal\n"));
|
|
6594
6820
|
const moduleDir = path11.join(getModulesDir(), moduleName);
|
|
6595
6821
|
try {
|
|
6596
|
-
const exists = await
|
|
6822
|
+
const exists = await fs10.access(moduleDir).then(() => true).catch(() => false);
|
|
6597
6823
|
if (!exists) {
|
|
6598
6824
|
displayError(
|
|
6599
|
-
new (
|
|
6600
|
-
`
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
`Check the spelling of the module name`
|
|
6604
|
-
]
|
|
6605
|
-
)
|
|
6825
|
+
new ServCraftError(`Module "${moduleName}" is not installed`, [
|
|
6826
|
+
`Run ${chalk11.cyan("servcraft list --installed")} to see installed modules`,
|
|
6827
|
+
`Check the spelling of the module name`
|
|
6828
|
+
])
|
|
6606
6829
|
);
|
|
6607
6830
|
return;
|
|
6608
6831
|
}
|
|
6609
|
-
const files = await
|
|
6832
|
+
const files = await fs10.readdir(moduleDir);
|
|
6610
6833
|
const fileCount = files.length;
|
|
6611
6834
|
if (!options?.yes) {
|
|
6612
6835
|
console.log(chalk11.yellow(`\u26A0 This will remove the "${moduleName}" module:`));
|
|
@@ -6627,7 +6850,7 @@ var removeCommand = new Command7("remove").alias("rm").description("Remove an in
|
|
|
6627
6850
|
}
|
|
6628
6851
|
}
|
|
6629
6852
|
const spinner = ora7("Removing module...").start();
|
|
6630
|
-
await
|
|
6853
|
+
await fs10.rm(moduleDir, { recursive: true, force: true });
|
|
6631
6854
|
spinner.succeed(`Module "${moduleName}" removed successfully!`);
|
|
6632
6855
|
console.log("\n" + chalk11.bold("\u2713 Removed:"));
|
|
6633
6856
|
success(` src/modules/${moduleName}/ (${fileCount} files)`);
|
|
@@ -6651,15 +6874,697 @@ var removeCommand = new Command7("remove").alias("rm").description("Remove an in
|
|
|
6651
6874
|
});
|
|
6652
6875
|
|
|
6653
6876
|
// src/cli/commands/doctor.ts
|
|
6654
|
-
init_esm_shims();
|
|
6655
6877
|
import { Command as Command8 } from "commander";
|
|
6656
6878
|
import chalk12 from "chalk";
|
|
6879
|
+
import fs11 from "fs/promises";
|
|
6880
|
+
async function checkNodeVersion() {
|
|
6881
|
+
const version = process.version;
|
|
6882
|
+
const major = parseInt(version.slice(1).split(".")[0] || "0", 10);
|
|
6883
|
+
if (major >= 18) {
|
|
6884
|
+
return { name: "Node.js", status: "pass", message: `${version} \u2713` };
|
|
6885
|
+
}
|
|
6886
|
+
return {
|
|
6887
|
+
name: "Node.js",
|
|
6888
|
+
status: "fail",
|
|
6889
|
+
message: `${version} (< 18)`,
|
|
6890
|
+
suggestion: "Upgrade to Node.js 18+"
|
|
6891
|
+
};
|
|
6892
|
+
}
|
|
6893
|
+
async function checkPackageJson() {
|
|
6894
|
+
const checks = [];
|
|
6895
|
+
try {
|
|
6896
|
+
const content = await fs11.readFile("package.json", "utf-8");
|
|
6897
|
+
const pkg = JSON.parse(content);
|
|
6898
|
+
checks.push({ name: "package.json", status: "pass", message: "Found" });
|
|
6899
|
+
if (pkg.dependencies?.fastify) {
|
|
6900
|
+
checks.push({ name: "Fastify", status: "pass", message: "Installed" });
|
|
6901
|
+
} else {
|
|
6902
|
+
checks.push({
|
|
6903
|
+
name: "Fastify",
|
|
6904
|
+
status: "fail",
|
|
6905
|
+
message: "Missing",
|
|
6906
|
+
suggestion: "npm install fastify"
|
|
6907
|
+
});
|
|
6908
|
+
}
|
|
6909
|
+
} catch {
|
|
6910
|
+
checks.push({
|
|
6911
|
+
name: "package.json",
|
|
6912
|
+
status: "fail",
|
|
6913
|
+
message: "Not found",
|
|
6914
|
+
suggestion: "Run servcraft init"
|
|
6915
|
+
});
|
|
6916
|
+
}
|
|
6917
|
+
return checks;
|
|
6918
|
+
}
|
|
6919
|
+
async function checkDirectories() {
|
|
6920
|
+
const checks = [];
|
|
6921
|
+
const dirs = ["src", "node_modules", ".git", ".env"];
|
|
6922
|
+
for (const dir of dirs) {
|
|
6923
|
+
try {
|
|
6924
|
+
await fs11.access(dir);
|
|
6925
|
+
checks.push({ name: dir, status: "pass", message: "Exists" });
|
|
6926
|
+
} catch {
|
|
6927
|
+
const isCritical = dir === "src" || dir === "node_modules";
|
|
6928
|
+
checks.push({
|
|
6929
|
+
name: dir,
|
|
6930
|
+
status: isCritical ? "fail" : "warn",
|
|
6931
|
+
message: "Not found",
|
|
6932
|
+
suggestion: dir === "node_modules" ? "npm install" : dir === ".env" ? "Create .env file" : void 0
|
|
6933
|
+
});
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
return checks;
|
|
6937
|
+
}
|
|
6657
6938
|
var doctorCommand = new Command8("doctor").description("Diagnose project configuration and dependencies").action(async () => {
|
|
6658
|
-
console.log(chalk12.bold.cyan("\
|
|
6939
|
+
console.log(chalk12.bold.cyan("\n\u{1F50D} ServCraft Doctor\n"));
|
|
6940
|
+
const allChecks = [];
|
|
6941
|
+
allChecks.push(await checkNodeVersion());
|
|
6942
|
+
allChecks.push(...await checkPackageJson());
|
|
6943
|
+
allChecks.push(...await checkDirectories());
|
|
6944
|
+
allChecks.forEach((check) => {
|
|
6945
|
+
const icon = check.status === "pass" ? chalk12.green("\u2713") : check.status === "warn" ? chalk12.yellow("\u26A0") : chalk12.red("\u2717");
|
|
6946
|
+
const color = check.status === "pass" ? chalk12.green : check.status === "warn" ? chalk12.yellow : chalk12.red;
|
|
6947
|
+
console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
|
|
6948
|
+
if (check.suggestion) {
|
|
6949
|
+
console.log(chalk12.gray(` \u2192 ${check.suggestion}`));
|
|
6950
|
+
}
|
|
6951
|
+
});
|
|
6952
|
+
const pass = allChecks.filter((c) => c.status === "pass").length;
|
|
6953
|
+
const warn2 = allChecks.filter((c) => c.status === "warn").length;
|
|
6954
|
+
const fail = allChecks.filter((c) => c.status === "fail").length;
|
|
6955
|
+
console.log(chalk12.gray("\n" + "\u2500".repeat(60)));
|
|
6956
|
+
console.log(
|
|
6957
|
+
`
|
|
6958
|
+
${chalk12.green(pass + " passed")} | ${chalk12.yellow(warn2 + " warnings")} | ${chalk12.red(fail + " failed")}
|
|
6959
|
+
`
|
|
6960
|
+
);
|
|
6961
|
+
if (fail === 0 && warn2 === 0) {
|
|
6962
|
+
console.log(chalk12.green.bold("\u2728 Everything looks good!\n"));
|
|
6963
|
+
} else if (fail > 0) {
|
|
6964
|
+
console.log(chalk12.red.bold("\u2717 Fix critical issues before using ServCraft.\n"));
|
|
6965
|
+
} else {
|
|
6966
|
+
console.log(chalk12.yellow.bold("\u26A0 Some warnings, but should work.\n"));
|
|
6967
|
+
}
|
|
6659
6968
|
});
|
|
6660
6969
|
|
|
6970
|
+
// src/cli/commands/update.ts
|
|
6971
|
+
import { Command as Command9 } from "commander";
|
|
6972
|
+
import chalk13 from "chalk";
|
|
6973
|
+
import fs12 from "fs/promises";
|
|
6974
|
+
import path12 from "path";
|
|
6975
|
+
import { fileURLToPath } from "url";
|
|
6976
|
+
import inquirer5 from "inquirer";
|
|
6977
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
6978
|
+
var __dirname2 = path12.dirname(__filename2);
|
|
6979
|
+
var AVAILABLE_MODULES3 = [
|
|
6980
|
+
"auth",
|
|
6981
|
+
"users",
|
|
6982
|
+
"email",
|
|
6983
|
+
"mfa",
|
|
6984
|
+
"oauth",
|
|
6985
|
+
"rate-limit",
|
|
6986
|
+
"cache",
|
|
6987
|
+
"upload",
|
|
6988
|
+
"search",
|
|
6989
|
+
"notification",
|
|
6990
|
+
"webhook",
|
|
6991
|
+
"websocket",
|
|
6992
|
+
"queue",
|
|
6993
|
+
"payment",
|
|
6994
|
+
"i18n",
|
|
6995
|
+
"feature-flag",
|
|
6996
|
+
"analytics",
|
|
6997
|
+
"media-processing",
|
|
6998
|
+
"api-versioning",
|
|
6999
|
+
"audit",
|
|
7000
|
+
"swagger",
|
|
7001
|
+
"validation"
|
|
7002
|
+
];
|
|
7003
|
+
async function getInstalledModules2() {
|
|
7004
|
+
try {
|
|
7005
|
+
const modulesDir = getModulesDir();
|
|
7006
|
+
const entries = await fs12.readdir(modulesDir, { withFileTypes: true });
|
|
7007
|
+
const installedModules = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => AVAILABLE_MODULES3.includes(name));
|
|
7008
|
+
return installedModules;
|
|
7009
|
+
} catch {
|
|
7010
|
+
return [];
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
async function copyModuleFiles(moduleName, _projectRoot) {
|
|
7014
|
+
const cliRoot = path12.resolve(__dirname2, "../../../");
|
|
7015
|
+
const sourceModulePath = path12.join(cliRoot, "src", "modules", moduleName);
|
|
7016
|
+
const targetModulesDir = getModulesDir();
|
|
7017
|
+
const targetModulePath = path12.join(targetModulesDir, moduleName);
|
|
7018
|
+
try {
|
|
7019
|
+
await fs12.access(sourceModulePath);
|
|
7020
|
+
} catch {
|
|
7021
|
+
throw new Error(`Module source not found: ${moduleName}`);
|
|
7022
|
+
}
|
|
7023
|
+
await fs12.cp(sourceModulePath, targetModulePath, { recursive: true });
|
|
7024
|
+
}
|
|
7025
|
+
async function updateModule(moduleName, options) {
|
|
7026
|
+
const projectError = validateProject();
|
|
7027
|
+
if (projectError) {
|
|
7028
|
+
displayError(projectError);
|
|
7029
|
+
return;
|
|
7030
|
+
}
|
|
7031
|
+
const projectRoot = getProjectRoot();
|
|
7032
|
+
const installedModules = await getInstalledModules2();
|
|
7033
|
+
if (!installedModules.includes(moduleName)) {
|
|
7034
|
+
console.log(chalk13.yellow(`
|
|
7035
|
+
\u26A0 Module "${moduleName}" is not installed
|
|
7036
|
+
`));
|
|
7037
|
+
console.log(
|
|
7038
|
+
chalk13.gray(`Run ${chalk13.cyan(`servcraft add ${moduleName}`)} to install it first.
|
|
7039
|
+
`)
|
|
7040
|
+
);
|
|
7041
|
+
return;
|
|
7042
|
+
}
|
|
7043
|
+
if (options.check) {
|
|
7044
|
+
console.log(chalk13.cyan(`
|
|
7045
|
+
\u{1F4E6} Checking updates for "${moduleName}"...
|
|
7046
|
+
`));
|
|
7047
|
+
console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
|
|
7048
|
+
console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7049
|
+
return;
|
|
7050
|
+
}
|
|
7051
|
+
const { confirmed } = await inquirer5.prompt([
|
|
7052
|
+
{
|
|
7053
|
+
type: "confirm",
|
|
7054
|
+
name: "confirmed",
|
|
7055
|
+
message: `Update "${moduleName}" module? This will overwrite existing files.`,
|
|
7056
|
+
default: false
|
|
7057
|
+
}
|
|
7058
|
+
]);
|
|
7059
|
+
if (!confirmed) {
|
|
7060
|
+
console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
|
|
7061
|
+
return;
|
|
7062
|
+
}
|
|
7063
|
+
console.log(chalk13.cyan(`
|
|
7064
|
+
\u{1F504} Updating "${moduleName}" module...
|
|
7065
|
+
`));
|
|
7066
|
+
try {
|
|
7067
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7068
|
+
console.log(chalk13.green(`\u2714 Module "${moduleName}" updated successfully!
|
|
7069
|
+
`));
|
|
7070
|
+
console.log(
|
|
7071
|
+
chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7072
|
+
);
|
|
7073
|
+
} catch (error2) {
|
|
7074
|
+
if (error2 instanceof Error) {
|
|
7075
|
+
console.error(chalk13.red(`
|
|
7076
|
+
\u2717 Failed to update module: ${error2.message}
|
|
7077
|
+
`));
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
async function updateAllModules(options) {
|
|
7082
|
+
const projectError = validateProject();
|
|
7083
|
+
if (projectError) {
|
|
7084
|
+
displayError(projectError);
|
|
7085
|
+
return;
|
|
7086
|
+
}
|
|
7087
|
+
const installedModules = await getInstalledModules2();
|
|
7088
|
+
if (installedModules.length === 0) {
|
|
7089
|
+
console.log(chalk13.yellow("\n\u26A0 No modules installed\n"));
|
|
7090
|
+
console.log(chalk13.gray(`Run ${chalk13.cyan("servcraft list")} to see available modules.
|
|
7091
|
+
`));
|
|
7092
|
+
return;
|
|
7093
|
+
}
|
|
7094
|
+
if (options.check) {
|
|
7095
|
+
console.log(chalk13.cyan("\n\u{1F4E6} Checking updates for all modules...\n"));
|
|
7096
|
+
console.log(chalk13.bold("Installed modules:"));
|
|
7097
|
+
installedModules.forEach((mod) => {
|
|
7098
|
+
console.log(` \u2022 ${chalk13.cyan(mod)}`);
|
|
7099
|
+
});
|
|
7100
|
+
console.log();
|
|
7101
|
+
console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
|
|
7102
|
+
console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7103
|
+
return;
|
|
7104
|
+
}
|
|
7105
|
+
console.log(chalk13.cyan(`
|
|
7106
|
+
\u{1F4E6} Found ${installedModules.length} installed module(s):
|
|
7107
|
+
`));
|
|
7108
|
+
installedModules.forEach((mod) => {
|
|
7109
|
+
console.log(` \u2022 ${chalk13.cyan(mod)}`);
|
|
7110
|
+
});
|
|
7111
|
+
console.log();
|
|
7112
|
+
const { confirmed } = await inquirer5.prompt([
|
|
7113
|
+
{
|
|
7114
|
+
type: "confirm",
|
|
7115
|
+
name: "confirmed",
|
|
7116
|
+
message: "Update all modules? This will overwrite existing files.",
|
|
7117
|
+
default: false
|
|
7118
|
+
}
|
|
7119
|
+
]);
|
|
7120
|
+
if (!confirmed) {
|
|
7121
|
+
console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
|
|
7122
|
+
return;
|
|
7123
|
+
}
|
|
7124
|
+
console.log(chalk13.cyan("\n\u{1F504} Updating all modules...\n"));
|
|
7125
|
+
const projectRoot = getProjectRoot();
|
|
7126
|
+
let successCount = 0;
|
|
7127
|
+
let failCount = 0;
|
|
7128
|
+
for (const moduleName of installedModules) {
|
|
7129
|
+
try {
|
|
7130
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7131
|
+
console.log(chalk13.green(`\u2714 Updated: ${moduleName}`));
|
|
7132
|
+
successCount++;
|
|
7133
|
+
} catch {
|
|
7134
|
+
console.error(chalk13.red(`\u2717 Failed: ${moduleName}`));
|
|
7135
|
+
failCount++;
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
console.log();
|
|
7139
|
+
console.log(
|
|
7140
|
+
chalk13.bold(
|
|
7141
|
+
`
|
|
7142
|
+
\u2714 Update complete: ${chalk13.green(successCount)} succeeded, ${chalk13.red(failCount)} failed
|
|
7143
|
+
`
|
|
7144
|
+
)
|
|
7145
|
+
);
|
|
7146
|
+
if (successCount > 0) {
|
|
7147
|
+
console.log(
|
|
7148
|
+
chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7149
|
+
);
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
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) => {
|
|
7153
|
+
if (moduleName) {
|
|
7154
|
+
await updateModule(moduleName, { check: options?.check });
|
|
7155
|
+
} else {
|
|
7156
|
+
await updateAllModules({ check: options?.check });
|
|
7157
|
+
}
|
|
7158
|
+
});
|
|
7159
|
+
|
|
7160
|
+
// src/cli/commands/completion.ts
|
|
7161
|
+
import { Command as Command10 } from "commander";
|
|
7162
|
+
var bashScript = `
|
|
7163
|
+
# servcraft bash completion script
|
|
7164
|
+
_servcraft_completions() {
|
|
7165
|
+
local cur prev words cword
|
|
7166
|
+
_init_completion || return
|
|
7167
|
+
|
|
7168
|
+
# Main commands
|
|
7169
|
+
local commands="init add generate list remove doctor update completion docs --version --help"
|
|
7170
|
+
|
|
7171
|
+
# Generate subcommands
|
|
7172
|
+
local generate_subcommands="module controller service repository types schema routes m c s r t"
|
|
7173
|
+
|
|
7174
|
+
case "\${words[1]}" in
|
|
7175
|
+
generate|g)
|
|
7176
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7177
|
+
COMPREPLY=( $(compgen -W "\${generate_subcommands}" -- "\${cur}") )
|
|
7178
|
+
fi
|
|
7179
|
+
;;
|
|
7180
|
+
add|remove|rm|update)
|
|
7181
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7182
|
+
# Get available modules
|
|
7183
|
+
local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
|
|
7184
|
+
COMPREPLY=( $(compgen -W "\${modules}" -- "\${cur}") )
|
|
7185
|
+
fi
|
|
7186
|
+
;;
|
|
7187
|
+
completion)
|
|
7188
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7189
|
+
COMPREPLY=( $(compgen -W "bash zsh" -- "\${cur}") )
|
|
7190
|
+
fi
|
|
7191
|
+
;;
|
|
7192
|
+
*)
|
|
7193
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
7194
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
7195
|
+
fi
|
|
7196
|
+
;;
|
|
7197
|
+
esac
|
|
7198
|
+
}
|
|
7199
|
+
|
|
7200
|
+
complete -F _servcraft_completions servcraft
|
|
7201
|
+
`;
|
|
7202
|
+
var zshScript = `
|
|
7203
|
+
#compdef servcraft
|
|
7204
|
+
|
|
7205
|
+
_servcraft() {
|
|
7206
|
+
local context state state_descr line
|
|
7207
|
+
typeset -A opt_args
|
|
7208
|
+
|
|
7209
|
+
_arguments -C \\
|
|
7210
|
+
'1: :_servcraft_commands' \\
|
|
7211
|
+
'*::arg:->args'
|
|
7212
|
+
|
|
7213
|
+
case $state in
|
|
7214
|
+
args)
|
|
7215
|
+
case $line[1] in
|
|
7216
|
+
generate|g)
|
|
7217
|
+
_servcraft_generate
|
|
7218
|
+
;;
|
|
7219
|
+
add|remove|rm|update)
|
|
7220
|
+
_servcraft_modules
|
|
7221
|
+
;;
|
|
7222
|
+
completion)
|
|
7223
|
+
_arguments '1: :(bash zsh)'
|
|
7224
|
+
;;
|
|
7225
|
+
esac
|
|
7226
|
+
;;
|
|
7227
|
+
esac
|
|
7228
|
+
}
|
|
7229
|
+
|
|
7230
|
+
_servcraft_commands() {
|
|
7231
|
+
local commands
|
|
7232
|
+
commands=(
|
|
7233
|
+
'init:Initialize a new ServCraft project'
|
|
7234
|
+
'add:Add a pre-built module to your project'
|
|
7235
|
+
'generate:Generate code files (controller, service, etc.)'
|
|
7236
|
+
'list:List available and installed modules'
|
|
7237
|
+
'remove:Remove an installed module'
|
|
7238
|
+
'doctor:Diagnose project configuration'
|
|
7239
|
+
'update:Update installed modules'
|
|
7240
|
+
'completion:Generate shell completion scripts'
|
|
7241
|
+
'docs:Open documentation'
|
|
7242
|
+
'--version:Show version'
|
|
7243
|
+
'--help:Show help'
|
|
7244
|
+
)
|
|
7245
|
+
_describe 'command' commands
|
|
7246
|
+
}
|
|
7247
|
+
|
|
7248
|
+
_servcraft_generate() {
|
|
7249
|
+
local subcommands
|
|
7250
|
+
subcommands=(
|
|
7251
|
+
'module:Generate a complete module (controller + service + routes)'
|
|
7252
|
+
'controller:Generate a controller'
|
|
7253
|
+
'service:Generate a service'
|
|
7254
|
+
'repository:Generate a repository'
|
|
7255
|
+
'types:Generate TypeScript types'
|
|
7256
|
+
'schema:Generate validation schema'
|
|
7257
|
+
'routes:Generate routes file'
|
|
7258
|
+
'm:Alias for module'
|
|
7259
|
+
'c:Alias for controller'
|
|
7260
|
+
's:Alias for service'
|
|
7261
|
+
'r:Alias for repository'
|
|
7262
|
+
't:Alias for types'
|
|
7263
|
+
)
|
|
7264
|
+
_describe 'subcommand' subcommands
|
|
7265
|
+
}
|
|
7266
|
+
|
|
7267
|
+
_servcraft_modules() {
|
|
7268
|
+
local modules
|
|
7269
|
+
modules=(
|
|
7270
|
+
'auth:Authentication & Authorization'
|
|
7271
|
+
'cache:Redis caching'
|
|
7272
|
+
'rate-limit:Rate limiting'
|
|
7273
|
+
'notification:Email/SMS notifications'
|
|
7274
|
+
'payment:Payment integration'
|
|
7275
|
+
'oauth:OAuth providers'
|
|
7276
|
+
'mfa:Multi-factor authentication'
|
|
7277
|
+
'queue:Background jobs'
|
|
7278
|
+
'websocket:WebSocket support'
|
|
7279
|
+
'upload:File upload handling'
|
|
7280
|
+
)
|
|
7281
|
+
_describe 'module' modules
|
|
7282
|
+
}
|
|
7283
|
+
|
|
7284
|
+
_servcraft "$@"
|
|
7285
|
+
`;
|
|
7286
|
+
var completionCommand = new Command10("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
|
|
7287
|
+
const shellLower = shell.toLowerCase();
|
|
7288
|
+
if (shellLower === "bash") {
|
|
7289
|
+
console.log(bashScript);
|
|
7290
|
+
} else if (shellLower === "zsh") {
|
|
7291
|
+
console.log(zshScript);
|
|
7292
|
+
} else {
|
|
7293
|
+
console.error(`Unsupported shell: ${shell}`);
|
|
7294
|
+
console.error("Supported shells: bash, zsh");
|
|
7295
|
+
process.exit(1);
|
|
7296
|
+
}
|
|
7297
|
+
});
|
|
7298
|
+
|
|
7299
|
+
// src/cli/commands/scaffold.ts
|
|
7300
|
+
import { Command as Command11 } from "commander";
|
|
7301
|
+
import path13 from "path";
|
|
7302
|
+
import ora8 from "ora";
|
|
7303
|
+
import chalk14 from "chalk";
|
|
7304
|
+
var scaffoldCommand = new Command11("scaffold").description("Generate complete CRUD with Prisma model").argument("<name>", "Resource name (e.g., product, user)").option(
|
|
7305
|
+
"--fields <fields>",
|
|
7306
|
+
'Field definitions: "name:string email:string? age:number category:relation"'
|
|
7307
|
+
).option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("--dry-run", "Preview changes without writing files").action(
|
|
7308
|
+
async (name, options) => {
|
|
7309
|
+
const dryRun = DryRunManager.getInstance();
|
|
7310
|
+
if (options.dryRun) {
|
|
7311
|
+
dryRun.enable();
|
|
7312
|
+
console.log(chalk14.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
7313
|
+
}
|
|
7314
|
+
if (!options.fields) {
|
|
7315
|
+
console.log(chalk14.red("\n\u2717 Error: --fields option is required\n"));
|
|
7316
|
+
console.log(chalk14.gray("Example:"));
|
|
7317
|
+
console.log(
|
|
7318
|
+
chalk14.cyan(
|
|
7319
|
+
' servcraft scaffold product --fields "name:string price:number category:relation"'
|
|
7320
|
+
)
|
|
7321
|
+
);
|
|
7322
|
+
process.exit(1);
|
|
7323
|
+
}
|
|
7324
|
+
const spinner = ora8("Scaffolding resource...").start();
|
|
7325
|
+
try {
|
|
7326
|
+
const fields = parseFields(options.fields || "");
|
|
7327
|
+
if (!fields || fields.length === 0) {
|
|
7328
|
+
spinner.fail("No valid fields provided");
|
|
7329
|
+
console.log(chalk14.gray(`
|
|
7330
|
+
Received: ${options.fields}`));
|
|
7331
|
+
console.log(chalk14.gray(`Parsed: ${JSON.stringify(fields)}`));
|
|
7332
|
+
process.exit(1);
|
|
7333
|
+
}
|
|
7334
|
+
const kebabName = toKebabCase(name);
|
|
7335
|
+
const pascalName = toPascalCase(name);
|
|
7336
|
+
const camelName = toCamelCase(name);
|
|
7337
|
+
const pluralName = pluralize(camelName);
|
|
7338
|
+
const tableName = pluralize(kebabName);
|
|
7339
|
+
const modulesDir = getModulesDir();
|
|
7340
|
+
const moduleDir = path13.join(modulesDir, kebabName);
|
|
7341
|
+
const controllerTpl = await getTemplate("controller", controllerTemplate);
|
|
7342
|
+
const serviceTpl = await getTemplate("service", serviceTemplate);
|
|
7343
|
+
const repositoryTpl = await getTemplate("repository", repositoryTemplate);
|
|
7344
|
+
const routesTpl = await getTemplate("routes", routesTemplate);
|
|
7345
|
+
const moduleIndexTpl = await getTemplate("module-index", moduleIndexTemplate);
|
|
7346
|
+
const files = [
|
|
7347
|
+
{
|
|
7348
|
+
name: `${kebabName}.types.ts`,
|
|
7349
|
+
content: dynamicTypesTemplate(kebabName, pascalName, fields)
|
|
7350
|
+
},
|
|
7351
|
+
{
|
|
7352
|
+
name: `${kebabName}.schemas.ts`,
|
|
7353
|
+
content: dynamicSchemasTemplate(
|
|
7354
|
+
kebabName,
|
|
7355
|
+
pascalName,
|
|
7356
|
+
camelName,
|
|
7357
|
+
fields,
|
|
7358
|
+
options.validator || "zod"
|
|
7359
|
+
)
|
|
7360
|
+
},
|
|
7361
|
+
{
|
|
7362
|
+
name: `${kebabName}.service.ts`,
|
|
7363
|
+
content: serviceTpl(kebabName, pascalName, camelName)
|
|
7364
|
+
},
|
|
7365
|
+
{
|
|
7366
|
+
name: `${kebabName}.controller.ts`,
|
|
7367
|
+
content: controllerTpl(kebabName, pascalName, camelName)
|
|
7368
|
+
},
|
|
7369
|
+
{
|
|
7370
|
+
name: "index.ts",
|
|
7371
|
+
content: moduleIndexTpl(kebabName, pascalName, camelName)
|
|
7372
|
+
},
|
|
7373
|
+
{
|
|
7374
|
+
name: `${kebabName}.repository.ts`,
|
|
7375
|
+
content: repositoryTpl(kebabName, pascalName, camelName, pluralName)
|
|
7376
|
+
},
|
|
7377
|
+
{
|
|
7378
|
+
name: `${kebabName}.routes.ts`,
|
|
7379
|
+
content: routesTpl(kebabName, pascalName, camelName, pluralName)
|
|
7380
|
+
}
|
|
7381
|
+
];
|
|
7382
|
+
for (const file of files) {
|
|
7383
|
+
await writeFile(path13.join(moduleDir, file.name), file.content);
|
|
7384
|
+
}
|
|
7385
|
+
const testDir = path13.join(moduleDir, "__tests__");
|
|
7386
|
+
const controllerTestTpl = await getTemplate("controller-test", controllerTestTemplate);
|
|
7387
|
+
const serviceTestTpl = await getTemplate("service-test", serviceTestTemplate);
|
|
7388
|
+
const integrationTestTpl = await getTemplate("integration-test", integrationTestTemplate);
|
|
7389
|
+
await writeFile(
|
|
7390
|
+
path13.join(testDir, `${kebabName}.controller.test.ts`),
|
|
7391
|
+
controllerTestTpl(kebabName, pascalName, camelName)
|
|
7392
|
+
);
|
|
7393
|
+
await writeFile(
|
|
7394
|
+
path13.join(testDir, `${kebabName}.service.test.ts`),
|
|
7395
|
+
serviceTestTpl(kebabName, pascalName, camelName)
|
|
7396
|
+
);
|
|
7397
|
+
await writeFile(
|
|
7398
|
+
path13.join(testDir, `${kebabName}.integration.test.ts`),
|
|
7399
|
+
integrationTestTpl(kebabName, pascalName, camelName)
|
|
7400
|
+
);
|
|
7401
|
+
spinner.succeed(`Resource "${pascalName}" scaffolded successfully!`);
|
|
7402
|
+
console.log("\n" + "\u2500".repeat(70));
|
|
7403
|
+
info("\u{1F4CB} Prisma model to add to schema.prisma:");
|
|
7404
|
+
console.log(chalk14.gray("\n// Copy this to your schema.prisma file:\n"));
|
|
7405
|
+
console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
|
|
7406
|
+
console.log("\u2500".repeat(70));
|
|
7407
|
+
console.log("\n\u{1F4CB} Fields scaffolded:");
|
|
7408
|
+
fields.forEach((f) => {
|
|
7409
|
+
const opts = [];
|
|
7410
|
+
if (f.isOptional) opts.push("optional");
|
|
7411
|
+
if (f.isArray) opts.push("array");
|
|
7412
|
+
if (f.isUnique) opts.push("unique");
|
|
7413
|
+
if (f.relation) opts.push(`relation: ${f.relation.model}`);
|
|
7414
|
+
const optsStr = opts.length > 0 ? ` (${opts.join(", ")})` : "";
|
|
7415
|
+
success(` ${f.name}: ${f.type}${optsStr}`);
|
|
7416
|
+
});
|
|
7417
|
+
console.log("\n\u{1F4C1} Files created:");
|
|
7418
|
+
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
7419
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
7420
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
7421
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
7422
|
+
console.log("\n\u{1F4CC} Next steps:");
|
|
7423
|
+
info(" 1. Add the Prisma model to your schema.prisma file");
|
|
7424
|
+
info(" 2. Run: npx prisma db push (or prisma migrate dev)");
|
|
7425
|
+
info(" 3. Run: npx prisma generate");
|
|
7426
|
+
info(" 4. Register the module routes in your app");
|
|
7427
|
+
info(" 5. Update the test files with actual test data");
|
|
7428
|
+
console.log(
|
|
7429
|
+
chalk14.gray("\n\u{1F4A1} Tip: Use --dry-run to preview changes before applying them\n")
|
|
7430
|
+
);
|
|
7431
|
+
if (options.dryRun) {
|
|
7432
|
+
dryRun.printSummary();
|
|
7433
|
+
}
|
|
7434
|
+
} catch (error2) {
|
|
7435
|
+
spinner.fail("Failed to scaffold resource");
|
|
7436
|
+
if (error2 instanceof Error) {
|
|
7437
|
+
console.error(chalk14.red(`
|
|
7438
|
+
\u2717 ${error2.message}
|
|
7439
|
+
`));
|
|
7440
|
+
console.error(chalk14.gray(error2.stack));
|
|
7441
|
+
}
|
|
7442
|
+
process.exit(1);
|
|
7443
|
+
}
|
|
7444
|
+
}
|
|
7445
|
+
);
|
|
7446
|
+
|
|
7447
|
+
// src/cli/commands/templates.ts
|
|
7448
|
+
import { Command as Command12 } from "commander";
|
|
7449
|
+
import chalk15 from "chalk";
|
|
7450
|
+
import fs13 from "fs/promises";
|
|
7451
|
+
import path14 from "path";
|
|
7452
|
+
var TEMPLATE_TYPES = [
|
|
7453
|
+
"controller",
|
|
7454
|
+
"service",
|
|
7455
|
+
"repository",
|
|
7456
|
+
"types",
|
|
7457
|
+
"schemas",
|
|
7458
|
+
"routes",
|
|
7459
|
+
"module-index",
|
|
7460
|
+
"controller-test",
|
|
7461
|
+
"service-test",
|
|
7462
|
+
"integration-test"
|
|
7463
|
+
];
|
|
7464
|
+
async function initTemplates() {
|
|
7465
|
+
const projectError = validateProject();
|
|
7466
|
+
if (projectError) {
|
|
7467
|
+
displayError(projectError);
|
|
7468
|
+
return;
|
|
7469
|
+
}
|
|
7470
|
+
const projectRoot = getProjectRoot();
|
|
7471
|
+
const templatesDir = path14.join(projectRoot, ".servcraft", "templates");
|
|
7472
|
+
try {
|
|
7473
|
+
await fs13.mkdir(templatesDir, { recursive: true });
|
|
7474
|
+
console.log(chalk15.cyan("\n\u{1F4C1} Creating custom template directory...\n"));
|
|
7475
|
+
const exampleController = `// Custom controller template
|
|
7476
|
+
// Available variables: name, pascalName, camelName, pluralName
|
|
7477
|
+
export function controllerTemplate(name: string, pascalName: string, camelName: string): string {
|
|
7478
|
+
return \`import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
7479
|
+
import type { \${pascalName}Service } from './\${name}.service.js';
|
|
7480
|
+
|
|
7481
|
+
export class \${pascalName}Controller {
|
|
7482
|
+
constructor(private \${camelName}Service: \${pascalName}Service) {}
|
|
7483
|
+
|
|
7484
|
+
// Add your custom controller methods here
|
|
7485
|
+
async getAll(request: FastifyRequest, reply: FastifyReply) {
|
|
7486
|
+
const data = await this.\${camelName}Service.getAll();
|
|
7487
|
+
return reply.send({ data });
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
\`;
|
|
7491
|
+
}
|
|
7492
|
+
`;
|
|
7493
|
+
await fs13.writeFile(
|
|
7494
|
+
path14.join(templatesDir, "controller.example.ts"),
|
|
7495
|
+
exampleController,
|
|
7496
|
+
"utf-8"
|
|
7497
|
+
);
|
|
7498
|
+
console.log(chalk15.green("\u2714 Created template directory: .servcraft/templates/"));
|
|
7499
|
+
console.log(chalk15.green("\u2714 Created example template: controller.example.ts\n"));
|
|
7500
|
+
console.log(chalk15.bold("\u{1F4CB} Available template types:\n"));
|
|
7501
|
+
TEMPLATE_TYPES.forEach((type) => {
|
|
7502
|
+
console.log(chalk15.gray(` \u2022 ${type}.ts`));
|
|
7503
|
+
});
|
|
7504
|
+
console.log(chalk15.yellow("\n\u{1F4A1} To customize a template:"));
|
|
7505
|
+
console.log(chalk15.gray(" 1. Copy the example template"));
|
|
7506
|
+
console.log(chalk15.gray(" 2. Rename it (remove .example)"));
|
|
7507
|
+
console.log(chalk15.gray(" 3. Modify the template code"));
|
|
7508
|
+
console.log(chalk15.gray(" 4. Use --template flag when generating\n"));
|
|
7509
|
+
} catch (error2) {
|
|
7510
|
+
if (error2 instanceof Error) {
|
|
7511
|
+
console.error(chalk15.red(`
|
|
7512
|
+
\u2717 Failed to initialize templates: ${error2.message}
|
|
7513
|
+
`));
|
|
7514
|
+
}
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
async function listTemplates() {
|
|
7518
|
+
const projectError = validateProject();
|
|
7519
|
+
if (projectError) {
|
|
7520
|
+
displayError(projectError);
|
|
7521
|
+
return;
|
|
7522
|
+
}
|
|
7523
|
+
const projectRoot = getProjectRoot();
|
|
7524
|
+
const projectTemplatesDir = path14.join(projectRoot, ".servcraft", "templates");
|
|
7525
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
7526
|
+
const userTemplatesDir = path14.join(homeDir, ".servcraft", "templates");
|
|
7527
|
+
console.log(chalk15.bold.cyan("\n\u{1F4CB} Available Templates\n"));
|
|
7528
|
+
console.log(chalk15.bold("Project templates (.servcraft/templates/):"));
|
|
7529
|
+
try {
|
|
7530
|
+
const files = await fs13.readdir(projectTemplatesDir);
|
|
7531
|
+
const templates = files.filter((f) => f.endsWith(".ts") && !f.endsWith(".example.ts"));
|
|
7532
|
+
if (templates.length > 0) {
|
|
7533
|
+
templates.forEach((t) => {
|
|
7534
|
+
console.log(chalk15.green(` \u2713 ${t}`));
|
|
7535
|
+
});
|
|
7536
|
+
} else {
|
|
7537
|
+
console.log(chalk15.gray(" (none)"));
|
|
7538
|
+
}
|
|
7539
|
+
} catch {
|
|
7540
|
+
console.log(chalk15.gray(" (directory not found)"));
|
|
7541
|
+
}
|
|
7542
|
+
console.log(chalk15.bold("\nUser templates (~/.servcraft/templates/):"));
|
|
7543
|
+
try {
|
|
7544
|
+
const files = await fs13.readdir(userTemplatesDir);
|
|
7545
|
+
const templates = files.filter((f) => f.endsWith(".ts") && !f.endsWith(".example.ts"));
|
|
7546
|
+
if (templates.length > 0) {
|
|
7547
|
+
templates.forEach((t) => {
|
|
7548
|
+
console.log(chalk15.green(` \u2713 ${t}`));
|
|
7549
|
+
});
|
|
7550
|
+
} else {
|
|
7551
|
+
console.log(chalk15.gray(" (none)"));
|
|
7552
|
+
}
|
|
7553
|
+
} catch {
|
|
7554
|
+
console.log(chalk15.gray(" (directory not found)"));
|
|
7555
|
+
}
|
|
7556
|
+
console.log(chalk15.bold("\nBuilt-in templates:"));
|
|
7557
|
+
TEMPLATE_TYPES.forEach((t) => {
|
|
7558
|
+
console.log(chalk15.cyan(` \u2022 ${t}.ts`));
|
|
7559
|
+
});
|
|
7560
|
+
console.log(chalk15.gray('\n\u{1F4A1} Run "servcraft templates init" to create custom templates\n'));
|
|
7561
|
+
}
|
|
7562
|
+
var templatesCommand = new Command12("templates").description("Manage code generation templates").addCommand(
|
|
7563
|
+
new Command12("init").description("Initialize custom templates directory").action(initTemplates)
|
|
7564
|
+
).addCommand(new Command12("list").description("List available templates").action(listTemplates));
|
|
7565
|
+
|
|
6661
7566
|
// src/cli/index.ts
|
|
6662
|
-
var program = new
|
|
7567
|
+
var program = new Command13();
|
|
6663
7568
|
program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
|
|
6664
7569
|
program.addCommand(initCommand);
|
|
6665
7570
|
program.addCommand(generateCommand);
|
|
@@ -6669,5 +7574,9 @@ program.addCommand(docsCommand);
|
|
|
6669
7574
|
program.addCommand(listCommand);
|
|
6670
7575
|
program.addCommand(removeCommand);
|
|
6671
7576
|
program.addCommand(doctorCommand);
|
|
7577
|
+
program.addCommand(updateCommand);
|
|
7578
|
+
program.addCommand(completionCommand);
|
|
7579
|
+
program.addCommand(scaffoldCommand);
|
|
7580
|
+
program.addCommand(templatesCommand);
|
|
6672
7581
|
program.parse();
|
|
6673
7582
|
//# sourceMappingURL=index.js.map
|