servcraft 0.1.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +9 -4
- package/README.md +63 -2
- package/ROADMAP.md +86 -41
- package/dist/cli/index.cjs +1510 -172
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1516 -172
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/add-module.ts +36 -7
- package/src/cli/commands/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/generate.ts +73 -1
- package/src/cli/commands/init.ts +29 -10
- package/src/cli/commands/list.ts +274 -0
- package/src/cli/commands/remove.ts +102 -0
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +20 -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/dry-run.ts +155 -0
- package/src/cli/utils/error-handler.ts +184 -0
- package/src/cli/utils/helpers.ts +13 -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,21 +1,110 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/cli/index.ts
|
|
4
|
-
import { Command as
|
|
10
|
+
import { Command as Command11 } from "commander";
|
|
5
11
|
|
|
6
12
|
// src/cli/commands/init.ts
|
|
7
13
|
import { Command } from "commander";
|
|
8
|
-
import
|
|
14
|
+
import path3 from "path";
|
|
9
15
|
import fs2 from "fs/promises";
|
|
10
16
|
import ora from "ora";
|
|
11
17
|
import inquirer from "inquirer";
|
|
12
|
-
import
|
|
18
|
+
import chalk3 from "chalk";
|
|
13
19
|
import { execSync } from "child_process";
|
|
14
20
|
|
|
15
21
|
// src/cli/utils/helpers.ts
|
|
16
22
|
import fs from "fs/promises";
|
|
17
|
-
import
|
|
23
|
+
import path2 from "path";
|
|
24
|
+
import chalk2 from "chalk";
|
|
25
|
+
|
|
26
|
+
// src/cli/utils/dry-run.ts
|
|
18
27
|
import chalk from "chalk";
|
|
28
|
+
import path from "path";
|
|
29
|
+
var DryRunManager = class _DryRunManager {
|
|
30
|
+
static instance;
|
|
31
|
+
enabled = false;
|
|
32
|
+
operations = [];
|
|
33
|
+
constructor() {
|
|
34
|
+
}
|
|
35
|
+
static getInstance() {
|
|
36
|
+
if (!_DryRunManager.instance) {
|
|
37
|
+
_DryRunManager.instance = new _DryRunManager();
|
|
38
|
+
}
|
|
39
|
+
return _DryRunManager.instance;
|
|
40
|
+
}
|
|
41
|
+
enable() {
|
|
42
|
+
this.enabled = true;
|
|
43
|
+
this.operations = [];
|
|
44
|
+
}
|
|
45
|
+
disable() {
|
|
46
|
+
this.enabled = false;
|
|
47
|
+
this.operations = [];
|
|
48
|
+
}
|
|
49
|
+
isEnabled() {
|
|
50
|
+
return this.enabled;
|
|
51
|
+
}
|
|
52
|
+
addOperation(operation) {
|
|
53
|
+
if (this.enabled) {
|
|
54
|
+
this.operations.push(operation);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
getOperations() {
|
|
58
|
+
return [...this.operations];
|
|
59
|
+
}
|
|
60
|
+
printSummary() {
|
|
61
|
+
if (!this.enabled || this.operations.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(chalk.bold.yellow("\n\u{1F4CB} Dry Run - Preview of changes:\n"));
|
|
65
|
+
console.log(chalk.gray("No files will be written. Remove --dry-run to apply changes.\n"));
|
|
66
|
+
const createOps = this.operations.filter((op) => op.type === "create");
|
|
67
|
+
const modifyOps = this.operations.filter((op) => op.type === "modify");
|
|
68
|
+
const deleteOps = this.operations.filter((op) => op.type === "delete");
|
|
69
|
+
if (createOps.length > 0) {
|
|
70
|
+
console.log(chalk.green.bold(`
|
|
71
|
+
\u2713 Files to be created (${createOps.length}):`));
|
|
72
|
+
createOps.forEach((op) => {
|
|
73
|
+
const size = op.content ? `${op.content.length} bytes` : "unknown size";
|
|
74
|
+
console.log(` ${chalk.green("+")} ${chalk.cyan(op.path)} ${chalk.gray(`(${size})`)}`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (modifyOps.length > 0) {
|
|
78
|
+
console.log(chalk.yellow.bold(`
|
|
79
|
+
~ Files to be modified (${modifyOps.length}):`));
|
|
80
|
+
modifyOps.forEach((op) => {
|
|
81
|
+
console.log(` ${chalk.yellow("~")} ${chalk.cyan(op.path)}`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (deleteOps.length > 0) {
|
|
85
|
+
console.log(chalk.red.bold(`
|
|
86
|
+
- Files to be deleted (${deleteOps.length}):`));
|
|
87
|
+
deleteOps.forEach((op) => {
|
|
88
|
+
console.log(` ${chalk.red("-")} ${chalk.cyan(op.path)}`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.gray("\n" + "\u2500".repeat(60)));
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.bold(` Total operations: ${this.operations.length}`) + chalk.gray(
|
|
94
|
+
` (${createOps.length} create, ${modifyOps.length} modify, ${deleteOps.length} delete)`
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
console.log(chalk.gray("\u2500".repeat(60)));
|
|
98
|
+
console.log(chalk.yellow("\n\u26A0 This was a dry run. No files were created or modified."));
|
|
99
|
+
console.log(chalk.gray(" Remove --dry-run to apply these changes.\n"));
|
|
100
|
+
}
|
|
101
|
+
// Helper to format file path relative to cwd
|
|
102
|
+
relativePath(filePath) {
|
|
103
|
+
return path.relative(process.cwd(), filePath);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/cli/utils/helpers.ts
|
|
19
108
|
function toPascalCase(str) {
|
|
20
109
|
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
21
110
|
}
|
|
@@ -47,39 +136,54 @@ async function ensureDir(dirPath) {
|
|
|
47
136
|
await fs.mkdir(dirPath, { recursive: true });
|
|
48
137
|
}
|
|
49
138
|
async function writeFile(filePath, content) {
|
|
50
|
-
|
|
139
|
+
const dryRun = DryRunManager.getInstance();
|
|
140
|
+
if (dryRun.isEnabled()) {
|
|
141
|
+
dryRun.addOperation({
|
|
142
|
+
type: "create",
|
|
143
|
+
path: dryRun.relativePath(filePath),
|
|
144
|
+
content,
|
|
145
|
+
size: content.length
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await ensureDir(path2.dirname(filePath));
|
|
51
150
|
await fs.writeFile(filePath, content, "utf-8");
|
|
52
151
|
}
|
|
53
152
|
function success(message) {
|
|
54
|
-
console.log(
|
|
153
|
+
console.log(chalk2.green("\u2713"), message);
|
|
55
154
|
}
|
|
56
155
|
function error(message) {
|
|
57
|
-
console.error(
|
|
156
|
+
console.error(chalk2.red("\u2717"), message);
|
|
58
157
|
}
|
|
59
158
|
function warn(message) {
|
|
60
|
-
console.log(
|
|
159
|
+
console.log(chalk2.yellow("\u26A0"), message);
|
|
61
160
|
}
|
|
62
161
|
function info(message) {
|
|
63
|
-
console.log(
|
|
162
|
+
console.log(chalk2.blue("\u2139"), message);
|
|
64
163
|
}
|
|
65
164
|
function getProjectRoot() {
|
|
66
165
|
return process.cwd();
|
|
67
166
|
}
|
|
68
167
|
function getSourceDir() {
|
|
69
|
-
return
|
|
168
|
+
return path2.join(getProjectRoot(), "src");
|
|
70
169
|
}
|
|
71
170
|
function getModulesDir() {
|
|
72
|
-
return
|
|
171
|
+
return path2.join(getSourceDir(), "modules");
|
|
73
172
|
}
|
|
74
173
|
|
|
75
174
|
// src/cli/commands/init.ts
|
|
76
|
-
var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--esm", "Use ES Modules (import/export) - default").option("--cjs, --commonjs", "Use CommonJS (require/module.exports)").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
|
|
175
|
+
var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--esm", "Use ES Modules (import/export) - default").option("--cjs, --commonjs", "Use CommonJS (require/module.exports)").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").option("--dry-run", "Preview changes without writing files").action(
|
|
77
176
|
async (name, cmdOptions) => {
|
|
177
|
+
const dryRun = DryRunManager.getInstance();
|
|
178
|
+
if (cmdOptions?.dryRun) {
|
|
179
|
+
dryRun.enable();
|
|
180
|
+
console.log(chalk3.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
181
|
+
}
|
|
78
182
|
console.log(
|
|
79
|
-
|
|
183
|
+
chalk3.blue(`
|
|
80
184
|
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
81
185
|
\u2551 \u2551
|
|
82
|
-
\u2551 ${
|
|
186
|
+
\u2551 ${chalk3.bold("\u{1F680} Servcraft Project Generator")} \u2551
|
|
83
187
|
\u2551 \u2551
|
|
84
188
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
85
189
|
`)
|
|
@@ -174,7 +278,7 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
174
278
|
orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
|
|
175
279
|
};
|
|
176
280
|
}
|
|
177
|
-
const projectDir =
|
|
281
|
+
const projectDir = path3.resolve(process.cwd(), options.name);
|
|
178
282
|
const spinner = ora("Creating project...").start();
|
|
179
283
|
try {
|
|
180
284
|
try {
|
|
@@ -188,21 +292,21 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
188
292
|
spinner.text = "Generating project files...";
|
|
189
293
|
const packageJson = generatePackageJson(options);
|
|
190
294
|
await writeFile(
|
|
191
|
-
|
|
295
|
+
path3.join(projectDir, "package.json"),
|
|
192
296
|
JSON.stringify(packageJson, null, 2)
|
|
193
297
|
);
|
|
194
298
|
if (options.language === "typescript") {
|
|
195
|
-
await writeFile(
|
|
196
|
-
await writeFile(
|
|
299
|
+
await writeFile(path3.join(projectDir, "tsconfig.json"), generateTsConfig(options));
|
|
300
|
+
await writeFile(path3.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
|
|
197
301
|
} else {
|
|
198
|
-
await writeFile(
|
|
302
|
+
await writeFile(path3.join(projectDir, "jsconfig.json"), generateJsConfig(options));
|
|
199
303
|
}
|
|
200
|
-
await writeFile(
|
|
201
|
-
await writeFile(
|
|
202
|
-
await writeFile(
|
|
203
|
-
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));
|
|
204
308
|
await writeFile(
|
|
205
|
-
|
|
309
|
+
path3.join(projectDir, "docker-compose.yml"),
|
|
206
310
|
generateDockerCompose(options)
|
|
207
311
|
);
|
|
208
312
|
const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
|
|
@@ -223,59 +327,63 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
223
327
|
dirs.push("src/database/models");
|
|
224
328
|
}
|
|
225
329
|
for (const dir of dirs) {
|
|
226
|
-
await ensureDir(
|
|
330
|
+
await ensureDir(path3.join(projectDir, dir));
|
|
227
331
|
}
|
|
228
|
-
await writeFile(
|
|
332
|
+
await writeFile(path3.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
|
|
229
333
|
await writeFile(
|
|
230
|
-
|
|
334
|
+
path3.join(projectDir, `src/core/server.${ext}`),
|
|
231
335
|
generateServerFile(options)
|
|
232
336
|
);
|
|
233
337
|
await writeFile(
|
|
234
|
-
|
|
338
|
+
path3.join(projectDir, `src/core/logger.${ext}`),
|
|
235
339
|
generateLoggerFile(options)
|
|
236
340
|
);
|
|
237
341
|
await writeFile(
|
|
238
|
-
|
|
342
|
+
path3.join(projectDir, `src/config/index.${ext}`),
|
|
239
343
|
generateConfigFile(options)
|
|
240
344
|
);
|
|
241
345
|
await writeFile(
|
|
242
|
-
|
|
346
|
+
path3.join(projectDir, `src/middleware/index.${ext}`),
|
|
243
347
|
generateMiddlewareFile(options)
|
|
244
348
|
);
|
|
245
349
|
await writeFile(
|
|
246
|
-
|
|
350
|
+
path3.join(projectDir, `src/utils/index.${ext}`),
|
|
247
351
|
generateUtilsFile(options)
|
|
248
352
|
);
|
|
249
353
|
await writeFile(
|
|
250
|
-
|
|
354
|
+
path3.join(projectDir, `src/types/index.${ext}`),
|
|
251
355
|
generateTypesFile(options)
|
|
252
356
|
);
|
|
253
357
|
if (options.orm === "prisma") {
|
|
254
358
|
await writeFile(
|
|
255
|
-
|
|
359
|
+
path3.join(projectDir, "prisma/schema.prisma"),
|
|
256
360
|
generatePrismaSchema(options)
|
|
257
361
|
);
|
|
258
362
|
} else if (options.orm === "mongoose") {
|
|
259
363
|
await writeFile(
|
|
260
|
-
|
|
364
|
+
path3.join(projectDir, `src/database/connection.${ext}`),
|
|
261
365
|
generateMongooseConnection(options)
|
|
262
366
|
);
|
|
263
367
|
await writeFile(
|
|
264
|
-
|
|
368
|
+
path3.join(projectDir, `src/database/models/user.model.${ext}`),
|
|
265
369
|
generateMongooseUserModel(options)
|
|
266
370
|
);
|
|
267
371
|
}
|
|
268
372
|
spinner.succeed("Project files generated!");
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
373
|
+
if (!cmdOptions?.dryRun) {
|
|
374
|
+
const installSpinner = ora("Installing dependencies...").start();
|
|
375
|
+
try {
|
|
376
|
+
execSync("npm install", { cwd: projectDir, stdio: "pipe" });
|
|
377
|
+
installSpinner.succeed("Dependencies installed!");
|
|
378
|
+
} catch {
|
|
379
|
+
installSpinner.warn("Failed to install dependencies automatically");
|
|
380
|
+
warn(' Run "npm install" manually in the project directory');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!cmdOptions?.dryRun) {
|
|
384
|
+
console.log("\n" + chalk3.green("\u2728 Project created successfully!"));
|
|
276
385
|
}
|
|
277
|
-
console.log("\n" +
|
|
278
|
-
console.log("\n" + chalk2.bold("\u{1F4C1} Project structure:"));
|
|
386
|
+
console.log("\n" + chalk3.bold("\u{1F4C1} Project structure:"));
|
|
279
387
|
console.log(`
|
|
280
388
|
${options.name}/
|
|
281
389
|
\u251C\u2500\u2500 src/
|
|
@@ -290,19 +398,22 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
290
398
|
\u251C\u2500\u2500 docker-compose.yml
|
|
291
399
|
\u2514\u2500\u2500 package.json
|
|
292
400
|
`);
|
|
293
|
-
console.log(
|
|
401
|
+
console.log(chalk3.bold("\u{1F680} Get started:"));
|
|
294
402
|
console.log(`
|
|
295
|
-
${
|
|
296
|
-
${options.database !== "none" ?
|
|
297
|
-
${
|
|
403
|
+
${chalk3.cyan(`cd ${options.name}`)}
|
|
404
|
+
${options.database !== "none" ? chalk3.cyan("npm run db:push # Setup database") : ""}
|
|
405
|
+
${chalk3.cyan("npm run dev # Start development server")}
|
|
298
406
|
`);
|
|
299
|
-
console.log(
|
|
407
|
+
console.log(chalk3.bold("\u{1F4DA} Available commands:"));
|
|
300
408
|
console.log(`
|
|
301
|
-
${
|
|
302
|
-
${
|
|
303
|
-
${
|
|
304
|
-
${
|
|
409
|
+
${chalk3.yellow("servcraft generate module <name>")} Generate a new module
|
|
410
|
+
${chalk3.yellow("servcraft generate controller <name>")} Generate a controller
|
|
411
|
+
${chalk3.yellow("servcraft generate service <name>")} Generate a service
|
|
412
|
+
${chalk3.yellow("servcraft add auth")} Add authentication module
|
|
305
413
|
`);
|
|
414
|
+
if (cmdOptions?.dryRun) {
|
|
415
|
+
dryRun.printSummary();
|
|
416
|
+
}
|
|
306
417
|
} catch (err) {
|
|
307
418
|
spinner.fail("Failed to create project");
|
|
308
419
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -1094,9 +1205,10 @@ declare module 'fastify' {
|
|
|
1094
1205
|
|
|
1095
1206
|
// src/cli/commands/generate.ts
|
|
1096
1207
|
import { Command as Command2 } from "commander";
|
|
1097
|
-
import
|
|
1208
|
+
import path4 from "path";
|
|
1098
1209
|
import ora2 from "ora";
|
|
1099
1210
|
import inquirer2 from "inquirer";
|
|
1211
|
+
import chalk5 from "chalk";
|
|
1100
1212
|
|
|
1101
1213
|
// src/cli/utils/field-parser.ts
|
|
1102
1214
|
var tsTypeMap = {
|
|
@@ -1240,6 +1352,108 @@ function parseFields(fieldsStr) {
|
|
|
1240
1352
|
return fieldsStr.split(/\s+/).filter(Boolean).map(parseField);
|
|
1241
1353
|
}
|
|
1242
1354
|
|
|
1355
|
+
// src/cli/utils/error-handler.ts
|
|
1356
|
+
import chalk4 from "chalk";
|
|
1357
|
+
var ServCraftError = class extends Error {
|
|
1358
|
+
suggestions;
|
|
1359
|
+
docsLink;
|
|
1360
|
+
constructor(message, suggestions = [], docsLink) {
|
|
1361
|
+
super(message);
|
|
1362
|
+
this.name = "ServCraftError";
|
|
1363
|
+
this.suggestions = suggestions;
|
|
1364
|
+
this.docsLink = docsLink;
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
var ErrorTypes = {
|
|
1368
|
+
MODULE_NOT_FOUND: (moduleName) => new ServCraftError(
|
|
1369
|
+
`Module "${moduleName}" not found`,
|
|
1370
|
+
[
|
|
1371
|
+
`Run ${chalk4.cyan("servcraft list")} to see available modules`,
|
|
1372
|
+
`Check the spelling of the module name`,
|
|
1373
|
+
`Visit ${chalk4.blue("https://github.com/Le-Sourcier/servcraft#modules")} for module list`
|
|
1374
|
+
],
|
|
1375
|
+
"https://github.com/Le-Sourcier/servcraft#add-pre-built-modules"
|
|
1376
|
+
),
|
|
1377
|
+
MODULE_ALREADY_EXISTS: (moduleName) => new ServCraftError(`Module "${moduleName}" already exists`, [
|
|
1378
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --force")} to overwrite`,
|
|
1379
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --update")} to update`,
|
|
1380
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --skip-existing")} to skip`
|
|
1381
|
+
]),
|
|
1382
|
+
NOT_IN_PROJECT: () => new ServCraftError(
|
|
1383
|
+
"Not in a ServCraft project directory",
|
|
1384
|
+
[
|
|
1385
|
+
`Run ${chalk4.cyan("servcraft init")} to create a new project`,
|
|
1386
|
+
`Navigate to your ServCraft project directory`,
|
|
1387
|
+
`Check if ${chalk4.yellow("package.json")} exists`
|
|
1388
|
+
],
|
|
1389
|
+
"https://github.com/Le-Sourcier/servcraft#initialize-project"
|
|
1390
|
+
),
|
|
1391
|
+
FILE_ALREADY_EXISTS: (fileName) => new ServCraftError(`File "${fileName}" already exists`, [
|
|
1392
|
+
`Use ${chalk4.cyan("--force")} flag to overwrite`,
|
|
1393
|
+
`Choose a different name`,
|
|
1394
|
+
`Delete the existing file first`
|
|
1395
|
+
]),
|
|
1396
|
+
INVALID_DATABASE: (database) => new ServCraftError(`Invalid database type: "${database}"`, [
|
|
1397
|
+
`Valid options: ${chalk4.cyan("postgresql, mysql, sqlite, mongodb, none")}`,
|
|
1398
|
+
`Use ${chalk4.cyan("servcraft init --db postgresql")} for PostgreSQL`
|
|
1399
|
+
]),
|
|
1400
|
+
INVALID_VALIDATOR: (validator) => new ServCraftError(`Invalid validator type: "${validator}"`, [
|
|
1401
|
+
`Valid options: ${chalk4.cyan("zod, joi, yup")}`,
|
|
1402
|
+
`Default is ${chalk4.cyan("zod")}`
|
|
1403
|
+
]),
|
|
1404
|
+
MISSING_DEPENDENCY: (dependency, command) => new ServCraftError(`Missing dependency: "${dependency}"`, [
|
|
1405
|
+
`Run ${chalk4.cyan(command)} to install`,
|
|
1406
|
+
`Check your ${chalk4.yellow("package.json")}`
|
|
1407
|
+
]),
|
|
1408
|
+
INVALID_FIELD_FORMAT: (field) => new ServCraftError(`Invalid field format: "${field}"`, [
|
|
1409
|
+
`Expected format: ${chalk4.cyan("name:type")}`,
|
|
1410
|
+
`Example: ${chalk4.cyan("name:string age:number isActive:boolean")}`,
|
|
1411
|
+
`Supported types: string, number, boolean, date`
|
|
1412
|
+
]),
|
|
1413
|
+
GIT_NOT_INITIALIZED: () => new ServCraftError("Git repository not initialized", [
|
|
1414
|
+
`Run ${chalk4.cyan("git init")} to initialize git`,
|
|
1415
|
+
`This is required for some ServCraft features`
|
|
1416
|
+
])
|
|
1417
|
+
};
|
|
1418
|
+
function displayError(error2) {
|
|
1419
|
+
console.error("\n" + chalk4.red.bold("\u2717 Error: ") + chalk4.red(error2.message));
|
|
1420
|
+
if (error2 instanceof ServCraftError) {
|
|
1421
|
+
if (error2.suggestions.length > 0) {
|
|
1422
|
+
console.log("\n" + chalk4.yellow.bold("\u{1F4A1} Suggestions:"));
|
|
1423
|
+
error2.suggestions.forEach((suggestion) => {
|
|
1424
|
+
console.log(chalk4.yellow(" \u2022 ") + suggestion);
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
if (error2.docsLink) {
|
|
1428
|
+
console.log(
|
|
1429
|
+
"\n" + chalk4.blue.bold("\u{1F4DA} Documentation: ") + chalk4.blue.underline(error2.docsLink)
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
console.log();
|
|
1434
|
+
}
|
|
1435
|
+
function validateProject() {
|
|
1436
|
+
try {
|
|
1437
|
+
const fs12 = __require("fs");
|
|
1438
|
+
if (!fs12.existsSync("package.json")) {
|
|
1439
|
+
return ErrorTypes.NOT_IN_PROJECT();
|
|
1440
|
+
}
|
|
1441
|
+
const packageJson = JSON.parse(fs12.readFileSync("package.json", "utf-8"));
|
|
1442
|
+
if (!packageJson.dependencies?.fastify) {
|
|
1443
|
+
return new ServCraftError("This does not appear to be a ServCraft project", [
|
|
1444
|
+
`ServCraft projects require Fastify`,
|
|
1445
|
+
`Run ${chalk4.cyan("servcraft init")} to create a new project`
|
|
1446
|
+
]);
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return new ServCraftError("Failed to validate project", [
|
|
1451
|
+
`Ensure you are in the project root directory`,
|
|
1452
|
+
`Check if ${chalk4.yellow("package.json")} is valid`
|
|
1453
|
+
]);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1243
1457
|
// src/cli/templates/controller.ts
|
|
1244
1458
|
function controllerTemplate(name, pascalName, camelName) {
|
|
1245
1459
|
return `import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
@@ -1928,11 +2142,371 @@ ${indexLines.join("\n")}
|
|
|
1928
2142
|
`;
|
|
1929
2143
|
}
|
|
1930
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
|
+
|
|
1931
2492
|
// src/cli/commands/generate.ts
|
|
2493
|
+
function enableDryRunIfNeeded(options) {
|
|
2494
|
+
const dryRun = DryRunManager.getInstance();
|
|
2495
|
+
if (options.dryRun) {
|
|
2496
|
+
dryRun.enable();
|
|
2497
|
+
console.log(chalk5.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
function showDryRunSummary(options) {
|
|
2501
|
+
if (options.dryRun) {
|
|
2502
|
+
DryRunManager.getInstance().printSummary();
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
1932
2505
|
var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
|
|
1933
2506
|
generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
1934
2507
|
"Generate a complete module with controller, service, repository, types, schemas, and routes"
|
|
1935
|
-
).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").action(async (name, fieldsArgs, options) => {
|
|
2508
|
+
).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").option("--with-tests", "Generate test files (__tests__ directory)").option("--dry-run", "Preview changes without writing files").action(async (name, fieldsArgs, options) => {
|
|
2509
|
+
enableDryRunIfNeeded(options);
|
|
1936
2510
|
let fields = [];
|
|
1937
2511
|
if (options.interactive) {
|
|
1938
2512
|
fields = await promptForFields();
|
|
@@ -1947,7 +2521,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
1947
2521
|
const pluralName = pluralize(kebabName);
|
|
1948
2522
|
const tableName = pluralize(kebabName.replace(/-/g, "_"));
|
|
1949
2523
|
const validatorType = options.validator || "zod";
|
|
1950
|
-
const moduleDir =
|
|
2524
|
+
const moduleDir = path4.join(getModulesDir(), kebabName);
|
|
1951
2525
|
if (await fileExists(moduleDir)) {
|
|
1952
2526
|
spinner.stop();
|
|
1953
2527
|
error(`Module "${kebabName}" already exists`);
|
|
@@ -1986,7 +2560,22 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
1986
2560
|
});
|
|
1987
2561
|
}
|
|
1988
2562
|
for (const file of files) {
|
|
1989
|
-
await writeFile(
|
|
2563
|
+
await writeFile(path4.join(moduleDir, file.name), file.content);
|
|
2564
|
+
}
|
|
2565
|
+
if (options.withTests) {
|
|
2566
|
+
const testDir = path4.join(moduleDir, "__tests__");
|
|
2567
|
+
await writeFile(
|
|
2568
|
+
path4.join(testDir, `${kebabName}.controller.test.ts`),
|
|
2569
|
+
controllerTestTemplate(kebabName, pascalName, camelName)
|
|
2570
|
+
);
|
|
2571
|
+
await writeFile(
|
|
2572
|
+
path4.join(testDir, `${kebabName}.service.test.ts`),
|
|
2573
|
+
serviceTestTemplate(kebabName, pascalName, camelName)
|
|
2574
|
+
);
|
|
2575
|
+
await writeFile(
|
|
2576
|
+
path4.join(testDir, `${kebabName}.integration.test.ts`),
|
|
2577
|
+
integrationTestTemplate(kebabName, pascalName, camelName)
|
|
2578
|
+
);
|
|
1990
2579
|
}
|
|
1991
2580
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
1992
2581
|
if (options.prisma || hasFields) {
|
|
@@ -2011,6 +2600,11 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2011
2600
|
}
|
|
2012
2601
|
console.log("\n\u{1F4C1} Files created:");
|
|
2013
2602
|
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
2603
|
+
if (options.withTests) {
|
|
2604
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
2605
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
2606
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
2607
|
+
}
|
|
2014
2608
|
console.log("\n\u{1F4CC} Next steps:");
|
|
2015
2609
|
if (!hasFields) {
|
|
2016
2610
|
info(` 1. Update the types in ${kebabName}.types.ts`);
|
|
@@ -2024,42 +2618,46 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2024
2618
|
info(` ${hasFields ? "3" : "4"}. Add the Prisma model to schema.prisma`);
|
|
2025
2619
|
info(` ${hasFields ? "4" : "5"}. Run: npm run db:migrate`);
|
|
2026
2620
|
}
|
|
2621
|
+
showDryRunSummary(options);
|
|
2027
2622
|
} catch (err) {
|
|
2028
2623
|
spinner.fail("Failed to generate module");
|
|
2029
2624
|
error(err instanceof Error ? err.message : String(err));
|
|
2030
2625
|
}
|
|
2031
2626
|
});
|
|
2032
|
-
generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2627
|
+
generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2628
|
+
enableDryRunIfNeeded(options);
|
|
2033
2629
|
const spinner = ora2("Generating controller...").start();
|
|
2034
2630
|
try {
|
|
2035
2631
|
const kebabName = toKebabCase(name);
|
|
2036
2632
|
const pascalName = toPascalCase(name);
|
|
2037
2633
|
const camelName = toCamelCase(name);
|
|
2038
2634
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2039
|
-
const moduleDir =
|
|
2040
|
-
const filePath =
|
|
2635
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2636
|
+
const filePath = path4.join(moduleDir, `${kebabName}.controller.ts`);
|
|
2041
2637
|
if (await fileExists(filePath)) {
|
|
2042
2638
|
spinner.stop();
|
|
2043
|
-
|
|
2639
|
+
displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
|
|
2044
2640
|
return;
|
|
2045
2641
|
}
|
|
2046
2642
|
await writeFile(filePath, controllerTemplate(kebabName, pascalName, camelName));
|
|
2047
2643
|
spinner.succeed(`Controller "${pascalName}Controller" generated!`);
|
|
2048
2644
|
success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
|
|
2645
|
+
showDryRunSummary(options);
|
|
2049
2646
|
} catch (err) {
|
|
2050
2647
|
spinner.fail("Failed to generate controller");
|
|
2051
2648
|
error(err instanceof Error ? err.message : String(err));
|
|
2052
2649
|
}
|
|
2053
2650
|
});
|
|
2054
|
-
generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2651
|
+
generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2652
|
+
enableDryRunIfNeeded(options);
|
|
2055
2653
|
const spinner = ora2("Generating service...").start();
|
|
2056
2654
|
try {
|
|
2057
2655
|
const kebabName = toKebabCase(name);
|
|
2058
2656
|
const pascalName = toPascalCase(name);
|
|
2059
2657
|
const camelName = toCamelCase(name);
|
|
2060
2658
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2061
|
-
const moduleDir =
|
|
2062
|
-
const filePath =
|
|
2659
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2660
|
+
const filePath = path4.join(moduleDir, `${kebabName}.service.ts`);
|
|
2063
2661
|
if (await fileExists(filePath)) {
|
|
2064
2662
|
spinner.stop();
|
|
2065
2663
|
error(`Service "${kebabName}" already exists`);
|
|
@@ -2068,12 +2666,14 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
|
|
|
2068
2666
|
await writeFile(filePath, serviceTemplate(kebabName, pascalName, camelName));
|
|
2069
2667
|
spinner.succeed(`Service "${pascalName}Service" generated!`);
|
|
2070
2668
|
success(` src/modules/${moduleName}/${kebabName}.service.ts`);
|
|
2669
|
+
showDryRunSummary(options);
|
|
2071
2670
|
} catch (err) {
|
|
2072
2671
|
spinner.fail("Failed to generate service");
|
|
2073
2672
|
error(err instanceof Error ? err.message : String(err));
|
|
2074
2673
|
}
|
|
2075
2674
|
});
|
|
2076
|
-
generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2675
|
+
generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2676
|
+
enableDryRunIfNeeded(options);
|
|
2077
2677
|
const spinner = ora2("Generating repository...").start();
|
|
2078
2678
|
try {
|
|
2079
2679
|
const kebabName = toKebabCase(name);
|
|
@@ -2081,8 +2681,8 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
2081
2681
|
const camelName = toCamelCase(name);
|
|
2082
2682
|
const pluralName = pluralize(kebabName);
|
|
2083
2683
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2084
|
-
const moduleDir =
|
|
2085
|
-
const filePath =
|
|
2684
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2685
|
+
const filePath = path4.join(moduleDir, `${kebabName}.repository.ts`);
|
|
2086
2686
|
if (await fileExists(filePath)) {
|
|
2087
2687
|
spinner.stop();
|
|
2088
2688
|
error(`Repository "${kebabName}" already exists`);
|
|
@@ -2091,19 +2691,21 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
2091
2691
|
await writeFile(filePath, repositoryTemplate(kebabName, pascalName, camelName, pluralName));
|
|
2092
2692
|
spinner.succeed(`Repository "${pascalName}Repository" generated!`);
|
|
2093
2693
|
success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
|
|
2694
|
+
showDryRunSummary(options);
|
|
2094
2695
|
} catch (err) {
|
|
2095
2696
|
spinner.fail("Failed to generate repository");
|
|
2096
2697
|
error(err instanceof Error ? err.message : String(err));
|
|
2097
2698
|
}
|
|
2098
2699
|
});
|
|
2099
|
-
generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2700
|
+
generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2701
|
+
enableDryRunIfNeeded(options);
|
|
2100
2702
|
const spinner = ora2("Generating types...").start();
|
|
2101
2703
|
try {
|
|
2102
2704
|
const kebabName = toKebabCase(name);
|
|
2103
2705
|
const pascalName = toPascalCase(name);
|
|
2104
2706
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2105
|
-
const moduleDir =
|
|
2106
|
-
const filePath =
|
|
2707
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2708
|
+
const filePath = path4.join(moduleDir, `${kebabName}.types.ts`);
|
|
2107
2709
|
if (await fileExists(filePath)) {
|
|
2108
2710
|
spinner.stop();
|
|
2109
2711
|
error(`Types file "${kebabName}.types.ts" already exists`);
|
|
@@ -2112,20 +2714,22 @@ generateCommand.command("types <name>").alias("t").description("Generate types/i
|
|
|
2112
2714
|
await writeFile(filePath, typesTemplate(kebabName, pascalName));
|
|
2113
2715
|
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
2114
2716
|
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
2717
|
+
showDryRunSummary(options);
|
|
2115
2718
|
} catch (err) {
|
|
2116
2719
|
spinner.fail("Failed to generate types");
|
|
2117
2720
|
error(err instanceof Error ? err.message : String(err));
|
|
2118
2721
|
}
|
|
2119
2722
|
});
|
|
2120
|
-
generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2723
|
+
generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2724
|
+
enableDryRunIfNeeded(options);
|
|
2121
2725
|
const spinner = ora2("Generating schemas...").start();
|
|
2122
2726
|
try {
|
|
2123
2727
|
const kebabName = toKebabCase(name);
|
|
2124
2728
|
const pascalName = toPascalCase(name);
|
|
2125
2729
|
const camelName = toCamelCase(name);
|
|
2126
2730
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2127
|
-
const moduleDir =
|
|
2128
|
-
const filePath =
|
|
2731
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2732
|
+
const filePath = path4.join(moduleDir, `${kebabName}.schemas.ts`);
|
|
2129
2733
|
if (await fileExists(filePath)) {
|
|
2130
2734
|
spinner.stop();
|
|
2131
2735
|
error(`Schemas file "${kebabName}.schemas.ts" already exists`);
|
|
@@ -2134,12 +2738,14 @@ generateCommand.command("schema <name>").alias("v").description("Generate valida
|
|
|
2134
2738
|
await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
|
|
2135
2739
|
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
2136
2740
|
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
2741
|
+
showDryRunSummary(options);
|
|
2137
2742
|
} catch (err) {
|
|
2138
2743
|
spinner.fail("Failed to generate schemas");
|
|
2139
2744
|
error(err instanceof Error ? err.message : String(err));
|
|
2140
2745
|
}
|
|
2141
2746
|
});
|
|
2142
|
-
generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2747
|
+
generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2748
|
+
enableDryRunIfNeeded(options);
|
|
2143
2749
|
const spinner = ora2("Generating routes...").start();
|
|
2144
2750
|
try {
|
|
2145
2751
|
const kebabName = toKebabCase(name);
|
|
@@ -2147,8 +2753,8 @@ generateCommand.command("routes <name>").description("Generate routes").option("
|
|
|
2147
2753
|
const camelName = toCamelCase(name);
|
|
2148
2754
|
const pluralName = pluralize(kebabName);
|
|
2149
2755
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2150
|
-
const moduleDir =
|
|
2151
|
-
const filePath =
|
|
2756
|
+
const moduleDir = path4.join(getModulesDir(), moduleName);
|
|
2757
|
+
const filePath = path4.join(moduleDir, `${kebabName}.routes.ts`);
|
|
2152
2758
|
if (await fileExists(filePath)) {
|
|
2153
2759
|
spinner.stop();
|
|
2154
2760
|
error(`Routes file "${kebabName}.routes.ts" already exists`);
|
|
@@ -2157,6 +2763,7 @@ generateCommand.command("routes <name>").description("Generate routes").option("
|
|
|
2157
2763
|
await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
|
|
2158
2764
|
spinner.succeed(`Routes for "${pascalName}" generated!`);
|
|
2159
2765
|
success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
|
|
2766
|
+
showDryRunSummary(options);
|
|
2160
2767
|
} catch (err) {
|
|
2161
2768
|
spinner.fail("Failed to generate routes");
|
|
2162
2769
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -2235,21 +2842,21 @@ async function promptForFields() {
|
|
|
2235
2842
|
|
|
2236
2843
|
// src/cli/commands/add-module.ts
|
|
2237
2844
|
import { Command as Command3 } from "commander";
|
|
2238
|
-
import
|
|
2845
|
+
import path7 from "path";
|
|
2239
2846
|
import ora3 from "ora";
|
|
2240
|
-
import
|
|
2847
|
+
import chalk7 from "chalk";
|
|
2241
2848
|
import * as fs5 from "fs/promises";
|
|
2242
2849
|
|
|
2243
2850
|
// src/cli/utils/env-manager.ts
|
|
2244
2851
|
import * as fs3 from "fs/promises";
|
|
2245
|
-
import * as
|
|
2852
|
+
import * as path5 from "path";
|
|
2246
2853
|
import { existsSync } from "fs";
|
|
2247
2854
|
var EnvManager = class {
|
|
2248
2855
|
envPath;
|
|
2249
2856
|
envExamplePath;
|
|
2250
2857
|
constructor(projectRoot) {
|
|
2251
|
-
this.envPath =
|
|
2252
|
-
this.envExamplePath =
|
|
2858
|
+
this.envPath = path5.join(projectRoot, ".env");
|
|
2859
|
+
this.envExamplePath = path5.join(projectRoot, ".env.example");
|
|
2253
2860
|
}
|
|
2254
2861
|
/**
|
|
2255
2862
|
* Add environment variables to .env file
|
|
@@ -2900,15 +3507,15 @@ var EnvManager = class {
|
|
|
2900
3507
|
|
|
2901
3508
|
// src/cli/utils/template-manager.ts
|
|
2902
3509
|
import * as fs4 from "fs/promises";
|
|
2903
|
-
import * as
|
|
3510
|
+
import * as path6 from "path";
|
|
2904
3511
|
import { createHash } from "crypto";
|
|
2905
3512
|
import { existsSync as existsSync2 } from "fs";
|
|
2906
3513
|
var TemplateManager = class {
|
|
2907
3514
|
templatesDir;
|
|
2908
3515
|
manifestsDir;
|
|
2909
3516
|
constructor(projectRoot) {
|
|
2910
|
-
this.templatesDir =
|
|
2911
|
-
this.manifestsDir =
|
|
3517
|
+
this.templatesDir = path6.join(projectRoot, ".servcraft", "templates");
|
|
3518
|
+
this.manifestsDir = path6.join(projectRoot, ".servcraft", "manifests");
|
|
2912
3519
|
}
|
|
2913
3520
|
/**
|
|
2914
3521
|
* Initialize template system
|
|
@@ -2922,10 +3529,10 @@ var TemplateManager = class {
|
|
|
2922
3529
|
*/
|
|
2923
3530
|
async saveTemplate(moduleName, files) {
|
|
2924
3531
|
await this.initialize();
|
|
2925
|
-
const moduleTemplateDir =
|
|
3532
|
+
const moduleTemplateDir = path6.join(this.templatesDir, moduleName);
|
|
2926
3533
|
await fs4.mkdir(moduleTemplateDir, { recursive: true });
|
|
2927
3534
|
for (const [fileName, content] of Object.entries(files)) {
|
|
2928
|
-
const filePath =
|
|
3535
|
+
const filePath = path6.join(moduleTemplateDir, fileName);
|
|
2929
3536
|
await fs4.writeFile(filePath, content, "utf-8");
|
|
2930
3537
|
}
|
|
2931
3538
|
}
|
|
@@ -2934,7 +3541,7 @@ var TemplateManager = class {
|
|
|
2934
3541
|
*/
|
|
2935
3542
|
async getTemplate(moduleName, fileName) {
|
|
2936
3543
|
try {
|
|
2937
|
-
const filePath =
|
|
3544
|
+
const filePath = path6.join(this.templatesDir, moduleName, fileName);
|
|
2938
3545
|
return await fs4.readFile(filePath, "utf-8");
|
|
2939
3546
|
} catch {
|
|
2940
3547
|
return null;
|
|
@@ -2959,7 +3566,7 @@ var TemplateManager = class {
|
|
|
2959
3566
|
installedAt: /* @__PURE__ */ new Date(),
|
|
2960
3567
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2961
3568
|
};
|
|
2962
|
-
const manifestPath =
|
|
3569
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
2963
3570
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2964
3571
|
}
|
|
2965
3572
|
/**
|
|
@@ -2967,7 +3574,7 @@ var TemplateManager = class {
|
|
|
2967
3574
|
*/
|
|
2968
3575
|
async getManifest(moduleName) {
|
|
2969
3576
|
try {
|
|
2970
|
-
const manifestPath =
|
|
3577
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
2971
3578
|
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
2972
3579
|
return JSON.parse(content);
|
|
2973
3580
|
} catch {
|
|
@@ -2996,7 +3603,7 @@ var TemplateManager = class {
|
|
|
2996
3603
|
}
|
|
2997
3604
|
const results = [];
|
|
2998
3605
|
for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
|
|
2999
|
-
const filePath =
|
|
3606
|
+
const filePath = path6.join(moduleDir, fileName);
|
|
3000
3607
|
if (!existsSync2(filePath)) {
|
|
3001
3608
|
results.push({
|
|
3002
3609
|
fileName,
|
|
@@ -3022,7 +3629,7 @@ var TemplateManager = class {
|
|
|
3022
3629
|
*/
|
|
3023
3630
|
async createBackup(moduleName, moduleDir) {
|
|
3024
3631
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
3025
|
-
const backupDir =
|
|
3632
|
+
const backupDir = path6.join(path6.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
|
|
3026
3633
|
await this.copyDirectory(moduleDir, backupDir);
|
|
3027
3634
|
return backupDir;
|
|
3028
3635
|
}
|
|
@@ -3033,8 +3640,8 @@ var TemplateManager = class {
|
|
|
3033
3640
|
await fs4.mkdir(dest, { recursive: true });
|
|
3034
3641
|
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
3035
3642
|
for (const entry of entries) {
|
|
3036
|
-
const srcPath =
|
|
3037
|
-
const destPath =
|
|
3643
|
+
const srcPath = path6.join(src, entry.name);
|
|
3644
|
+
const destPath = path6.join(dest, entry.name);
|
|
3038
3645
|
if (entry.isDirectory()) {
|
|
3039
3646
|
await this.copyDirectory(srcPath, destPath);
|
|
3040
3647
|
} else {
|
|
@@ -3135,23 +3742,23 @@ var TemplateManager = class {
|
|
|
3135
3742
|
}
|
|
3136
3743
|
manifest.files = fileHashes;
|
|
3137
3744
|
manifest.updatedAt = /* @__PURE__ */ new Date();
|
|
3138
|
-
const manifestPath =
|
|
3745
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
3139
3746
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
3140
3747
|
}
|
|
3141
3748
|
};
|
|
3142
3749
|
|
|
3143
3750
|
// src/cli/utils/interactive-prompt.ts
|
|
3144
3751
|
import inquirer3 from "inquirer";
|
|
3145
|
-
import
|
|
3752
|
+
import chalk6 from "chalk";
|
|
3146
3753
|
var InteractivePrompt = class {
|
|
3147
3754
|
/**
|
|
3148
3755
|
* Ask what to do when module already exists
|
|
3149
3756
|
*/
|
|
3150
3757
|
static async askModuleExists(moduleName, hasModifications) {
|
|
3151
|
-
console.log(
|
|
3758
|
+
console.log(chalk6.yellow(`
|
|
3152
3759
|
\u26A0\uFE0F Module "${moduleName}" already exists`));
|
|
3153
3760
|
if (hasModifications) {
|
|
3154
|
-
console.log(
|
|
3761
|
+
console.log(chalk6.yellow(" Some files have been modified by you.\n"));
|
|
3155
3762
|
}
|
|
3156
3763
|
const { action } = await inquirer3.prompt([
|
|
3157
3764
|
{
|
|
@@ -3189,12 +3796,12 @@ var InteractivePrompt = class {
|
|
|
3189
3796
|
* Ask what to do with a specific file
|
|
3190
3797
|
*/
|
|
3191
3798
|
static async askFileAction(fileName, isModified, yourLines, newLines) {
|
|
3192
|
-
console.log(
|
|
3799
|
+
console.log(chalk6.cyan(`
|
|
3193
3800
|
\u{1F4C1} ${fileName}`));
|
|
3194
3801
|
console.log(
|
|
3195
|
-
|
|
3802
|
+
chalk6.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
|
|
3196
3803
|
);
|
|
3197
|
-
console.log(
|
|
3804
|
+
console.log(chalk6.gray(` New version: ${newLines} lines
|
|
3198
3805
|
`));
|
|
3199
3806
|
const { action } = await inquirer3.prompt([
|
|
3200
3807
|
{
|
|
@@ -3246,7 +3853,7 @@ var InteractivePrompt = class {
|
|
|
3246
3853
|
* Display diff and ask to continue
|
|
3247
3854
|
*/
|
|
3248
3855
|
static async showDiffAndAsk(diff) {
|
|
3249
|
-
console.log(
|
|
3856
|
+
console.log(chalk6.cyan("\n\u{1F4CA} Differences:\n"));
|
|
3250
3857
|
console.log(diff);
|
|
3251
3858
|
return await this.confirm("\nDo you want to proceed with this change?", true);
|
|
3252
3859
|
}
|
|
@@ -3254,41 +3861,41 @@ var InteractivePrompt = class {
|
|
|
3254
3861
|
* Display merge conflicts
|
|
3255
3862
|
*/
|
|
3256
3863
|
static displayConflicts(conflicts) {
|
|
3257
|
-
console.log(
|
|
3864
|
+
console.log(chalk6.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
|
|
3258
3865
|
conflicts.forEach((conflict, i) => {
|
|
3259
|
-
console.log(
|
|
3866
|
+
console.log(chalk6.yellow(` ${i + 1}. ${conflict}`));
|
|
3260
3867
|
});
|
|
3261
|
-
console.log(
|
|
3262
|
-
console.log(
|
|
3263
|
-
console.log(
|
|
3264
|
-
console.log(
|
|
3265
|
-
console.log(
|
|
3266
|
-
console.log(
|
|
3868
|
+
console.log(chalk6.gray("\n Conflict markers have been added to the file:"));
|
|
3869
|
+
console.log(chalk6.gray(" <<<<<<< YOUR VERSION"));
|
|
3870
|
+
console.log(chalk6.gray(" ... your code ..."));
|
|
3871
|
+
console.log(chalk6.gray(" ======="));
|
|
3872
|
+
console.log(chalk6.gray(" ... new code ..."));
|
|
3873
|
+
console.log(chalk6.gray(" >>>>>>> NEW VERSION\n"));
|
|
3267
3874
|
}
|
|
3268
3875
|
/**
|
|
3269
3876
|
* Show backup location
|
|
3270
3877
|
*/
|
|
3271
3878
|
static showBackupCreated(backupPath) {
|
|
3272
|
-
console.log(
|
|
3273
|
-
\u2713 Backup created: ${
|
|
3879
|
+
console.log(chalk6.green(`
|
|
3880
|
+
\u2713 Backup created: ${chalk6.cyan(backupPath)}`));
|
|
3274
3881
|
}
|
|
3275
3882
|
/**
|
|
3276
3883
|
* Show merge summary
|
|
3277
3884
|
*/
|
|
3278
3885
|
static showMergeSummary(stats) {
|
|
3279
|
-
console.log(
|
|
3886
|
+
console.log(chalk6.bold("\n\u{1F4CA} Merge Summary:\n"));
|
|
3280
3887
|
if (stats.merged > 0) {
|
|
3281
|
-
console.log(
|
|
3888
|
+
console.log(chalk6.green(` \u2713 Merged: ${stats.merged} file(s)`));
|
|
3282
3889
|
}
|
|
3283
3890
|
if (stats.kept > 0) {
|
|
3284
|
-
console.log(
|
|
3891
|
+
console.log(chalk6.blue(` \u2192 Kept: ${stats.kept} file(s)`));
|
|
3285
3892
|
}
|
|
3286
3893
|
if (stats.overwritten > 0) {
|
|
3287
|
-
console.log(
|
|
3894
|
+
console.log(chalk6.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
|
|
3288
3895
|
}
|
|
3289
3896
|
if (stats.conflicts > 0) {
|
|
3290
|
-
console.log(
|
|
3291
|
-
console.log(
|
|
3897
|
+
console.log(chalk6.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
|
|
3898
|
+
console.log(chalk6.gray("\n Please resolve conflicts manually before committing.\n"));
|
|
3292
3899
|
}
|
|
3293
3900
|
}
|
|
3294
3901
|
/**
|
|
@@ -3460,31 +4067,40 @@ var AVAILABLE_MODULES = {
|
|
|
3460
4067
|
var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument(
|
|
3461
4068
|
"[module]",
|
|
3462
4069
|
"Module to add (auth, users, email, audit, upload, cache, notifications, settings)"
|
|
3463
|
-
).option("-l, --list", "List available modules").option("-f, --force", "Force overwrite existing module").option("-u, --update", "Update existing module (smart merge)").option("--skip-existing", "Skip if module already exists").action(
|
|
4070
|
+
).option("-l, --list", "List available modules").option("-f, --force", "Force overwrite existing module").option("-u, --update", "Update existing module (smart merge)").option("--skip-existing", "Skip if module already exists").option("--dry-run", "Preview changes without writing files").action(
|
|
3464
4071
|
async (moduleName, options) => {
|
|
3465
4072
|
if (options?.list || !moduleName) {
|
|
3466
|
-
console.log(
|
|
4073
|
+
console.log(chalk7.bold("\n\u{1F4E6} Available Modules:\n"));
|
|
3467
4074
|
for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
|
|
3468
|
-
console.log(` ${
|
|
3469
|
-
console.log(` ${" ".repeat(15)} ${
|
|
4075
|
+
console.log(` ${chalk7.cyan(key.padEnd(15))} ${mod.name}`);
|
|
4076
|
+
console.log(` ${" ".repeat(15)} ${chalk7.gray(mod.description)}
|
|
3470
4077
|
`);
|
|
3471
4078
|
}
|
|
3472
|
-
console.log(
|
|
3473
|
-
console.log(` ${
|
|
3474
|
-
console.log(` ${
|
|
3475
|
-
console.log(` ${
|
|
4079
|
+
console.log(chalk7.bold("Usage:"));
|
|
4080
|
+
console.log(` ${chalk7.yellow("servcraft add auth")} Add authentication module`);
|
|
4081
|
+
console.log(` ${chalk7.yellow("servcraft add users")} Add user management module`);
|
|
4082
|
+
console.log(` ${chalk7.yellow("servcraft add email")} Add email service module
|
|
3476
4083
|
`);
|
|
3477
4084
|
return;
|
|
3478
4085
|
}
|
|
4086
|
+
const dryRun = DryRunManager.getInstance();
|
|
4087
|
+
if (options?.dryRun) {
|
|
4088
|
+
dryRun.enable();
|
|
4089
|
+
console.log(chalk7.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
4090
|
+
}
|
|
3479
4091
|
const module = AVAILABLE_MODULES[moduleName];
|
|
3480
4092
|
if (!module) {
|
|
3481
|
-
|
|
3482
|
-
|
|
4093
|
+
displayError(ErrorTypes.MODULE_NOT_FOUND(moduleName));
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
const projectError = validateProject();
|
|
4097
|
+
if (projectError) {
|
|
4098
|
+
displayError(projectError);
|
|
3483
4099
|
return;
|
|
3484
4100
|
}
|
|
3485
4101
|
const spinner = ora3(`Adding ${module.name} module...`).start();
|
|
3486
4102
|
try {
|
|
3487
|
-
const moduleDir =
|
|
4103
|
+
const moduleDir = path7.join(getModulesDir(), moduleName);
|
|
3488
4104
|
const templateManager = new TemplateManager(process.cwd());
|
|
3489
4105
|
const moduleExists = await fileExists(moduleDir);
|
|
3490
4106
|
if (moduleExists) {
|
|
@@ -3552,16 +4168,16 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
|
|
|
3552
4168
|
info("\n\u{1F4DD} Created new .env file");
|
|
3553
4169
|
}
|
|
3554
4170
|
if (result.added.length > 0) {
|
|
3555
|
-
console.log(
|
|
4171
|
+
console.log(chalk7.bold("\n\u2705 Added to .env:"));
|
|
3556
4172
|
result.added.forEach((key) => success(` ${key}`));
|
|
3557
4173
|
}
|
|
3558
4174
|
if (result.skipped.length > 0) {
|
|
3559
|
-
console.log(
|
|
4175
|
+
console.log(chalk7.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
|
|
3560
4176
|
result.skipped.forEach((key) => info(` ${key}`));
|
|
3561
4177
|
}
|
|
3562
4178
|
const requiredVars = envSections.flatMap((section) => section.variables).filter((v) => v.required && !v.value).map((v) => v.key);
|
|
3563
4179
|
if (requiredVars.length > 0) {
|
|
3564
|
-
console.log(
|
|
4180
|
+
console.log(chalk7.bold("\n\u26A0\uFE0F Required configuration:"));
|
|
3565
4181
|
requiredVars.forEach((key) => warn(` ${key} - Please configure this variable`));
|
|
3566
4182
|
}
|
|
3567
4183
|
} catch (err) {
|
|
@@ -3569,10 +4185,15 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
|
|
|
3569
4185
|
error(err instanceof Error ? err.message : String(err));
|
|
3570
4186
|
}
|
|
3571
4187
|
}
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
4188
|
+
if (!options?.dryRun) {
|
|
4189
|
+
console.log("\n\u{1F4CC} Next steps:");
|
|
4190
|
+
info(" 1. Configure environment variables in .env (if needed)");
|
|
4191
|
+
info(" 2. Register the module in your main app file");
|
|
4192
|
+
info(" 3. Run database migrations if needed");
|
|
4193
|
+
}
|
|
4194
|
+
if (options?.dryRun) {
|
|
4195
|
+
dryRun.printSummary();
|
|
4196
|
+
}
|
|
3576
4197
|
} catch (err) {
|
|
3577
4198
|
spinner.fail("Failed to add module");
|
|
3578
4199
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -3623,7 +4244,7 @@ export * from './auth.schemas.js';
|
|
|
3623
4244
|
`
|
|
3624
4245
|
};
|
|
3625
4246
|
for (const [name, content] of Object.entries(files)) {
|
|
3626
|
-
await writeFile(
|
|
4247
|
+
await writeFile(path7.join(dir, name), content);
|
|
3627
4248
|
}
|
|
3628
4249
|
}
|
|
3629
4250
|
async function generateUsersModule(dir) {
|
|
@@ -3663,7 +4284,7 @@ export * from './user.schemas.js';
|
|
|
3663
4284
|
`
|
|
3664
4285
|
};
|
|
3665
4286
|
for (const [name, content] of Object.entries(files)) {
|
|
3666
|
-
await writeFile(
|
|
4287
|
+
await writeFile(path7.join(dir, name), content);
|
|
3667
4288
|
}
|
|
3668
4289
|
}
|
|
3669
4290
|
async function generateEmailModule(dir) {
|
|
@@ -3720,7 +4341,7 @@ export { EmailService, emailService } from './email.service.js';
|
|
|
3720
4341
|
`
|
|
3721
4342
|
};
|
|
3722
4343
|
for (const [name, content] of Object.entries(files)) {
|
|
3723
|
-
await writeFile(
|
|
4344
|
+
await writeFile(path7.join(dir, name), content);
|
|
3724
4345
|
}
|
|
3725
4346
|
}
|
|
3726
4347
|
async function generateAuditModule(dir) {
|
|
@@ -3764,7 +4385,7 @@ export { AuditService, auditService } from './audit.service.js';
|
|
|
3764
4385
|
`
|
|
3765
4386
|
};
|
|
3766
4387
|
for (const [name, content] of Object.entries(files)) {
|
|
3767
|
-
await writeFile(
|
|
4388
|
+
await writeFile(path7.join(dir, name), content);
|
|
3768
4389
|
}
|
|
3769
4390
|
}
|
|
3770
4391
|
async function generateUploadModule(dir) {
|
|
@@ -3790,7 +4411,7 @@ export interface UploadOptions {
|
|
|
3790
4411
|
`
|
|
3791
4412
|
};
|
|
3792
4413
|
for (const [name, content] of Object.entries(files)) {
|
|
3793
|
-
await writeFile(
|
|
4414
|
+
await writeFile(path7.join(dir, name), content);
|
|
3794
4415
|
}
|
|
3795
4416
|
}
|
|
3796
4417
|
async function generateCacheModule(dir) {
|
|
@@ -3836,7 +4457,7 @@ export { CacheService, cacheService } from './cache.service.js';
|
|
|
3836
4457
|
`
|
|
3837
4458
|
};
|
|
3838
4459
|
for (const [name, content] of Object.entries(files)) {
|
|
3839
|
-
await writeFile(
|
|
4460
|
+
await writeFile(path7.join(dir, name), content);
|
|
3840
4461
|
}
|
|
3841
4462
|
}
|
|
3842
4463
|
async function generateGenericModule(dir, name) {
|
|
@@ -3850,18 +4471,18 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
|
|
|
3850
4471
|
`
|
|
3851
4472
|
};
|
|
3852
4473
|
for (const [fileName, content] of Object.entries(files)) {
|
|
3853
|
-
await writeFile(
|
|
4474
|
+
await writeFile(path7.join(dir, fileName), content);
|
|
3854
4475
|
}
|
|
3855
4476
|
}
|
|
3856
4477
|
async function findServercraftModules() {
|
|
3857
|
-
const scriptDir =
|
|
4478
|
+
const scriptDir = path7.dirname(new URL(import.meta.url).pathname);
|
|
3858
4479
|
const possiblePaths = [
|
|
3859
4480
|
// Local node_modules (when servcraft is a dependency)
|
|
3860
|
-
|
|
4481
|
+
path7.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
|
|
3861
4482
|
// From dist/cli/index.js -> src/modules (npx or global install)
|
|
3862
|
-
|
|
4483
|
+
path7.resolve(scriptDir, "..", "..", "src", "modules"),
|
|
3863
4484
|
// From src/cli/commands/add-module.ts -> src/modules (development)
|
|
3864
|
-
|
|
4485
|
+
path7.resolve(scriptDir, "..", "..", "modules")
|
|
3865
4486
|
];
|
|
3866
4487
|
for (const p of possiblePaths) {
|
|
3867
4488
|
try {
|
|
@@ -3885,7 +4506,7 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
3885
4506
|
const sourceDirName = moduleNameMap[moduleName] || moduleName;
|
|
3886
4507
|
const servercraftModulesDir = await findServercraftModules();
|
|
3887
4508
|
if (servercraftModulesDir) {
|
|
3888
|
-
const sourceModuleDir =
|
|
4509
|
+
const sourceModuleDir = path7.join(servercraftModulesDir, sourceDirName);
|
|
3889
4510
|
if (await fileExists(sourceModuleDir)) {
|
|
3890
4511
|
await copyModuleFromSource(sourceModuleDir, moduleDir);
|
|
3891
4512
|
return;
|
|
@@ -3917,8 +4538,8 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
3917
4538
|
async function copyModuleFromSource(sourceDir, targetDir) {
|
|
3918
4539
|
const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
|
|
3919
4540
|
for (const entry of entries) {
|
|
3920
|
-
const sourcePath =
|
|
3921
|
-
const targetPath =
|
|
4541
|
+
const sourcePath = path7.join(sourceDir, entry.name);
|
|
4542
|
+
const targetPath = path7.join(targetDir, entry.name);
|
|
3922
4543
|
if (entry.isDirectory()) {
|
|
3923
4544
|
await fs5.mkdir(targetPath, { recursive: true });
|
|
3924
4545
|
await copyModuleFromSource(sourcePath, targetPath);
|
|
@@ -3931,7 +4552,7 @@ async function getModuleFiles(moduleName, moduleDir) {
|
|
|
3931
4552
|
const files = {};
|
|
3932
4553
|
const entries = await fs5.readdir(moduleDir);
|
|
3933
4554
|
for (const entry of entries) {
|
|
3934
|
-
const filePath =
|
|
4555
|
+
const filePath = path7.join(moduleDir, entry);
|
|
3935
4556
|
const stat2 = await fs5.stat(filePath);
|
|
3936
4557
|
if (stat2.isFile() && entry.endsWith(".ts")) {
|
|
3937
4558
|
const content = await fs5.readFile(filePath, "utf-8");
|
|
@@ -3942,14 +4563,14 @@ async function getModuleFiles(moduleName, moduleDir) {
|
|
|
3942
4563
|
}
|
|
3943
4564
|
async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
3944
4565
|
const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
|
|
3945
|
-
console.log(
|
|
4566
|
+
console.log(chalk7.cyan(`
|
|
3946
4567
|
\u{1F4CA} Changes in module "${moduleName}":
|
|
3947
4568
|
`));
|
|
3948
4569
|
for (const file of modifiedFiles) {
|
|
3949
4570
|
if (file.isModified) {
|
|
3950
|
-
console.log(
|
|
4571
|
+
console.log(chalk7.yellow(`
|
|
3951
4572
|
\u{1F4C4} ${file.fileName}:`));
|
|
3952
|
-
const currentPath =
|
|
4573
|
+
const currentPath = path7.join(moduleDir, file.fileName);
|
|
3953
4574
|
const currentContent = await fs5.readFile(currentPath, "utf-8");
|
|
3954
4575
|
const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
|
|
3955
4576
|
if (originalContent) {
|
|
@@ -3962,11 +4583,11 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
|
3962
4583
|
async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
|
|
3963
4584
|
const spinner = ora3("Analyzing files for merge...").start();
|
|
3964
4585
|
const newFiles = {};
|
|
3965
|
-
const templateDir =
|
|
4586
|
+
const templateDir = path7.join(templateManager["templatesDir"], moduleName);
|
|
3966
4587
|
try {
|
|
3967
4588
|
const entries = await fs5.readdir(templateDir);
|
|
3968
4589
|
for (const entry of entries) {
|
|
3969
|
-
const content = await fs5.readFile(
|
|
4590
|
+
const content = await fs5.readFile(path7.join(templateDir, entry), "utf-8");
|
|
3970
4591
|
newFiles[entry] = content;
|
|
3971
4592
|
}
|
|
3972
4593
|
} catch {
|
|
@@ -3984,7 +4605,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
3984
4605
|
};
|
|
3985
4606
|
for (const fileInfo of modifiedFiles) {
|
|
3986
4607
|
const fileName = fileInfo.fileName;
|
|
3987
|
-
const filePath =
|
|
4608
|
+
const filePath = path7.join(moduleDir, fileName);
|
|
3988
4609
|
const newContent = newFiles[fileName];
|
|
3989
4610
|
if (!newContent) {
|
|
3990
4611
|
continue;
|
|
@@ -4056,7 +4677,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4056
4677
|
import { Command as Command4 } from "commander";
|
|
4057
4678
|
import { execSync as execSync2, spawn } from "child_process";
|
|
4058
4679
|
import ora4 from "ora";
|
|
4059
|
-
import
|
|
4680
|
+
import chalk8 from "chalk";
|
|
4060
4681
|
var dbCommand = new Command4("db").description("Database management commands");
|
|
4061
4682
|
dbCommand.command("migrate").description("Run database migrations").option("-n, --name <name>", "Migration name").action(async (options) => {
|
|
4062
4683
|
const spinner = ora4("Running migrations...").start();
|
|
@@ -4113,7 +4734,7 @@ dbCommand.command("seed").description("Run database seed").action(async () => {
|
|
|
4113
4734
|
});
|
|
4114
4735
|
dbCommand.command("reset").description("Reset database (drop all data and re-run migrations)").option("-f, --force", "Skip confirmation").action(async (options) => {
|
|
4115
4736
|
if (!options.force) {
|
|
4116
|
-
console.log(
|
|
4737
|
+
console.log(chalk8.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
|
|
4117
4738
|
const readline = await import("readline");
|
|
4118
4739
|
const rl = readline.createInterface({
|
|
4119
4740
|
input: process.stdin,
|
|
@@ -4147,14 +4768,14 @@ dbCommand.command("status").description("Show migration status").action(async ()
|
|
|
4147
4768
|
|
|
4148
4769
|
// src/cli/commands/docs.ts
|
|
4149
4770
|
import { Command as Command5 } from "commander";
|
|
4150
|
-
import
|
|
4771
|
+
import path9 from "path";
|
|
4151
4772
|
import fs7 from "fs/promises";
|
|
4152
4773
|
import ora6 from "ora";
|
|
4153
|
-
import
|
|
4774
|
+
import chalk9 from "chalk";
|
|
4154
4775
|
|
|
4155
4776
|
// src/cli/utils/docs-generator.ts
|
|
4156
4777
|
import fs6 from "fs/promises";
|
|
4157
|
-
import
|
|
4778
|
+
import path8 from "path";
|
|
4158
4779
|
import ora5 from "ora";
|
|
4159
4780
|
|
|
4160
4781
|
// src/core/server.ts
|
|
@@ -4875,11 +5496,11 @@ function validateQuery(schema, data) {
|
|
|
4875
5496
|
function formatZodErrors(error2) {
|
|
4876
5497
|
const errors = {};
|
|
4877
5498
|
for (const issue of error2.issues) {
|
|
4878
|
-
const
|
|
4879
|
-
if (!errors[
|
|
4880
|
-
errors[
|
|
5499
|
+
const path12 = issue.path.join(".") || "root";
|
|
5500
|
+
if (!errors[path12]) {
|
|
5501
|
+
errors[path12] = [];
|
|
4881
5502
|
}
|
|
4882
|
-
errors[
|
|
5503
|
+
errors[path12].push(issue.message);
|
|
4883
5504
|
}
|
|
4884
5505
|
return errors;
|
|
4885
5506
|
}
|
|
@@ -5712,8 +6333,8 @@ async function generateDocs(outputPath = "openapi.json", silent = false) {
|
|
|
5712
6333
|
await registerUserModule(app, authService);
|
|
5713
6334
|
await app.ready();
|
|
5714
6335
|
const spec = app.swagger();
|
|
5715
|
-
const absoluteOutput =
|
|
5716
|
-
await fs6.mkdir(
|
|
6336
|
+
const absoluteOutput = path8.resolve(outputPath);
|
|
6337
|
+
await fs6.mkdir(path8.dirname(absoluteOutput), { recursive: true });
|
|
5717
6338
|
await fs6.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
|
|
5718
6339
|
spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
|
|
5719
6340
|
await app.close();
|
|
@@ -5758,7 +6379,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
5758
6379
|
const spinner = ora6("Exporting documentation...").start();
|
|
5759
6380
|
try {
|
|
5760
6381
|
const projectRoot = getProjectRoot();
|
|
5761
|
-
const specPath =
|
|
6382
|
+
const specPath = path9.join(projectRoot, "openapi.json");
|
|
5762
6383
|
try {
|
|
5763
6384
|
await fs7.access(specPath);
|
|
5764
6385
|
} catch {
|
|
@@ -5785,7 +6406,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
5785
6406
|
default:
|
|
5786
6407
|
throw new Error(`Unknown format: ${options.format}`);
|
|
5787
6408
|
}
|
|
5788
|
-
const outPath =
|
|
6409
|
+
const outPath = path9.join(projectRoot, options.output || defaultName);
|
|
5789
6410
|
await fs7.writeFile(outPath, output);
|
|
5790
6411
|
spinner.succeed(`Exported to: ${options.output || defaultName}`);
|
|
5791
6412
|
if (options.format === "postman") {
|
|
@@ -5798,8 +6419,8 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
5798
6419
|
});
|
|
5799
6420
|
docsCommand.command("status").description("Show documentation status").action(async () => {
|
|
5800
6421
|
const projectRoot = getProjectRoot();
|
|
5801
|
-
console.log(
|
|
5802
|
-
const specPath =
|
|
6422
|
+
console.log(chalk9.bold("\n\u{1F4CA} Documentation Status\n"));
|
|
6423
|
+
const specPath = path9.join(projectRoot, "openapi.json");
|
|
5803
6424
|
try {
|
|
5804
6425
|
const stat2 = await fs7.stat(specPath);
|
|
5805
6426
|
success(
|
|
@@ -5914,13 +6535,736 @@ function formatDate(date) {
|
|
|
5914
6535
|
});
|
|
5915
6536
|
}
|
|
5916
6537
|
|
|
6538
|
+
// src/cli/commands/list.ts
|
|
6539
|
+
import { Command as Command6 } from "commander";
|
|
6540
|
+
import chalk10 from "chalk";
|
|
6541
|
+
import fs8 from "fs/promises";
|
|
6542
|
+
var AVAILABLE_MODULES2 = {
|
|
6543
|
+
// Core
|
|
6544
|
+
auth: {
|
|
6545
|
+
name: "Authentication",
|
|
6546
|
+
description: "JWT authentication with access/refresh tokens",
|
|
6547
|
+
category: "Core"
|
|
6548
|
+
},
|
|
6549
|
+
users: {
|
|
6550
|
+
name: "User Management",
|
|
6551
|
+
description: "User CRUD with RBAC (roles & permissions)",
|
|
6552
|
+
category: "Core"
|
|
6553
|
+
},
|
|
6554
|
+
email: {
|
|
6555
|
+
name: "Email Service",
|
|
6556
|
+
description: "SMTP email with templates (Handlebars)",
|
|
6557
|
+
category: "Core"
|
|
6558
|
+
},
|
|
6559
|
+
// Security
|
|
6560
|
+
mfa: {
|
|
6561
|
+
name: "MFA/TOTP",
|
|
6562
|
+
description: "Two-factor authentication with QR codes",
|
|
6563
|
+
category: "Security"
|
|
6564
|
+
},
|
|
6565
|
+
oauth: {
|
|
6566
|
+
name: "OAuth",
|
|
6567
|
+
description: "Social login (Google, GitHub, Facebook, Twitter, Apple)",
|
|
6568
|
+
category: "Security"
|
|
6569
|
+
},
|
|
6570
|
+
"rate-limit": {
|
|
6571
|
+
name: "Rate Limiting",
|
|
6572
|
+
description: "Advanced rate limiting with multiple algorithms",
|
|
6573
|
+
category: "Security"
|
|
6574
|
+
},
|
|
6575
|
+
// Data & Storage
|
|
6576
|
+
cache: {
|
|
6577
|
+
name: "Redis Cache",
|
|
6578
|
+
description: "Redis caching with TTL & invalidation",
|
|
6579
|
+
category: "Data & Storage"
|
|
6580
|
+
},
|
|
6581
|
+
upload: {
|
|
6582
|
+
name: "File Upload",
|
|
6583
|
+
description: "File upload with local/S3/Cloudinary storage",
|
|
6584
|
+
category: "Data & Storage"
|
|
6585
|
+
},
|
|
6586
|
+
search: {
|
|
6587
|
+
name: "Search",
|
|
6588
|
+
description: "Full-text search with Elasticsearch/Meilisearch",
|
|
6589
|
+
category: "Data & Storage"
|
|
6590
|
+
},
|
|
6591
|
+
// Communication
|
|
6592
|
+
notification: {
|
|
6593
|
+
name: "Notifications",
|
|
6594
|
+
description: "Email, SMS, Push notifications",
|
|
6595
|
+
category: "Communication"
|
|
6596
|
+
},
|
|
6597
|
+
webhook: {
|
|
6598
|
+
name: "Webhooks",
|
|
6599
|
+
description: "Outgoing webhooks with HMAC signatures & retry",
|
|
6600
|
+
category: "Communication"
|
|
6601
|
+
},
|
|
6602
|
+
websocket: {
|
|
6603
|
+
name: "WebSockets",
|
|
6604
|
+
description: "Real-time communication with Socket.io",
|
|
6605
|
+
category: "Communication"
|
|
6606
|
+
},
|
|
6607
|
+
// Background Processing
|
|
6608
|
+
queue: {
|
|
6609
|
+
name: "Queue/Jobs",
|
|
6610
|
+
description: "Background jobs with Bull/BullMQ & cron scheduling",
|
|
6611
|
+
category: "Background Processing"
|
|
6612
|
+
},
|
|
6613
|
+
"media-processing": {
|
|
6614
|
+
name: "Media Processing",
|
|
6615
|
+
description: "Image/video processing with FFmpeg",
|
|
6616
|
+
category: "Background Processing"
|
|
6617
|
+
},
|
|
6618
|
+
// Monitoring & Analytics
|
|
6619
|
+
audit: {
|
|
6620
|
+
name: "Audit Logs",
|
|
6621
|
+
description: "Activity logging and audit trail",
|
|
6622
|
+
category: "Monitoring & Analytics"
|
|
6623
|
+
},
|
|
6624
|
+
analytics: {
|
|
6625
|
+
name: "Analytics/Metrics",
|
|
6626
|
+
description: "Prometheus metrics & event tracking",
|
|
6627
|
+
category: "Monitoring & Analytics"
|
|
6628
|
+
},
|
|
6629
|
+
// Internationalization
|
|
6630
|
+
i18n: {
|
|
6631
|
+
name: "i18n/Localization",
|
|
6632
|
+
description: "Multi-language support with 7+ locales",
|
|
6633
|
+
category: "Internationalization"
|
|
6634
|
+
},
|
|
6635
|
+
// API Management
|
|
6636
|
+
"feature-flag": {
|
|
6637
|
+
name: "Feature Flags",
|
|
6638
|
+
description: "A/B testing & progressive rollout",
|
|
6639
|
+
category: "API Management"
|
|
6640
|
+
},
|
|
6641
|
+
"api-versioning": {
|
|
6642
|
+
name: "API Versioning",
|
|
6643
|
+
description: "Multiple API versions support",
|
|
6644
|
+
category: "API Management"
|
|
6645
|
+
},
|
|
6646
|
+
// Payments
|
|
6647
|
+
payment: {
|
|
6648
|
+
name: "Payments",
|
|
6649
|
+
description: "Payment processing (Stripe, PayPal, Mobile Money)",
|
|
6650
|
+
category: "Payments"
|
|
6651
|
+
}
|
|
6652
|
+
};
|
|
6653
|
+
async function getInstalledModules() {
|
|
6654
|
+
try {
|
|
6655
|
+
const modulesDir = getModulesDir();
|
|
6656
|
+
const entries = await fs8.readdir(modulesDir, { withFileTypes: true });
|
|
6657
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6658
|
+
} catch {
|
|
6659
|
+
return [];
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
function isServercraftProject() {
|
|
6663
|
+
try {
|
|
6664
|
+
getProjectRoot();
|
|
6665
|
+
return true;
|
|
6666
|
+
} catch {
|
|
6667
|
+
return false;
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
var listCommand = new Command6("list").alias("ls").description("List available and installed modules").option("-a, --available", "Show only available modules").option("-i, --installed", "Show only installed modules").option("-c, --category <category>", "Filter by category").option("--json", "Output as JSON").action(
|
|
6671
|
+
async (options) => {
|
|
6672
|
+
const installedModules = await getInstalledModules();
|
|
6673
|
+
const isProject = isServercraftProject();
|
|
6674
|
+
if (options.json) {
|
|
6675
|
+
const output = {
|
|
6676
|
+
available: Object.entries(AVAILABLE_MODULES2).map(([key, mod]) => ({
|
|
6677
|
+
id: key,
|
|
6678
|
+
...mod,
|
|
6679
|
+
installed: installedModules.includes(key)
|
|
6680
|
+
}))
|
|
6681
|
+
};
|
|
6682
|
+
if (isProject) {
|
|
6683
|
+
output.installed = installedModules;
|
|
6684
|
+
}
|
|
6685
|
+
console.log(JSON.stringify(output, null, 2));
|
|
6686
|
+
return;
|
|
6687
|
+
}
|
|
6688
|
+
const byCategory = {};
|
|
6689
|
+
for (const [key, mod] of Object.entries(AVAILABLE_MODULES2)) {
|
|
6690
|
+
if (options.category && mod.category.toLowerCase() !== options.category.toLowerCase()) {
|
|
6691
|
+
continue;
|
|
6692
|
+
}
|
|
6693
|
+
if (!byCategory[mod.category]) {
|
|
6694
|
+
byCategory[mod.category] = [];
|
|
6695
|
+
}
|
|
6696
|
+
byCategory[mod.category]?.push({
|
|
6697
|
+
id: key,
|
|
6698
|
+
name: mod.name,
|
|
6699
|
+
description: mod.description,
|
|
6700
|
+
installed: installedModules.includes(key)
|
|
6701
|
+
});
|
|
6702
|
+
}
|
|
6703
|
+
if (options.installed) {
|
|
6704
|
+
if (!isProject) {
|
|
6705
|
+
console.log(chalk10.yellow("\n\u26A0 Not in a Servcraft project directory\n"));
|
|
6706
|
+
return;
|
|
6707
|
+
}
|
|
6708
|
+
console.log(chalk10.bold("\n\u{1F4E6} Installed Modules:\n"));
|
|
6709
|
+
if (installedModules.length === 0) {
|
|
6710
|
+
console.log(chalk10.gray(" No modules installed yet.\n"));
|
|
6711
|
+
console.log(` Run ${chalk10.cyan("servcraft add <module>")} to add a module.
|
|
6712
|
+
`);
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6715
|
+
for (const modId of installedModules) {
|
|
6716
|
+
const mod = AVAILABLE_MODULES2[modId];
|
|
6717
|
+
if (mod) {
|
|
6718
|
+
console.log(` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${mod.name}`);
|
|
6719
|
+
} else {
|
|
6720
|
+
console.log(
|
|
6721
|
+
` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${chalk10.gray("(custom module)")}`
|
|
6722
|
+
);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
console.log(`
|
|
6726
|
+
Total: ${chalk10.bold(installedModules.length)} module(s) installed
|
|
6727
|
+
`);
|
|
6728
|
+
return;
|
|
6729
|
+
}
|
|
6730
|
+
console.log(chalk10.bold("\n\u{1F4E6} Available Modules\n"));
|
|
6731
|
+
if (isProject) {
|
|
6732
|
+
console.log(
|
|
6733
|
+
chalk10.gray(` ${chalk10.green("\u2713")} = installed ${chalk10.dim("\u25CB")} = not installed
|
|
6734
|
+
`)
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6737
|
+
for (const [category, modules] of Object.entries(byCategory)) {
|
|
6738
|
+
console.log(chalk10.bold.blue(` ${category}`));
|
|
6739
|
+
console.log(chalk10.gray(" " + "\u2500".repeat(40)));
|
|
6740
|
+
for (const mod of modules) {
|
|
6741
|
+
const status = isProject ? mod.installed ? chalk10.green("\u2713") : chalk10.dim("\u25CB") : " ";
|
|
6742
|
+
const nameColor = mod.installed ? chalk10.green : chalk10.cyan;
|
|
6743
|
+
console.log(` ${status} ${nameColor(mod.id.padEnd(18))} ${mod.name}`);
|
|
6744
|
+
console.log(` ${chalk10.gray(mod.description)}`);
|
|
6745
|
+
}
|
|
6746
|
+
console.log();
|
|
6747
|
+
}
|
|
6748
|
+
const totalAvailable = Object.keys(AVAILABLE_MODULES2).length;
|
|
6749
|
+
const totalInstalled = installedModules.filter((m) => AVAILABLE_MODULES2[m]).length;
|
|
6750
|
+
console.log(chalk10.gray("\u2500".repeat(50)));
|
|
6751
|
+
console.log(
|
|
6752
|
+
` ${chalk10.bold(totalAvailable)} modules available` + (isProject ? ` | ${chalk10.green.bold(totalInstalled)} installed` : "")
|
|
6753
|
+
);
|
|
6754
|
+
console.log();
|
|
6755
|
+
console.log(chalk10.bold(" Usage:"));
|
|
6756
|
+
console.log(` ${chalk10.yellow("servcraft add <module>")} Add a module`);
|
|
6757
|
+
console.log(` ${chalk10.yellow("servcraft list --installed")} Show installed only`);
|
|
6758
|
+
console.log(` ${chalk10.yellow("servcraft list --category Security")} Filter by category`);
|
|
6759
|
+
console.log();
|
|
6760
|
+
}
|
|
6761
|
+
);
|
|
6762
|
+
|
|
6763
|
+
// src/cli/commands/remove.ts
|
|
6764
|
+
import { Command as Command7 } from "commander";
|
|
6765
|
+
import path10 from "path";
|
|
6766
|
+
import ora7 from "ora";
|
|
6767
|
+
import chalk11 from "chalk";
|
|
6768
|
+
import fs9 from "fs/promises";
|
|
6769
|
+
import inquirer4 from "inquirer";
|
|
6770
|
+
var removeCommand = new Command7("remove").alias("rm").description("Remove an installed module from your project").argument("<module>", "Module to remove").option("-y, --yes", "Skip confirmation prompt").option("--keep-env", "Keep environment variables").action(async (moduleName, options) => {
|
|
6771
|
+
const projectError = validateProject();
|
|
6772
|
+
if (projectError) {
|
|
6773
|
+
displayError(projectError);
|
|
6774
|
+
return;
|
|
6775
|
+
}
|
|
6776
|
+
console.log(chalk11.bold.cyan("\n\u{1F5D1}\uFE0F ServCraft Module Removal\n"));
|
|
6777
|
+
const moduleDir = path10.join(getModulesDir(), moduleName);
|
|
6778
|
+
try {
|
|
6779
|
+
const exists = await fs9.access(moduleDir).then(() => true).catch(() => false);
|
|
6780
|
+
if (!exists) {
|
|
6781
|
+
displayError(
|
|
6782
|
+
new ServCraftError(`Module "${moduleName}" is not installed`, [
|
|
6783
|
+
`Run ${chalk11.cyan("servcraft list --installed")} to see installed modules`,
|
|
6784
|
+
`Check the spelling of the module name`
|
|
6785
|
+
])
|
|
6786
|
+
);
|
|
6787
|
+
return;
|
|
6788
|
+
}
|
|
6789
|
+
const files = await fs9.readdir(moduleDir);
|
|
6790
|
+
const fileCount = files.length;
|
|
6791
|
+
if (!options?.yes) {
|
|
6792
|
+
console.log(chalk11.yellow(`\u26A0 This will remove the "${moduleName}" module:`));
|
|
6793
|
+
console.log(chalk11.gray(` Directory: ${moduleDir}`));
|
|
6794
|
+
console.log(chalk11.gray(` Files: ${fileCount} file(s)`));
|
|
6795
|
+
console.log();
|
|
6796
|
+
const { confirm } = await inquirer4.prompt([
|
|
6797
|
+
{
|
|
6798
|
+
type: "confirm",
|
|
6799
|
+
name: "confirm",
|
|
6800
|
+
message: "Are you sure you want to remove this module?",
|
|
6801
|
+
default: false
|
|
6802
|
+
}
|
|
6803
|
+
]);
|
|
6804
|
+
if (!confirm) {
|
|
6805
|
+
console.log(chalk11.yellow("\n\u2716 Removal cancelled\n"));
|
|
6806
|
+
return;
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
const spinner = ora7("Removing module...").start();
|
|
6810
|
+
await fs9.rm(moduleDir, { recursive: true, force: true });
|
|
6811
|
+
spinner.succeed(`Module "${moduleName}" removed successfully!`);
|
|
6812
|
+
console.log("\n" + chalk11.bold("\u2713 Removed:"));
|
|
6813
|
+
success(` src/modules/${moduleName}/ (${fileCount} files)`);
|
|
6814
|
+
if (!options?.keepEnv) {
|
|
6815
|
+
console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
|
|
6816
|
+
info(" 1. Remove environment variables related to this module from .env");
|
|
6817
|
+
info(" 2. Remove module imports from your main app file");
|
|
6818
|
+
info(" 3. Remove related database migrations if any");
|
|
6819
|
+
info(" 4. Update your routes if they reference this module");
|
|
6820
|
+
} else {
|
|
6821
|
+
console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
|
|
6822
|
+
info(" 1. Environment variables were kept (--keep-env flag)");
|
|
6823
|
+
info(" 2. Remove module imports from your main app file");
|
|
6824
|
+
info(" 3. Update your routes if they reference this module");
|
|
6825
|
+
}
|
|
6826
|
+
console.log();
|
|
6827
|
+
} catch (err) {
|
|
6828
|
+
error(err instanceof Error ? err.message : String(err));
|
|
6829
|
+
console.log();
|
|
6830
|
+
}
|
|
6831
|
+
});
|
|
6832
|
+
|
|
6833
|
+
// src/cli/commands/doctor.ts
|
|
6834
|
+
import { Command as Command8 } from "commander";
|
|
6835
|
+
import chalk12 from "chalk";
|
|
6836
|
+
import fs10 from "fs/promises";
|
|
6837
|
+
async function checkNodeVersion() {
|
|
6838
|
+
const version = process.version;
|
|
6839
|
+
const major = parseInt(version.slice(1).split(".")[0] || "0", 10);
|
|
6840
|
+
if (major >= 18) {
|
|
6841
|
+
return { name: "Node.js", status: "pass", message: `${version} \u2713` };
|
|
6842
|
+
}
|
|
6843
|
+
return {
|
|
6844
|
+
name: "Node.js",
|
|
6845
|
+
status: "fail",
|
|
6846
|
+
message: `${version} (< 18)`,
|
|
6847
|
+
suggestion: "Upgrade to Node.js 18+"
|
|
6848
|
+
};
|
|
6849
|
+
}
|
|
6850
|
+
async function checkPackageJson() {
|
|
6851
|
+
const checks = [];
|
|
6852
|
+
try {
|
|
6853
|
+
const content = await fs10.readFile("package.json", "utf-8");
|
|
6854
|
+
const pkg = JSON.parse(content);
|
|
6855
|
+
checks.push({ name: "package.json", status: "pass", message: "Found" });
|
|
6856
|
+
if (pkg.dependencies?.fastify) {
|
|
6857
|
+
checks.push({ name: "Fastify", status: "pass", message: "Installed" });
|
|
6858
|
+
} else {
|
|
6859
|
+
checks.push({
|
|
6860
|
+
name: "Fastify",
|
|
6861
|
+
status: "fail",
|
|
6862
|
+
message: "Missing",
|
|
6863
|
+
suggestion: "npm install fastify"
|
|
6864
|
+
});
|
|
6865
|
+
}
|
|
6866
|
+
} catch {
|
|
6867
|
+
checks.push({
|
|
6868
|
+
name: "package.json",
|
|
6869
|
+
status: "fail",
|
|
6870
|
+
message: "Not found",
|
|
6871
|
+
suggestion: "Run servcraft init"
|
|
6872
|
+
});
|
|
6873
|
+
}
|
|
6874
|
+
return checks;
|
|
6875
|
+
}
|
|
6876
|
+
async function checkDirectories() {
|
|
6877
|
+
const checks = [];
|
|
6878
|
+
const dirs = ["src", "node_modules", ".git", ".env"];
|
|
6879
|
+
for (const dir of dirs) {
|
|
6880
|
+
try {
|
|
6881
|
+
await fs10.access(dir);
|
|
6882
|
+
checks.push({ name: dir, status: "pass", message: "Exists" });
|
|
6883
|
+
} catch {
|
|
6884
|
+
const isCritical = dir === "src" || dir === "node_modules";
|
|
6885
|
+
checks.push({
|
|
6886
|
+
name: dir,
|
|
6887
|
+
status: isCritical ? "fail" : "warn",
|
|
6888
|
+
message: "Not found",
|
|
6889
|
+
suggestion: dir === "node_modules" ? "npm install" : dir === ".env" ? "Create .env file" : void 0
|
|
6890
|
+
});
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
return checks;
|
|
6894
|
+
}
|
|
6895
|
+
var doctorCommand = new Command8("doctor").description("Diagnose project configuration and dependencies").action(async () => {
|
|
6896
|
+
console.log(chalk12.bold.cyan("\n\u{1F50D} ServCraft Doctor\n"));
|
|
6897
|
+
const allChecks = [];
|
|
6898
|
+
allChecks.push(await checkNodeVersion());
|
|
6899
|
+
allChecks.push(...await checkPackageJson());
|
|
6900
|
+
allChecks.push(...await checkDirectories());
|
|
6901
|
+
allChecks.forEach((check) => {
|
|
6902
|
+
const icon = check.status === "pass" ? chalk12.green("\u2713") : check.status === "warn" ? chalk12.yellow("\u26A0") : chalk12.red("\u2717");
|
|
6903
|
+
const color = check.status === "pass" ? chalk12.green : check.status === "warn" ? chalk12.yellow : chalk12.red;
|
|
6904
|
+
console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
|
|
6905
|
+
if (check.suggestion) {
|
|
6906
|
+
console.log(chalk12.gray(` \u2192 ${check.suggestion}`));
|
|
6907
|
+
}
|
|
6908
|
+
});
|
|
6909
|
+
const pass = allChecks.filter((c) => c.status === "pass").length;
|
|
6910
|
+
const warn2 = allChecks.filter((c) => c.status === "warn").length;
|
|
6911
|
+
const fail = allChecks.filter((c) => c.status === "fail").length;
|
|
6912
|
+
console.log(chalk12.gray("\n" + "\u2500".repeat(60)));
|
|
6913
|
+
console.log(
|
|
6914
|
+
`
|
|
6915
|
+
${chalk12.green(pass + " passed")} | ${chalk12.yellow(warn2 + " warnings")} | ${chalk12.red(fail + " failed")}
|
|
6916
|
+
`
|
|
6917
|
+
);
|
|
6918
|
+
if (fail === 0 && warn2 === 0) {
|
|
6919
|
+
console.log(chalk12.green.bold("\u2728 Everything looks good!\n"));
|
|
6920
|
+
} else if (fail > 0) {
|
|
6921
|
+
console.log(chalk12.red.bold("\u2717 Fix critical issues before using ServCraft.\n"));
|
|
6922
|
+
} else {
|
|
6923
|
+
console.log(chalk12.yellow.bold("\u26A0 Some warnings, but should work.\n"));
|
|
6924
|
+
}
|
|
6925
|
+
});
|
|
6926
|
+
|
|
6927
|
+
// src/cli/commands/update.ts
|
|
6928
|
+
import { Command as Command9 } from "commander";
|
|
6929
|
+
import chalk13 from "chalk";
|
|
6930
|
+
import fs11 from "fs/promises";
|
|
6931
|
+
import path11 from "path";
|
|
6932
|
+
import { fileURLToPath } from "url";
|
|
6933
|
+
import inquirer5 from "inquirer";
|
|
6934
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
6935
|
+
var __dirname2 = path11.dirname(__filename2);
|
|
6936
|
+
var AVAILABLE_MODULES3 = [
|
|
6937
|
+
"auth",
|
|
6938
|
+
"users",
|
|
6939
|
+
"email",
|
|
6940
|
+
"mfa",
|
|
6941
|
+
"oauth",
|
|
6942
|
+
"rate-limit",
|
|
6943
|
+
"cache",
|
|
6944
|
+
"upload",
|
|
6945
|
+
"search",
|
|
6946
|
+
"notification",
|
|
6947
|
+
"webhook",
|
|
6948
|
+
"websocket",
|
|
6949
|
+
"queue",
|
|
6950
|
+
"payment",
|
|
6951
|
+
"i18n",
|
|
6952
|
+
"feature-flag",
|
|
6953
|
+
"analytics",
|
|
6954
|
+
"media-processing",
|
|
6955
|
+
"api-versioning",
|
|
6956
|
+
"audit",
|
|
6957
|
+
"swagger",
|
|
6958
|
+
"validation"
|
|
6959
|
+
];
|
|
6960
|
+
async function getInstalledModules2() {
|
|
6961
|
+
try {
|
|
6962
|
+
const modulesDir = getModulesDir();
|
|
6963
|
+
const entries = await fs11.readdir(modulesDir, { withFileTypes: true });
|
|
6964
|
+
const installedModules = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => AVAILABLE_MODULES3.includes(name));
|
|
6965
|
+
return installedModules;
|
|
6966
|
+
} catch {
|
|
6967
|
+
return [];
|
|
6968
|
+
}
|
|
6969
|
+
}
|
|
6970
|
+
async function copyModuleFiles(moduleName, _projectRoot) {
|
|
6971
|
+
const cliRoot = path11.resolve(__dirname2, "../../../");
|
|
6972
|
+
const sourceModulePath = path11.join(cliRoot, "src", "modules", moduleName);
|
|
6973
|
+
const targetModulesDir = getModulesDir();
|
|
6974
|
+
const targetModulePath = path11.join(targetModulesDir, moduleName);
|
|
6975
|
+
try {
|
|
6976
|
+
await fs11.access(sourceModulePath);
|
|
6977
|
+
} catch {
|
|
6978
|
+
throw new Error(`Module source not found: ${moduleName}`);
|
|
6979
|
+
}
|
|
6980
|
+
await fs11.cp(sourceModulePath, targetModulePath, { recursive: true });
|
|
6981
|
+
}
|
|
6982
|
+
async function updateModule(moduleName, options) {
|
|
6983
|
+
const projectError = validateProject();
|
|
6984
|
+
if (projectError) {
|
|
6985
|
+
displayError(projectError);
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
const projectRoot = getProjectRoot();
|
|
6989
|
+
const installedModules = await getInstalledModules2();
|
|
6990
|
+
if (!installedModules.includes(moduleName)) {
|
|
6991
|
+
console.log(chalk13.yellow(`
|
|
6992
|
+
\u26A0 Module "${moduleName}" is not installed
|
|
6993
|
+
`));
|
|
6994
|
+
console.log(
|
|
6995
|
+
chalk13.gray(`Run ${chalk13.cyan(`servcraft add ${moduleName}`)} to install it first.
|
|
6996
|
+
`)
|
|
6997
|
+
);
|
|
6998
|
+
return;
|
|
6999
|
+
}
|
|
7000
|
+
if (options.check) {
|
|
7001
|
+
console.log(chalk13.cyan(`
|
|
7002
|
+
\u{1F4E6} Checking updates for "${moduleName}"...
|
|
7003
|
+
`));
|
|
7004
|
+
console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
|
|
7005
|
+
console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7006
|
+
return;
|
|
7007
|
+
}
|
|
7008
|
+
const { confirmed } = await inquirer5.prompt([
|
|
7009
|
+
{
|
|
7010
|
+
type: "confirm",
|
|
7011
|
+
name: "confirmed",
|
|
7012
|
+
message: `Update "${moduleName}" module? This will overwrite existing files.`,
|
|
7013
|
+
default: false
|
|
7014
|
+
}
|
|
7015
|
+
]);
|
|
7016
|
+
if (!confirmed) {
|
|
7017
|
+
console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
console.log(chalk13.cyan(`
|
|
7021
|
+
\u{1F504} Updating "${moduleName}" module...
|
|
7022
|
+
`));
|
|
7023
|
+
try {
|
|
7024
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7025
|
+
console.log(chalk13.green(`\u2714 Module "${moduleName}" updated successfully!
|
|
7026
|
+
`));
|
|
7027
|
+
console.log(
|
|
7028
|
+
chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7029
|
+
);
|
|
7030
|
+
} catch (error2) {
|
|
7031
|
+
if (error2 instanceof Error) {
|
|
7032
|
+
console.error(chalk13.red(`
|
|
7033
|
+
\u2717 Failed to update module: ${error2.message}
|
|
7034
|
+
`));
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
}
|
|
7038
|
+
async function updateAllModules(options) {
|
|
7039
|
+
const projectError = validateProject();
|
|
7040
|
+
if (projectError) {
|
|
7041
|
+
displayError(projectError);
|
|
7042
|
+
return;
|
|
7043
|
+
}
|
|
7044
|
+
const installedModules = await getInstalledModules2();
|
|
7045
|
+
if (installedModules.length === 0) {
|
|
7046
|
+
console.log(chalk13.yellow("\n\u26A0 No modules installed\n"));
|
|
7047
|
+
console.log(chalk13.gray(`Run ${chalk13.cyan("servcraft list")} to see available modules.
|
|
7048
|
+
`));
|
|
7049
|
+
return;
|
|
7050
|
+
}
|
|
7051
|
+
if (options.check) {
|
|
7052
|
+
console.log(chalk13.cyan("\n\u{1F4E6} Checking updates for all modules...\n"));
|
|
7053
|
+
console.log(chalk13.bold("Installed modules:"));
|
|
7054
|
+
installedModules.forEach((mod) => {
|
|
7055
|
+
console.log(` \u2022 ${chalk13.cyan(mod)}`);
|
|
7056
|
+
});
|
|
7057
|
+
console.log();
|
|
7058
|
+
console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
|
|
7059
|
+
console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7060
|
+
return;
|
|
7061
|
+
}
|
|
7062
|
+
console.log(chalk13.cyan(`
|
|
7063
|
+
\u{1F4E6} Found ${installedModules.length} installed module(s):
|
|
7064
|
+
`));
|
|
7065
|
+
installedModules.forEach((mod) => {
|
|
7066
|
+
console.log(` \u2022 ${chalk13.cyan(mod)}`);
|
|
7067
|
+
});
|
|
7068
|
+
console.log();
|
|
7069
|
+
const { confirmed } = await inquirer5.prompt([
|
|
7070
|
+
{
|
|
7071
|
+
type: "confirm",
|
|
7072
|
+
name: "confirmed",
|
|
7073
|
+
message: "Update all modules? This will overwrite existing files.",
|
|
7074
|
+
default: false
|
|
7075
|
+
}
|
|
7076
|
+
]);
|
|
7077
|
+
if (!confirmed) {
|
|
7078
|
+
console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
|
|
7079
|
+
return;
|
|
7080
|
+
}
|
|
7081
|
+
console.log(chalk13.cyan("\n\u{1F504} Updating all modules...\n"));
|
|
7082
|
+
const projectRoot = getProjectRoot();
|
|
7083
|
+
let successCount = 0;
|
|
7084
|
+
let failCount = 0;
|
|
7085
|
+
for (const moduleName of installedModules) {
|
|
7086
|
+
try {
|
|
7087
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7088
|
+
console.log(chalk13.green(`\u2714 Updated: ${moduleName}`));
|
|
7089
|
+
successCount++;
|
|
7090
|
+
} catch {
|
|
7091
|
+
console.error(chalk13.red(`\u2717 Failed: ${moduleName}`));
|
|
7092
|
+
failCount++;
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
console.log();
|
|
7096
|
+
console.log(
|
|
7097
|
+
chalk13.bold(
|
|
7098
|
+
`
|
|
7099
|
+
\u2714 Update complete: ${chalk13.green(successCount)} succeeded, ${chalk13.red(failCount)} failed
|
|
7100
|
+
`
|
|
7101
|
+
)
|
|
7102
|
+
);
|
|
7103
|
+
if (successCount > 0) {
|
|
7104
|
+
console.log(
|
|
7105
|
+
chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7106
|
+
);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
var updateCommand = new Command9("update").description("Update installed modules to latest version").argument("[module]", "Specific module to update").option("--check", "Check for updates without applying").option("-y, --yes", "Skip confirmation prompt").action(async (moduleName, options) => {
|
|
7110
|
+
if (moduleName) {
|
|
7111
|
+
await updateModule(moduleName, { check: options?.check });
|
|
7112
|
+
} else {
|
|
7113
|
+
await updateAllModules({ check: options?.check });
|
|
7114
|
+
}
|
|
7115
|
+
});
|
|
7116
|
+
|
|
7117
|
+
// src/cli/commands/completion.ts
|
|
7118
|
+
import { Command as Command10 } from "commander";
|
|
7119
|
+
var bashScript = `
|
|
7120
|
+
# servcraft bash completion script
|
|
7121
|
+
_servcraft_completions() {
|
|
7122
|
+
local cur prev words cword
|
|
7123
|
+
_init_completion || return
|
|
7124
|
+
|
|
7125
|
+
# Main commands
|
|
7126
|
+
local commands="init add generate list remove doctor update completion docs --version --help"
|
|
7127
|
+
|
|
7128
|
+
# Generate subcommands
|
|
7129
|
+
local generate_subcommands="module controller service repository types schema routes m c s r t"
|
|
7130
|
+
|
|
7131
|
+
case "\${words[1]}" in
|
|
7132
|
+
generate|g)
|
|
7133
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7134
|
+
COMPREPLY=( $(compgen -W "\${generate_subcommands}" -- "\${cur}") )
|
|
7135
|
+
fi
|
|
7136
|
+
;;
|
|
7137
|
+
add|remove|rm|update)
|
|
7138
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7139
|
+
# Get available modules
|
|
7140
|
+
local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
|
|
7141
|
+
COMPREPLY=( $(compgen -W "\${modules}" -- "\${cur}") )
|
|
7142
|
+
fi
|
|
7143
|
+
;;
|
|
7144
|
+
completion)
|
|
7145
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7146
|
+
COMPREPLY=( $(compgen -W "bash zsh" -- "\${cur}") )
|
|
7147
|
+
fi
|
|
7148
|
+
;;
|
|
7149
|
+
*)
|
|
7150
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
7151
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
7152
|
+
fi
|
|
7153
|
+
;;
|
|
7154
|
+
esac
|
|
7155
|
+
}
|
|
7156
|
+
|
|
7157
|
+
complete -F _servcraft_completions servcraft
|
|
7158
|
+
`;
|
|
7159
|
+
var zshScript = `
|
|
7160
|
+
#compdef servcraft
|
|
7161
|
+
|
|
7162
|
+
_servcraft() {
|
|
7163
|
+
local context state state_descr line
|
|
7164
|
+
typeset -A opt_args
|
|
7165
|
+
|
|
7166
|
+
_arguments -C \\
|
|
7167
|
+
'1: :_servcraft_commands' \\
|
|
7168
|
+
'*::arg:->args'
|
|
7169
|
+
|
|
7170
|
+
case $state in
|
|
7171
|
+
args)
|
|
7172
|
+
case $line[1] in
|
|
7173
|
+
generate|g)
|
|
7174
|
+
_servcraft_generate
|
|
7175
|
+
;;
|
|
7176
|
+
add|remove|rm|update)
|
|
7177
|
+
_servcraft_modules
|
|
7178
|
+
;;
|
|
7179
|
+
completion)
|
|
7180
|
+
_arguments '1: :(bash zsh)'
|
|
7181
|
+
;;
|
|
7182
|
+
esac
|
|
7183
|
+
;;
|
|
7184
|
+
esac
|
|
7185
|
+
}
|
|
7186
|
+
|
|
7187
|
+
_servcraft_commands() {
|
|
7188
|
+
local commands
|
|
7189
|
+
commands=(
|
|
7190
|
+
'init:Initialize a new ServCraft project'
|
|
7191
|
+
'add:Add a pre-built module to your project'
|
|
7192
|
+
'generate:Generate code files (controller, service, etc.)'
|
|
7193
|
+
'list:List available and installed modules'
|
|
7194
|
+
'remove:Remove an installed module'
|
|
7195
|
+
'doctor:Diagnose project configuration'
|
|
7196
|
+
'update:Update installed modules'
|
|
7197
|
+
'completion:Generate shell completion scripts'
|
|
7198
|
+
'docs:Open documentation'
|
|
7199
|
+
'--version:Show version'
|
|
7200
|
+
'--help:Show help'
|
|
7201
|
+
)
|
|
7202
|
+
_describe 'command' commands
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
_servcraft_generate() {
|
|
7206
|
+
local subcommands
|
|
7207
|
+
subcommands=(
|
|
7208
|
+
'module:Generate a complete module (controller + service + routes)'
|
|
7209
|
+
'controller:Generate a controller'
|
|
7210
|
+
'service:Generate a service'
|
|
7211
|
+
'repository:Generate a repository'
|
|
7212
|
+
'types:Generate TypeScript types'
|
|
7213
|
+
'schema:Generate validation schema'
|
|
7214
|
+
'routes:Generate routes file'
|
|
7215
|
+
'm:Alias for module'
|
|
7216
|
+
'c:Alias for controller'
|
|
7217
|
+
's:Alias for service'
|
|
7218
|
+
'r:Alias for repository'
|
|
7219
|
+
't:Alias for types'
|
|
7220
|
+
)
|
|
7221
|
+
_describe 'subcommand' subcommands
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
_servcraft_modules() {
|
|
7225
|
+
local modules
|
|
7226
|
+
modules=(
|
|
7227
|
+
'auth:Authentication & Authorization'
|
|
7228
|
+
'cache:Redis caching'
|
|
7229
|
+
'rate-limit:Rate limiting'
|
|
7230
|
+
'notification:Email/SMS notifications'
|
|
7231
|
+
'payment:Payment integration'
|
|
7232
|
+
'oauth:OAuth providers'
|
|
7233
|
+
'mfa:Multi-factor authentication'
|
|
7234
|
+
'queue:Background jobs'
|
|
7235
|
+
'websocket:WebSocket support'
|
|
7236
|
+
'upload:File upload handling'
|
|
7237
|
+
)
|
|
7238
|
+
_describe 'module' modules
|
|
7239
|
+
}
|
|
7240
|
+
|
|
7241
|
+
_servcraft "$@"
|
|
7242
|
+
`;
|
|
7243
|
+
var completionCommand = new Command10("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
|
|
7244
|
+
const shellLower = shell.toLowerCase();
|
|
7245
|
+
if (shellLower === "bash") {
|
|
7246
|
+
console.log(bashScript);
|
|
7247
|
+
} else if (shellLower === "zsh") {
|
|
7248
|
+
console.log(zshScript);
|
|
7249
|
+
} else {
|
|
7250
|
+
console.error(`Unsupported shell: ${shell}`);
|
|
7251
|
+
console.error("Supported shells: bash, zsh");
|
|
7252
|
+
process.exit(1);
|
|
7253
|
+
}
|
|
7254
|
+
});
|
|
7255
|
+
|
|
5917
7256
|
// src/cli/index.ts
|
|
5918
|
-
var program = new
|
|
7257
|
+
var program = new Command11();
|
|
5919
7258
|
program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
|
|
5920
7259
|
program.addCommand(initCommand);
|
|
5921
7260
|
program.addCommand(generateCommand);
|
|
5922
7261
|
program.addCommand(addModuleCommand);
|
|
5923
7262
|
program.addCommand(dbCommand);
|
|
5924
7263
|
program.addCommand(docsCommand);
|
|
7264
|
+
program.addCommand(listCommand);
|
|
7265
|
+
program.addCommand(removeCommand);
|
|
7266
|
+
program.addCommand(doctorCommand);
|
|
7267
|
+
program.addCommand(updateCommand);
|
|
7268
|
+
program.addCommand(completionCommand);
|
|
5925
7269
|
program.parse();
|
|
5926
7270
|
//# sourceMappingURL=index.js.map
|