go-gin-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs").promises;
4
+ const path = require("path");
5
+ const prompts = require("prompts");
6
+ const createDir = require("../shares/createDir");
7
+ const createFile = require("../shares/createFile");
8
+ const { COLORS } = require("../constants/index");
9
+
10
+ /**
11
+ * ServiceGenerator - Adds custom services to existing resources
12
+ * Following Clean Architecture pattern with action folders
13
+ *
14
+ * Creates:
15
+ * src/infrastructure/repositories/{module}/{serviceName}/
16
+ * ├── action.go
17
+ * └── validation.go
18
+ */
19
+ class ServiceGenerator {
20
+ constructor() {
21
+ this.moduleName = "";
22
+ this.serviceName = "";
23
+ this.serviceNamePascal = "";
24
+ this.serviceNameCamel = "";
25
+ this.goModule = "";
26
+
27
+ // Paths matching Clean Architecture structure
28
+ this.srcDir = "src";
29
+ this.domainDir = path.join(this.srcDir, "domain");
30
+ this.infrastructureDir = path.join(this.srcDir, "infrastructure");
31
+ this.usecasesDir = path.join(this.srcDir, "usecases");
32
+ }
33
+
34
+ getGoModuleName() {
35
+ try {
36
+ const goModPath = path.join(process.cwd(), "go.mod");
37
+ const content = require("fs").readFileSync(goModPath, "utf8");
38
+ const match = content.match(/module\s+(.+)/);
39
+ return match ? match[1].trim() : "github.com/example/project";
40
+ } catch {
41
+ return "github.com/example/project";
42
+ }
43
+ }
44
+
45
+ getRepositoryFolders() {
46
+ try {
47
+ const repoPath = path.join(this.infrastructureDir, "repositories");
48
+
49
+ if (!require("fs").existsSync(repoPath)) {
50
+ return [];
51
+ }
52
+
53
+ return require("fs").readdirSync(repoPath, { withFileTypes: true })
54
+ .filter(dirent => dirent.isDirectory())
55
+ .map(dirent => dirent.name)
56
+ .filter(folder => folder !== "base");
57
+ } catch (error) {
58
+ console.error(`${COLORS.RED}❌ Error listing modules: ${error.message}${COLORS.NC}`);
59
+ return [];
60
+ }
61
+ }
62
+
63
+ validateServiceName(name) {
64
+ const isValid = /^[a-zA-Z][a-zA-Z0-9]*$/.test(name);
65
+ if (!isValid) {
66
+ console.error(`${COLORS.RED}Service name must start with a letter and contain only letters and numbers${COLORS.NC}`);
67
+ }
68
+ return isValid;
69
+ }
70
+
71
+ toPascalCase(str) {
72
+ return str.charAt(0).toUpperCase() + str.slice(1);
73
+ }
74
+
75
+ toCamelCase(str) {
76
+ return str.charAt(0).toLowerCase() + str.slice(1);
77
+ }
78
+
79
+ async buildParams() {
80
+ // Check if we're in the right directory
81
+ if (!require("fs").existsSync(path.join(this.infrastructureDir, "repositories"))) {
82
+ throw new Error("This command must be run from within a Go Clean Architecture project root directory.");
83
+ }
84
+
85
+ const modules = this.getRepositoryFolders();
86
+
87
+ if (modules.length === 0) {
88
+ throw new Error(
89
+ "No modules found. Please create a resource first using: gcg g res <resource-name>",
90
+ );
91
+ }
92
+
93
+ const { selectedModule } = await prompts({
94
+ type: "select",
95
+ name: "selectedModule",
96
+ message: "Select a module for the service:",
97
+ choices: modules.map(module => ({
98
+ title: module,
99
+ value: module
100
+ })),
101
+ initial: 0,
102
+ onState: (state) => {
103
+ if (state.aborted) {
104
+ console.log(`${COLORS.CYAN}Service generation cancelled${COLORS.NC}`);
105
+ process.exit(0);
106
+ }
107
+ }
108
+ });
109
+
110
+ if (!selectedModule) {
111
+ throw new Error("Module selection is required");
112
+ }
113
+
114
+ const { serviceName } = await prompts({
115
+ type: "text",
116
+ name: "serviceName",
117
+ message: "Enter service name (e.g., getByEmail, activate):",
118
+ validate: this.validateServiceName.bind(this),
119
+ hint: "Use camelCase format",
120
+ onState: (state) => {
121
+ if (state.aborted) {
122
+ console.log(`${COLORS.CYAN}Service generation cancelled${COLORS.NC}`);
123
+ process.exit(0);
124
+ }
125
+ }
126
+ });
127
+
128
+ if (!serviceName) {
129
+ throw new Error("Service name is required");
130
+ }
131
+
132
+ this.moduleName = selectedModule;
133
+ this.serviceName = serviceName;
134
+ this.serviceNamePascal = this.toPascalCase(serviceName);
135
+ this.serviceNameCamel = this.toCamelCase(serviceName);
136
+ this.goModule = this.getGoModuleName();
137
+
138
+ // Get module pascal case
139
+ this.moduleNamePascal = selectedModule
140
+ .split("-")
141
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
142
+ .join("");
143
+ this.moduleNameCamel = selectedModule
144
+ .split("-")
145
+ .map((part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))
146
+ .join("");
147
+ }
148
+
149
+ async execute() {
150
+ try {
151
+ console.log(`${COLORS.CYAN}🚀 Starting service generation...${COLORS.NC}`);
152
+
153
+ await this.buildParams();
154
+
155
+ console.log(`
156
+ ${COLORS.MAGENTA}📋 Service Details:${COLORS.NC}
157
+ ${COLORS.YELLOW}Module:${COLORS.NC} ${this.moduleName}
158
+ ${COLORS.YELLOW}Service:${COLORS.NC} ${this.serviceName}
159
+ `);
160
+
161
+ // Create service action folder
162
+ await this.createServiceFolder();
163
+
164
+ // Update files
165
+ await this.updateRepositoryInterface();
166
+ await this.updateMainRepository();
167
+ await this.updateUsecase();
168
+ await this.updateController();
169
+ await this.updateRouter();
170
+
171
+ console.log(`${COLORS.GREEN}🎉 Service generation completed successfully!${COLORS.NC}`);
172
+ this.printSummary();
173
+ } catch (error) {
174
+ console.error(`${COLORS.RED}❌ Service generation failed: ${error.message}${COLORS.NC}`);
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ async createServiceFolder() {
180
+ const serviceDir = path.join(
181
+ this.infrastructureDir,
182
+ "repositories",
183
+ this.moduleName,
184
+ `${this.serviceNameCamel}${this.moduleNamePascal}`
185
+ );
186
+
187
+ await createDir(serviceDir);
188
+
189
+ // Create action.go
190
+ await createFile(
191
+ path.join(serviceDir, "action.go"),
192
+ this.getServiceActionContent()
193
+ );
194
+
195
+ // Create validation.go
196
+ await createFile(
197
+ path.join(serviceDir, "validation.go"),
198
+ this.getServiceValidationContent()
199
+ );
200
+ }
201
+
202
+ getServiceActionContent() {
203
+ return `package ${this.serviceNameCamel}${this.moduleNamePascal}
204
+
205
+ import (
206
+ "context"
207
+
208
+ "${this.goModule}/src/domain/models"
209
+ "${this.goModule}/src/infrastructure/entities"
210
+
211
+ "github.com/google/uuid"
212
+ "gorm.io/gorm"
213
+ )
214
+
215
+ // Action performs the ${this.serviceNameCamel} operation
216
+ func Action(ctx context.Context, db *gorm.DB, id uuid.UUID) (*models.${this.moduleNamePascal}Model, error) {
217
+ var entity entities.${this.moduleNamePascal}
218
+ err := db.WithContext(ctx).Where("id = ? AND is_active = ?", id, true).First(&entity).Error
219
+ if err != nil {
220
+ return nil, err
221
+ }
222
+ return entity.ToModel(), nil
223
+ }
224
+ `;
225
+ }
226
+
227
+ getServiceValidationContent() {
228
+ return `package ${this.serviceNameCamel}${this.moduleNamePascal}
229
+
230
+ import (
231
+ "errors"
232
+
233
+ "github.com/google/uuid"
234
+ )
235
+
236
+ // Validate validates the ${this.serviceNameCamel} request
237
+ func Validate(id uuid.UUID) error {
238
+ if id == uuid.Nil {
239
+ return errors.New("id is required")
240
+ }
241
+ return nil
242
+ }
243
+ `;
244
+ }
245
+
246
+ async updateRepositoryInterface() {
247
+ const filePath = path.join(this.domainDir, "repositories", `${this.moduleName}.go`);
248
+
249
+ try {
250
+ let content = await fs.readFile(filePath, "utf8");
251
+
252
+ // Check if method already exists
253
+ if (content.includes(`${this.serviceNamePascal}(`)) {
254
+ console.log(`${COLORS.YELLOW}⚠ Repository interface already contains ${this.serviceNamePascal}${COLORS.NC}`);
255
+ return;
256
+ }
257
+
258
+ // Add new method to interface
259
+ const methodSignature = `\t${this.serviceNamePascal}(ctx context.Context, id uuid.UUID) (*models.${this.moduleNamePascal}Model, error)`;
260
+
261
+ // Find the interface closing brace and add method before it
262
+ const interfaceMatch = content.match(/(type\s+\w+Repository\s+interface\s*\{[\s\S]*?)(^\})/m);
263
+ if (interfaceMatch) {
264
+ const newContent = content.replace(
265
+ interfaceMatch[0],
266
+ `${interfaceMatch[1]}${methodSignature}\n}`
267
+ );
268
+ await fs.writeFile(filePath, newContent, "utf8");
269
+ console.log(`${COLORS.GREEN}✔ Updated ${filePath}${COLORS.NC}`);
270
+ }
271
+ } catch (error) {
272
+ console.error(`${COLORS.RED}❌ Failed to update repository interface: ${error.message}${COLORS.NC}`);
273
+ }
274
+ }
275
+
276
+ async updateMainRepository() {
277
+ const filePath = path.join(this.infrastructureDir, "repositories", this.moduleName, `${this.moduleName}.repository.go`);
278
+
279
+ try {
280
+ let content = await fs.readFile(filePath, "utf8");
281
+
282
+ // Check if method already exists
283
+ if (content.includes(`func (r *Repository) ${this.serviceNamePascal}`)) {
284
+ console.log(`${COLORS.YELLOW}⚠ Repository already contains ${this.serviceNamePascal}${COLORS.NC}`);
285
+ return;
286
+ }
287
+
288
+ // Add import for the new service package
289
+ const importLine = `\t"${this.goModule}/src/infrastructure/repositories/${this.moduleName}/${this.serviceNameCamel}${this.moduleNamePascal}"`;
290
+ content = content.replace(
291
+ /import \(([\s\S]*?)\)/,
292
+ (match, imports) => `import (${imports}${importLine}\n)`
293
+ );
294
+
295
+ // Add new method implementation
296
+ const methodImpl = `
297
+ func (r *Repository) ${this.serviceNamePascal}(ctx context.Context, id uuid.UUID) (*models.${this.moduleNamePascal}Model, error) {
298
+ if err := ${this.serviceNameCamel}${this.moduleNamePascal}.Validate(id); err != nil {
299
+ return nil, err
300
+ }
301
+ return ${this.serviceNameCamel}${this.moduleNamePascal}.Action(ctx, r.db, id)
302
+ }
303
+ `;
304
+
305
+ content += methodImpl;
306
+ await fs.writeFile(filePath, content, "utf8");
307
+ console.log(`${COLORS.GREEN}✔ Updated ${filePath}${COLORS.NC}`);
308
+ } catch (error) {
309
+ console.error(`${COLORS.RED}❌ Failed to update main repository: ${error.message}${COLORS.NC}`);
310
+ }
311
+ }
312
+
313
+ async updateUsecase() {
314
+ const filePath = path.join(this.usecasesDir, this.moduleName, `${this.moduleName}.usecase.go`);
315
+
316
+ try {
317
+ let content = await fs.readFile(filePath, "utf8");
318
+
319
+ // Check if method already exists
320
+ if (content.includes(`func (u *Usecase) ${this.serviceNamePascal}`)) {
321
+ console.log(`${COLORS.YELLOW}⚠ Usecase already contains ${this.serviceNamePascal}${COLORS.NC}`);
322
+ return;
323
+ }
324
+
325
+ // Add new method implementation
326
+ const methodImpl = `
327
+ // ${this.serviceNamePascal} performs ${this.serviceNameCamel} operation
328
+ func (u *Usecase) ${this.serviceNamePascal}(ctx context.Context, id uuid.UUID) (*models.${this.moduleNamePascal}Model, error) {
329
+ model, err := u.repo.${this.serviceNamePascal}(ctx, id)
330
+ if err != nil {
331
+ u.logger.Error("${this.moduleNamePascal}Usecase", "Failed to ${this.serviceNameCamel}", err)
332
+ return nil, err
333
+ }
334
+ return model, nil
335
+ }
336
+ `;
337
+
338
+ content += methodImpl;
339
+ await fs.writeFile(filePath, content, "utf8");
340
+ console.log(`${COLORS.GREEN}✔ Updated ${filePath}${COLORS.NC}`);
341
+ } catch (error) {
342
+ console.error(`${COLORS.RED}❌ Failed to update usecase: ${error.message}${COLORS.NC}`);
343
+ }
344
+ }
345
+
346
+ async updateController() {
347
+ const filePath = path.join(this.infrastructureDir, "controllers", this.moduleName, `${this.moduleName}.controller.go`);
348
+
349
+ try {
350
+ let content = await fs.readFile(filePath, "utf8");
351
+
352
+ // Check if method already exists
353
+ if (content.includes(`func (c *Controller) ${this.serviceNamePascal}`)) {
354
+ console.log(`${COLORS.YELLOW}⚠ Controller already contains ${this.serviceNamePascal}${COLORS.NC}`);
355
+ return;
356
+ }
357
+
358
+ // Add new controller method
359
+ const controllerMethod = `
360
+ // ${this.serviceNamePascal} performs ${this.serviceNameCamel} operation
361
+ // @Summary ${this.serviceNamePascal} for ${this.moduleNamePascal}
362
+ // @Tags ${this.moduleNamePascal}
363
+ // @Produce json
364
+ // @Param id path string true "${this.moduleNamePascal} ID"
365
+ // @Success 200 {object} response.Response{data=dtos.${this.moduleNamePascal}Response}
366
+ // @Failure 400 {object} response.Response
367
+ // @Failure 404 {object} response.Response
368
+ // @Router /api/v1/${this.moduleName}s/{id}/${this.serviceNameCamel} [get]
369
+ func (c *Controller) ${this.serviceNamePascal}(ctx *gin.Context) {
370
+ id, err := uuid.Parse(ctx.Param("id"))
371
+ if err != nil {
372
+ response.Error(ctx, http.StatusBadRequest, "Invalid ID format")
373
+ return
374
+ }
375
+
376
+ model, err := c.usecase.${this.serviceNamePascal}(ctx.Request.Context(), id)
377
+ if err != nil {
378
+ if appErr, ok := utils.IsAppError(err); ok {
379
+ response.Error(ctx, appErr.Code, appErr.Message)
380
+ return
381
+ }
382
+ response.Error(ctx, http.StatusNotFound, "${this.moduleNamePascal} not found")
383
+ return
384
+ }
385
+
386
+ response.Success(ctx, http.StatusOK, modelToResponse(model))
387
+ }
388
+ `;
389
+
390
+ content += controllerMethod;
391
+ await fs.writeFile(filePath, content, "utf8");
392
+ console.log(`${COLORS.GREEN}✔ Updated ${filePath}${COLORS.NC}`);
393
+ } catch (error) {
394
+ console.error(`${COLORS.RED}❌ Failed to update controller: ${error.message}${COLORS.NC}`);
395
+ }
396
+ }
397
+
398
+ async updateRouter() {
399
+ const routerPath = path.join(this.infrastructureDir, "controllers", "router.go");
400
+
401
+ try {
402
+ let content = await fs.readFile(routerPath, "utf8");
403
+
404
+ // Check if route already exists
405
+ const routePattern = `${this.moduleName}s/{id}/${this.serviceNameCamel}`;
406
+ if (content.includes(routePattern)) {
407
+ console.log(`${COLORS.YELLOW}⚠ Router already contains route for ${this.serviceNamePascal}${COLORS.NC}`);
408
+ return;
409
+ }
410
+
411
+ // Find the module's route group and add the new route
412
+ // Look for pattern like: moduleName := v1.Group("/moduleNames")
413
+ const groupPattern = new RegExp(
414
+ `(${this.moduleName}s?\\s*:=\\s*v1\\.Group\\(["\']\/${this.moduleName}s?["\']\\)[\\s\\S]*?\\{[\\s\\S]*?)(\\})`,
415
+ 'm'
416
+ );
417
+
418
+ const match = content.match(groupPattern);
419
+ if (match) {
420
+ const newRoute = `\t\t${this.moduleName}s.GET("/:id/${this.serviceNameCamel}", ${this.moduleName}Ctrl.${this.serviceNamePascal})\n\t`;
421
+ content = content.replace(
422
+ groupPattern,
423
+ `$1${newRoute}$2`
424
+ );
425
+ await fs.writeFile(routerPath, content, "utf8");
426
+ console.log(`${COLORS.GREEN}✔ Updated ${routerPath}${COLORS.NC}`);
427
+ } else {
428
+ console.log(`${COLORS.YELLOW}⚠ Could not find ${this.moduleName} route group in router.go. Please add route manually.${COLORS.NC}`);
429
+ }
430
+ } catch (error) {
431
+ console.error(`${COLORS.YELLOW}⚠ Could not update router: ${error.message}${COLORS.NC}`);
432
+ }
433
+ }
434
+
435
+ printSummary() {
436
+ console.log(`
437
+ ${COLORS.CYAN}📁 Created files:${COLORS.NC}
438
+ ${COLORS.GREEN}✓${COLORS.NC} src/infrastructure/repositories/${this.moduleName}/${this.serviceNameCamel}${this.moduleNamePascal}/
439
+ ├── action.go
440
+ └── validation.go
441
+
442
+ ${COLORS.CYAN}📝 Updated files:${COLORS.NC}
443
+ ${COLORS.GREEN}✓${COLORS.NC} src/domain/repositories/${this.moduleName}.go
444
+ ${COLORS.GREEN}✓${COLORS.NC} src/infrastructure/repositories/${this.moduleName}/${this.moduleName}.repository.go
445
+ ${COLORS.GREEN}✓${COLORS.NC} src/usecases/${this.moduleName}/${this.moduleName}.usecase.go
446
+ ${COLORS.GREEN}✓${COLORS.NC} src/infrastructure/controllers/${this.moduleName}/${this.moduleName}.controller.go
447
+ ${COLORS.GREEN}✓${COLORS.NC} src/infrastructure/controllers/router.go
448
+
449
+ ${COLORS.YELLOW}⚠ Don't forget to:${COLORS.NC}
450
+ 1. Customize the service logic in action.go as needed
451
+ 2. Run ${COLORS.CYAN}go mod tidy${COLORS.NC} if you added new dependencies
452
+ `);
453
+ }
454
+ }
455
+
456
+ module.exports = ServiceGenerator;
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ const packageJson = require("../../../package.json");
4
+
5
+ const COLORS = {
6
+ GREEN: "\x1b[32m",
7
+ YELLOW: "\x1b[33m",
8
+ CYAN: "\x1b[36m",
9
+ NC: "\x1b[0m",
10
+ };
11
+
12
+ function displayVersion() {
13
+ console.log(`${COLORS.CYAN}╔══════════════════════════════════════════╗${COLORS.NC}`);
14
+ console.log(` ${COLORS.GREEN}Go Clean Architecture CLI${COLORS.NC}`);
15
+ console.log(` ${COLORS.YELLOW}Version: ${packageJson.version}${COLORS.NC}`);
16
+ console.log(`${COLORS.CYAN}╚══════════════════════════════════════════╝${COLORS.NC}`);
17
+ }
18
+
19
+ module.exports = { displayVersion };
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Colors for output
4
+ const GREEN = "\x1b[32m";
5
+ const YELLOW = "\x1b[33m";
6
+ const RED = "\x1b[31m";
7
+ const CYAN = "\x1b[36m";
8
+ const BLUE = "\x1b[34m";
9
+ const MAGENTA = "\x1b[35m";
10
+ const NC = "\x1b[0m";
11
+
12
+ // Header
13
+ console.log(`${CYAN}╔══════════════════════════════════════════╗${NC}`);
14
+ console.log(` ${BLUE}✨ ${GREEN}Go Clean Architecture CLI ${BLUE}✨${NC} `);
15
+ console.log(`${CYAN}╚══════════════════════════════════════════╝${NC}`);
16
+ console.log("");
17
+ console.log(
18
+ `${YELLOW}📚 This CLI follows Clean Architecture principles${NC}`
19
+ );
20
+ console.log(`${YELLOW} and helps scaffold your Go/Gin-Gonic application${NC}`);
21
+ console.log("");
22
+
23
+ // Commands section
24
+ console.log(`${MAGENTA}🌈 Available Commands:${NC}`);
25
+ console.log("");
26
+
27
+ console.log(`${BLUE}⚙️ Create New Project:${NC}`);
28
+ console.log(
29
+ ` ${GREEN}➜${NC} ${CYAN}gcg n${NC} - Initialize project structure`,
30
+ );
31
+ console.log(` ${GREEN}➜${NC} ${CYAN}gcg new${NC} `);
32
+ console.log("");
33
+
34
+ console.log(`${GREEN}🛠 Create Resources:${NC}`);
35
+ console.log(
36
+ ` ${GREEN}➜${NC} ${CYAN}gcg g res${NC} - Generate a new resource`,
37
+ );
38
+ console.log(` ${GREEN}➜${NC} ${CYAN}gcg g resource${NC}`);
39
+ console.log("");
40
+
41
+ console.log(`${BLUE}⚡ Create Services:${NC}`);
42
+ console.log(
43
+ ` ${GREEN}➜${NC} ${CYAN}gcg g s${NC} - Generate a new service`,
44
+ );
45
+ console.log(` ${GREEN}➜${NC} ${CYAN}gcg g service${NC}`);
46
+ console.log("");
47
+
48
+ console.log(`${MAGENTA}🔐 Create Authentication:${NC}`);
49
+ console.log(
50
+ ` ${GREEN}➜${NC} ${CYAN}gcg g au${NC} - Generate JWT & Passport auth`,
51
+ );
52
+ console.log(` ${GREEN}➜${NC} ${CYAN}gcg g auth${NC}`);
53
+ console.log("");
54
+
55
+ console.log(`${YELLOW}🔒 Add Auth to Resource:${NC}`);
56
+ console.log(
57
+ ` ${GREEN}➜${NC} ${CYAN}gcg add au${NC} - Add auth guard to a resource`,
58
+ );
59
+ console.log(` ${GREEN}➜${NC} ${CYAN}gcg add auth${NC}`);
60
+ console.log("");
61
+
62
+ console.log(`${RED}🗑 Remove Resources:${NC}`);
63
+ console.log(
64
+ ` ${GREEN}➜${NC} ${CYAN}gcg rm res${NC} - Remove an existing resource`,
65
+ );
66
+ console.log(` ${GREEN}➜${NC} ${CYAN}gcg rm resource${NC}`);
67
+ console.log("");
68
+
69
+ console.log(`${YELLOW}ℹ️ Other Commands:${NC}`);
70
+ console.log(
71
+ ` ${GREEN}➜${NC} ${CYAN}gcg --help${NC} - Show help message`,
72
+ );
73
+ console.log(
74
+ ` ${GREEN}➜${NC} ${CYAN}gcg --version${NC} - Display version`,
75
+ );
76
+ console.log("");
77
+
78
+ // Tech Stack
79
+ console.log(`${MAGENTA}🔧 Tech Stack:${NC}`);
80
+ console.log(` ${GREEN}➜${NC} Framework: ${CYAN}Gin-Gonic${NC}`);
81
+ console.log(` ${GREEN}➜${NC} ORM: ${CYAN}GORM${NC}`);
82
+ console.log(` ${GREEN}➜${NC} Database: ${CYAN}PostgreSQL${NC}`);
83
+ console.log(` ${GREEN}➜${NC} API: ${CYAN}RESTful${NC}`);
84
+ console.log(` ${GREEN}➜${NC} Logging: ${CYAN}Zap${NC}`);
85
+ console.log(` ${GREEN}➜${NC} Config: ${CYAN}Viper${NC}`);
86
+ console.log("");
87
+
88
+ // Footer
89
+ console.log(`${CYAN}╔══════════════════════════════════════════╗${NC}`);
90
+ console.log(
91
+ `${BLUE}🔹 ${GREEN}Happy Coding with Clean Architecture! ${BLUE} 🔹${NC}`
92
+ );
93
+ console.log(`${CYAN}╚══════════════════════════════════════════╝${NC}`);