polyforge-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/commands/create.js +46 -0
  4. package/dist/commands/doctor.js +136 -0
  5. package/dist/commands/list.js +32 -0
  6. package/dist/core/git.js +25 -0
  7. package/dist/core/installer.js +64 -0
  8. package/dist/core/prompts.js +147 -0
  9. package/dist/core/renderer.js +175 -0
  10. package/dist/core/validator.js +86 -0
  11. package/dist/index.js +47 -0
  12. package/dist/templates/backend/go-gin/apps/api/Makefile +10 -0
  13. package/dist/templates/backend/go-gin/apps/api/cmd/server/main.go +29 -0
  14. package/dist/templates/backend/go-gin/apps/api/go.mod +5 -0
  15. package/dist/templates/backend/go-gin/apps/api/internal/config/config.go +22 -0
  16. package/dist/templates/backend/go-gin/apps/api/internal/handler/health.go +27 -0
  17. package/dist/templates/backend/go-gin/apps/api/internal/handler/helpers.go +8 -0
  18. package/dist/templates/backend/go-gin/apps/api/internal/handler/ping.go +15 -0
  19. package/dist/templates/backend/go-gin/apps/api/internal/middleware/logger.go +18 -0
  20. package/dist/templates/backend/go-gin/apps/api/internal/middleware/recovery.go +23 -0
  21. package/dist/templates/backend/go-gin/apps/api/internal/middleware/trace.go +28 -0
  22. package/dist/templates/backend/go-gin/apps/api/internal/repository/ping.go +11 -0
  23. package/dist/templates/backend/go-gin/apps/api/internal/service/ping.go +17 -0
  24. package/dist/templates/backend/go-gin/apps/api/pkg/response/response.go +35 -0
  25. package/dist/templates/backend/go-gin/apps/api/pkg/response/response_test.go +21 -0
  26. package/dist/templates/backend/springboot/apps/api/pom.xml +44 -0
  27. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/Application.java +11 -0
  28. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/common/ApiResponse.java +19 -0
  29. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/common/GlobalExceptionHandler.java +22 -0
  30. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/common/TraceIdHolder.java +19 -0
  31. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/config/TraceIdFilter.java +38 -0
  32. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/controller/HealthController.java +17 -0
  33. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/controller/PingController.java +23 -0
  34. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/repository/PingRepository.java +10 -0
  35. package/dist/templates/backend/springboot/apps/api/src/main/java/com/scaffold/api/service/PingService.java +17 -0
  36. package/dist/templates/backend/springboot/apps/api/src/main/resources/application-dev.yml +2 -0
  37. package/dist/templates/backend/springboot/apps/api/src/main/resources/application-prod.yml +2 -0
  38. package/dist/templates/backend/springboot/apps/api/src/main/resources/application-test.yml +2 -0
  39. package/dist/templates/backend/springboot/apps/api/src/main/resources/application.yml +8 -0
  40. package/dist/templates/backend/springboot/apps/api/src/test/java/com/scaffold/api/ApplicationTests.java +12 -0
  41. package/dist/templates/base/.env.example +5 -0
  42. package/dist/templates/base/docs/ARCHITECTURE.md +9 -0
  43. package/dist/templates/base/infra/scripts/build.sh +39 -0
  44. package/dist/templates/base/infra/scripts/check.sh +58 -0
  45. package/dist/templates/base/infra/scripts/dev.sh +32 -0
  46. package/dist/templates/base/infra/scripts/test.sh +41 -0
  47. package/dist/templates/base/package.json +11 -0
  48. package/dist/templates/data/mongodb/infra/data/mongodb/README.md +4 -0
  49. package/dist/templates/data/mysql/infra/data/mysql/README.md +4 -0
  50. package/dist/templates/data/mysql/infra/data/mysql/schema.sql +5 -0
  51. package/dist/templates/data/postgresql/infra/data/postgresql/README.md +4 -0
  52. package/dist/templates/data/postgresql/infra/data/postgresql/schema.sql +5 -0
  53. package/dist/templates/data/redis/infra/data/redis/README.md +4 -0
  54. package/dist/templates/data/sqlite/infra/data/sqlite/README.md +4 -0
  55. package/dist/templates/frontend/react/apps/web/.env.example +4 -0
  56. package/dist/templates/frontend/react/apps/web/index.html +12 -0
  57. package/dist/templates/frontend/react/apps/web/package.json +23 -0
  58. package/dist/templates/frontend/react/apps/web/src/App.test.tsx +7 -0
  59. package/dist/templates/frontend/react/apps/web/src/App.tsx +59 -0
  60. package/dist/templates/frontend/react/apps/web/src/api/client.ts +20 -0
  61. package/dist/templates/frontend/react/apps/web/src/api/request.ts +104 -0
  62. package/dist/templates/frontend/react/apps/web/src/api/services/api.ts +15 -0
  63. package/dist/templates/frontend/react/apps/web/src/api/services/bff.ts +11 -0
  64. package/dist/templates/frontend/react/apps/web/src/main.tsx +9 -0
  65. package/dist/templates/frontend/react/apps/web/tsconfig.json +12 -0
  66. package/dist/templates/frontend/react/apps/web/vite.config.ts +22 -0
  67. package/dist/templates/frontend/vue/apps/web/.env.example +4 -0
  68. package/dist/templates/frontend/vue/apps/web/index.html +12 -0
  69. package/dist/templates/frontend/vue/apps/web/package.json +20 -0
  70. package/dist/templates/frontend/vue/apps/web/src/App.test.ts +7 -0
  71. package/dist/templates/frontend/vue/apps/web/src/App.vue +59 -0
  72. package/dist/templates/frontend/vue/apps/web/src/api/client.ts +20 -0
  73. package/dist/templates/frontend/vue/apps/web/src/api/request.ts +104 -0
  74. package/dist/templates/frontend/vue/apps/web/src/api/services/api.ts +15 -0
  75. package/dist/templates/frontend/vue/apps/web/src/api/services/bff.ts +11 -0
  76. package/dist/templates/frontend/vue/apps/web/src/env.d.ts +3 -0
  77. package/dist/templates/frontend/vue/apps/web/src/main.ts +4 -0
  78. package/dist/templates/frontend/vue/apps/web/tsconfig.json +11 -0
  79. package/dist/templates/frontend/vue/apps/web/vite.config.ts +22 -0
  80. package/dist/templates/modules/auth-center/apps/auth-center/.env.example +3 -0
  81. package/dist/templates/modules/auth-center/apps/auth-center/README.md +5 -0
  82. package/dist/templates/modules/auth-center/apps/auth-center/package.json +14 -0
  83. package/dist/templates/modules/auth-center/apps/auth-center/server.js +21 -0
  84. package/dist/templates/modules/cache-redis/infra/cache/README.md +5 -0
  85. package/dist/templates/modules/cache-redis/infra/cache/policies.md +5 -0
  86. package/dist/templates/modules/gateway-bff/apps/gateway-bff/README.md +5 -0
  87. package/dist/templates/modules/gateway-bff/apps/gateway-bff/package.json +13 -0
  88. package/dist/templates/modules/gateway-bff/apps/gateway-bff/server.js +17 -0
  89. package/dist/templates/modules/grpc-service/apps/grpc-service/README.md +5 -0
  90. package/dist/templates/modules/grpc-service/contracts/proto/greeter.proto +17 -0
  91. package/dist/templates/modules/grpc-service/infra/scripts/gen-proto.sh +9 -0
  92. package/dist/templates/modules/mq/infra/mq/README.md +5 -0
  93. package/dist/templates/modules/mq/infra/mq/kafka-topics.md +5 -0
  94. package/dist/templates/modules/mq/infra/mq/nats-subjects.md +4 -0
  95. package/dist/templates/modules/mq/infra/mq/rabbitmq-exchanges.md +5 -0
  96. package/dist/templates/modules/observability/infra/observability/README.md +5 -0
  97. package/dist/templates/modules/observability/infra/observability/grafana/README.md +3 -0
  98. package/dist/templates/modules/observability/infra/observability/otel-collector.yaml +15 -0
  99. package/dist/templates/modules/observability/infra/observability/prometheus/prometheus.yml +7 -0
  100. package/dist/templates/modules/python-ai/apps/python-ai/README.md +5 -0
  101. package/dist/templates/modules/python-ai/apps/python-ai/app/main.py +27 -0
  102. package/dist/templates/modules/python-ai/apps/python-ai/requirements.txt +2 -0
  103. package/dist/templates/modules/python-ai/apps/python-ai/scripts/batch_infer.py +6 -0
  104. package/dist/templates/modules/python-worker/apps/worker-python/README.md +5 -0
  105. package/dist/templates/modules/python-worker/apps/worker-python/requirements.txt +1 -0
  106. package/dist/templates/modules/python-worker/apps/worker-python/tasks/sample_task.py +6 -0
  107. package/dist/templates/modules/python-worker/apps/worker-python/tests/test_worker.py +13 -0
  108. package/dist/templates/modules/python-worker/apps/worker-python/worker.py +10 -0
  109. package/dist/templates/modules/worker-go/apps/worker-go/README.md +5 -0
  110. package/dist/templates/modules/worker-go/apps/worker-go/cmd/worker/main.go +16 -0
  111. package/dist/templates/modules/worker-go/apps/worker-go/go.mod +3 -0
  112. package/dist/templates/modules/worker-go/apps/worker-go/internal/tasks/heartbeat.go +7 -0
  113. package/dist/types/config.js +18 -0
  114. package/package.json +52 -0
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderProject = renderProject;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const TEXT_EXT = new Set([
10
+ ".ts",
11
+ ".tsx",
12
+ ".js",
13
+ ".jsx",
14
+ ".vue",
15
+ ".json",
16
+ ".md",
17
+ ".txt",
18
+ ".go",
19
+ ".mod",
20
+ ".sum",
21
+ ".java",
22
+ ".yml",
23
+ ".yaml",
24
+ ".xml",
25
+ ".properties",
26
+ ".env",
27
+ ".gitignore",
28
+ ".sh",
29
+ ".py",
30
+ ".toml",
31
+ ".cfg",
32
+ ".ini",
33
+ ".sql",
34
+ ".html",
35
+ ".css",
36
+ ]);
37
+ function templateRoot() {
38
+ return path_1.default.resolve(__dirname, "..", "templates");
39
+ }
40
+ function replacements(config) {
41
+ return {
42
+ PROJECT_NAME: config.projectName,
43
+ BACKEND_MAIN: config.backendMain,
44
+ FRONTEND: config.frontend,
45
+ DATA_MODULES: config.dataModules.join(", "),
46
+ EXTRA_MODULES: config.extraModules.join(", ") || "none",
47
+ HAS_GATEWAY_BFF: String(config.extraModules.includes("gateway-bff")),
48
+ };
49
+ }
50
+ async function renderTextFile(file, map) {
51
+ const ext = path_1.default.extname(file);
52
+ const base = path_1.default.basename(file);
53
+ if (!TEXT_EXT.has(ext) && !TEXT_EXT.has(base))
54
+ return;
55
+ const content = await fs_extra_1.default.readFile(file, "utf8");
56
+ const rendered = content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_m, key) => map[key] ?? "");
57
+ if (content !== rendered) {
58
+ await fs_extra_1.default.writeFile(file, rendered, "utf8");
59
+ }
60
+ }
61
+ async function renderDir(target, map) {
62
+ const entries = await fs_extra_1.default.readdir(target, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ const full = path_1.default.join(target, entry.name);
65
+ if (entry.isDirectory()) {
66
+ await renderDir(full, map);
67
+ }
68
+ else {
69
+ await renderTextFile(full, map);
70
+ }
71
+ }
72
+ }
73
+ async function mergeTemplate(templateDir, targetDir, map) {
74
+ const source = path_1.default.join(templateRoot(), templateDir);
75
+ if (!(await fs_extra_1.default.pathExists(source)))
76
+ return;
77
+ await fs_extra_1.default.copy(source, targetDir, { overwrite: true });
78
+ await renderDir(targetDir, map);
79
+ }
80
+ async function ensureExecutableScripts(targetDir) {
81
+ const scripts = [
82
+ path_1.default.join(targetDir, "infra", "scripts", "dev.sh"),
83
+ path_1.default.join(targetDir, "infra", "scripts", "build.sh"),
84
+ path_1.default.join(targetDir, "infra", "scripts", "test.sh"),
85
+ path_1.default.join(targetDir, "infra", "scripts", "check.sh"),
86
+ ];
87
+ for (const script of scripts) {
88
+ if (await fs_extra_1.default.pathExists(script)) {
89
+ await fs_extra_1.default.chmod(script, 0o755);
90
+ }
91
+ }
92
+ }
93
+ function dockerServices(config) {
94
+ const services = [];
95
+ if (config.dataModules.includes("mysql")) {
96
+ services.push(` mysql:\n image: mysql:8\n environment:\n MYSQL_ROOT_PASSWORD: root\n MYSQL_DATABASE: app\n ports:\n - \"3306:3306\"`);
97
+ }
98
+ if (config.dataModules.includes("postgresql")) {
99
+ services.push(` postgresql:\n image: postgres:16\n environment:\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: app\n ports:\n - \"5432:5432\"`);
100
+ }
101
+ if (config.dataModules.includes("redis")) {
102
+ services.push(` redis:\n image: redis:7\n ports:\n - \"6379:6379\"`);
103
+ }
104
+ if (config.dataModules.includes("mongodb")) {
105
+ services.push(` mongodb:\n image: mongo:7\n ports:\n - \"27017:27017\"`);
106
+ }
107
+ if (config.extraModules.includes("python-worker")) {
108
+ services.push(` worker-python:\n build:\n context: ../apps/worker-python\n command: python worker.py`);
109
+ }
110
+ if (config.extraModules.includes("worker-go")) {
111
+ services.push(` worker-go:\n build:\n context: ../apps/worker-go\n command: go run ./cmd/worker`);
112
+ }
113
+ if (config.extraModules.includes("gateway-bff")) {
114
+ services.push(` gateway-bff:\n build:\n context: ../apps/gateway-bff\n command: node server.js\n ports:\n - "3001:3001"`);
115
+ }
116
+ if (config.extraModules.includes("python-ai")) {
117
+ services.push(` python-ai:\n build:\n context: ../apps/python-ai\n command: uvicorn app.main:app --host 0.0.0.0 --port 8090\n ports:\n - "8090:8090"`);
118
+ }
119
+ if (config.extraModules.includes("auth-center")) {
120
+ services.push(` auth-center:\n image: node:20-alpine\n working_dir: /app\n volumes:\n - ../apps/auth-center:/app\n command: node server.js\n ports:\n - "8081:8081"`);
121
+ }
122
+ return services.join("\n\n");
123
+ }
124
+ async function writeGeneratedReadme(config) {
125
+ const readme = `# ${config.projectName}\n\nGenerated by scaffold-hybrid-cli.\n\n## Stack\n- Frontend: ${config.frontend}\n- Backend: ${config.backendMain}\n- Extra modules: ${config.extraModules.join(", ") || "none"}\n- Data modules: ${config.dataModules.join(", ")}\n\n## Quick Start\n\n\`\`\`bash\n# basic sanity checks\nnpm run check\n\n# start local dev flow\nnpm run dev\n\n# build\nnpm run build\n\n# test\nnpm run test\n\`\`\`\n\nHealth endpoint: \`/health\`\n`;
126
+ await fs_extra_1.default.writeFile(path_1.default.join(config.targetDir, "README.md"), readme, "utf8");
127
+ }
128
+ async function updateGitignoreForSqlite(config) {
129
+ if (!config.dataModules.includes("sqlite"))
130
+ return;
131
+ const gitignorePath = path_1.default.join(config.targetDir, ".gitignore");
132
+ const exists = await fs_extra_1.default.pathExists(gitignorePath);
133
+ const marker = "data/*.db";
134
+ if (!exists) {
135
+ await fs_extra_1.default.writeFile(gitignorePath, `${marker}\n`, "utf8");
136
+ return;
137
+ }
138
+ const content = await fs_extra_1.default.readFile(gitignorePath, "utf8");
139
+ if (!content.includes(marker)) {
140
+ await fs_extra_1.default.writeFile(gitignorePath, `${content.trimEnd()}\n${marker}\n`, "utf8");
141
+ }
142
+ }
143
+ async function maybeWriteDocker(config) {
144
+ if (!config.docker)
145
+ return;
146
+ const dockerDir = path_1.default.join(config.targetDir, "infra", "docker");
147
+ await fs_extra_1.default.ensureDir(dockerDir);
148
+ const compose = `services:\n${dockerServices(config) || " # add services here"}\n`;
149
+ await fs_extra_1.default.writeFile(path_1.default.join(dockerDir, "docker-compose.yml"), compose, "utf8");
150
+ }
151
+ async function renderProject(config) {
152
+ const target = config.targetDir;
153
+ if (await fs_extra_1.default.pathExists(target)) {
154
+ throw new Error(`target directory already exists: ${target}`);
155
+ }
156
+ await fs_extra_1.default.ensureDir(target);
157
+ const map = replacements(config);
158
+ await mergeTemplate("base", target, map);
159
+ await mergeTemplate(path_1.default.join("backend", config.backendMain), target, map);
160
+ if (config.frontend !== "none") {
161
+ await mergeTemplate(path_1.default.join("frontend", config.frontend), target, map);
162
+ }
163
+ for (const module of config.extraModules) {
164
+ await mergeTemplate(path_1.default.join("modules", module), target, map);
165
+ }
166
+ for (const data of config.dataModules) {
167
+ if (data === "none")
168
+ continue;
169
+ await mergeTemplate(path_1.default.join("data", data), target, map);
170
+ }
171
+ await maybeWriteDocker(config);
172
+ await updateGitignoreForSqlite(config);
173
+ await writeGeneratedReadme(config);
174
+ await ensureExecutableScripts(target);
175
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULTS = void 0;
4
+ exports.parseCsv = parseCsv;
5
+ exports.unique = unique;
6
+ exports.parseDataModules = parseDataModules;
7
+ exports.parseExtraModules = parseExtraModules;
8
+ exports.validateConfig = validateConfig;
9
+ const config_1 = require("../types/config");
10
+ exports.DEFAULTS = {
11
+ frontend: "react",
12
+ backendMain: "go-gin",
13
+ extraModules: [],
14
+ dataModules: ["none"],
15
+ packageManager: "npm",
16
+ installDeps: false,
17
+ initGit: false,
18
+ docker: false,
19
+ };
20
+ function parseCsv(input) {
21
+ if (!input)
22
+ return [];
23
+ return input
24
+ .split(",")
25
+ .map((x) => x.trim())
26
+ .filter(Boolean);
27
+ }
28
+ function unique(arr) {
29
+ return [...new Set(arr)];
30
+ }
31
+ function parseDataModules(input) {
32
+ const parsed = parseCsv(input);
33
+ return parsed.filter((x) => config_1.DATA_MODULES.includes(x));
34
+ }
35
+ function parseExtraModules(input) {
36
+ const parsed = parseCsv(input);
37
+ return parsed.filter((x) => config_1.EXTRA_MODULES.includes(x));
38
+ }
39
+ function validateConfig(config) {
40
+ const errors = [];
41
+ const warnings = [];
42
+ if (!config.projectName.trim()) {
43
+ errors.push("projectName is required");
44
+ }
45
+ if (!config_1.FRONTENDS.includes(config.frontend)) {
46
+ errors.push(`unsupported frontend: ${config.frontend}`);
47
+ }
48
+ if (!config_1.BACKENDS.includes(config.backendMain)) {
49
+ errors.push(`unsupported backend: ${config.backendMain}`);
50
+ }
51
+ if (!config_1.PACKAGE_MANAGERS.includes(config.packageManager)) {
52
+ errors.push(`unsupported package manager: ${config.packageManager}`);
53
+ }
54
+ const uniqueData = unique(config.dataModules);
55
+ if (uniqueData.length !== config.dataModules.length) {
56
+ errors.push("data modules contain duplicates");
57
+ }
58
+ const uniqueModules = unique(config.extraModules);
59
+ if (uniqueModules.length !== config.extraModules.length) {
60
+ errors.push("extra modules contain duplicates");
61
+ }
62
+ const hasNone = config.dataModules.includes("none");
63
+ if (hasNone && config.dataModules.length > 1) {
64
+ errors.push("data=none cannot be combined with other data modules");
65
+ }
66
+ if (config.backendMain === "springboot" && config.dataModules.includes("sqlite")) {
67
+ warnings.push("springboot + sqlite is better for lightweight local scenarios");
68
+ }
69
+ if (config.extraModules.includes("python-worker")) {
70
+ warnings.push("python-worker selected: ensure Python 3.10+ and venv tooling are available");
71
+ }
72
+ if (config.extraModules.includes("python-ai")) {
73
+ warnings.push("python-ai selected: ensure Python 3.10+ and model runtime dependencies are available");
74
+ }
75
+ if (config.extraModules.includes("grpc-service")) {
76
+ warnings.push("grpc-service selected: install protoc and language plugins for code generation");
77
+ }
78
+ if (config.extraModules.includes("mq")) {
79
+ warnings.push("mq selected: verify local broker runtime (Kafka/RabbitMQ/NATS) for integration testing");
80
+ }
81
+ return {
82
+ valid: errors.length === 0,
83
+ errors,
84
+ warnings,
85
+ };
86
+ }
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const create_1 = require("./commands/create");
6
+ const doctor_1 = require("./commands/doctor");
7
+ const list_1 = require("./commands/list");
8
+ const program = new commander_1.Command();
9
+ program.name("scaffold").description("Hybrid full-stack scaffold CLI").version("0.1.0");
10
+ program
11
+ .command("create")
12
+ .argument("<project-name>", "project name")
13
+ .option("--frontend <frontend>", "react|vue|none")
14
+ .option("--backend <backend>", "go-gin|springboot")
15
+ .option("--modules <modules>", "comma separated modules: python-worker,worker-go,gateway-bff,python-ai,grpc-service,mq,cache-redis,observability,auth-center")
16
+ .option("--data <data>", "comma separated data modules")
17
+ .option("--pm <pm>", "pnpm|npm|yarn")
18
+ .option("--install", "install dependencies")
19
+ .option("--git", "initialize git")
20
+ .option("--docker", "generate docker baseline")
21
+ .option("--yes", "use defaults for missing options")
22
+ .action(async (projectName, options) => {
23
+ try {
24
+ await (0, create_1.runCreate)(projectName, options);
25
+ }
26
+ catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ console.error(message);
29
+ process.exit(1);
30
+ }
31
+ });
32
+ program
33
+ .command("doctor")
34
+ .description("check local toolchain")
35
+ .option("--backend <backend>", "target backend for focused diagnostics: go-gin|springboot")
36
+ .option("--modules <modules>", "target extra modules, e.g. python-worker")
37
+ .option("--data <data>", "target data modules, e.g. mysql,redis")
38
+ .action(async (options) => {
39
+ await (0, doctor_1.runDoctor)(options);
40
+ });
41
+ program
42
+ .command("list")
43
+ .description("list supported templates/modules and defaults")
44
+ .action(async () => {
45
+ await (0, list_1.runList)();
46
+ });
47
+ program.parseAsync(process.argv);
@@ -0,0 +1,10 @@
1
+ .PHONY: dev build test
2
+
3
+ dev:
4
+ go run ./cmd/server
5
+
6
+ build:
7
+ go build ./...
8
+
9
+ test:
10
+ go test ./...
@@ -0,0 +1,29 @@
1
+ package main
2
+
3
+ import (
4
+ "log"
5
+
6
+ "github.com/gin-gonic/gin"
7
+
8
+ "{{PROJECT_NAME}}/apps/api/internal/config"
9
+ "{{PROJECT_NAME}}/apps/api/internal/handler"
10
+ "{{PROJECT_NAME}}/apps/api/internal/middleware"
11
+ )
12
+
13
+ func main() {
14
+ cfg := config.Load()
15
+
16
+ r := gin.New()
17
+ r.Use(middleware.TraceID())
18
+ r.Use(middleware.RequestLogger())
19
+ r.Use(middleware.Recover())
20
+
21
+ h := handler.New()
22
+ r.GET("/health", h.Health)
23
+ r.GET("/api/v1/ping", h.Ping)
24
+
25
+ log.Printf("api listening on :%s", cfg.Port)
26
+ if err := r.Run(":" + cfg.Port); err != nil {
27
+ log.Fatal(err)
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ module {{PROJECT_NAME}}/apps/api
2
+
3
+ go 1.22
4
+
5
+ require github.com/gin-gonic/gin v1.10.1
@@ -0,0 +1,22 @@
1
+ package config
2
+
3
+ import "os"
4
+
5
+ type Config struct {
6
+ Port string
7
+ Env string
8
+ }
9
+
10
+ func Load() Config {
11
+ return Config{
12
+ Port: getEnv("APP_PORT", "8080"),
13
+ Env: getEnv("APP_ENV", "dev"),
14
+ }
15
+ }
16
+
17
+ func getEnv(key, fallback string) string {
18
+ if v := os.Getenv(key); v != "" {
19
+ return v
20
+ }
21
+ return fallback
22
+ }
@@ -0,0 +1,27 @@
1
+ package handler
2
+
3
+ import (
4
+ "net/http"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ "{{PROJECT_NAME}}/apps/api/internal/middleware"
8
+ "{{PROJECT_NAME}}/apps/api/internal/repository"
9
+ "{{PROJECT_NAME}}/apps/api/internal/service"
10
+ "{{PROJECT_NAME}}/apps/api/pkg/response"
11
+ )
12
+
13
+ type Handler struct {
14
+ pingService *service.PingService
15
+ }
16
+
17
+ func New() *Handler {
18
+ repo := repository.NewPingRepository()
19
+ return &Handler{
20
+ pingService: service.NewPingService(repo),
21
+ }
22
+ }
23
+
24
+ func (h *Handler) Health(c *gin.Context) {
25
+ traceID, _ := c.Get(middleware.TraceIDKey)
26
+ response.Success(c, http.StatusOK, gin.H{"status": "ok"}, traceIDString(traceID))
27
+ }
@@ -0,0 +1,8 @@
1
+ package handler
2
+
3
+ func traceIDString(v any) string {
4
+ if s, ok := v.(string); ok {
5
+ return s
6
+ }
7
+ return ""
8
+ }
@@ -0,0 +1,15 @@
1
+ package handler
2
+
3
+ import (
4
+ "net/http"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ "{{PROJECT_NAME}}/apps/api/internal/middleware"
8
+ "{{PROJECT_NAME}}/apps/api/pkg/response"
9
+ )
10
+
11
+ func (h *Handler) Ping(c *gin.Context) {
12
+ traceID, _ := c.Get(middleware.TraceIDKey)
13
+ result := h.pingService.Ping()
14
+ response.Success(c, http.StatusOK, gin.H{"message": result}, traceIDString(traceID))
15
+ }
@@ -0,0 +1,18 @@
1
+ package middleware
2
+
3
+ import (
4
+ "log"
5
+ "time"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ func RequestLogger() gin.HandlerFunc {
11
+ return func(c *gin.Context) {
12
+ start := time.Now()
13
+ c.Next()
14
+ latency := time.Since(start)
15
+ traceID, _ := c.Get(TraceIDKey)
16
+ log.Printf("traceId=%v method=%s path=%s status=%d latency=%s", traceID, c.Request.Method, c.Request.URL.Path, c.Writer.Status(), latency)
17
+ }
18
+ }
@@ -0,0 +1,23 @@
1
+ package middleware
2
+
3
+ import (
4
+ "net/http"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ "{{PROJECT_NAME}}/apps/api/pkg/response"
8
+ )
9
+
10
+ func Recover() gin.HandlerFunc {
11
+ return gin.CustomRecovery(func(c *gin.Context, recovered any) {
12
+ traceID, _ := c.Get(TraceIDKey)
13
+ response.Error(c, http.StatusInternalServerError, 10000, "internal server error", traceIDString(traceID))
14
+ c.Abort()
15
+ })
16
+ }
17
+
18
+ func traceIDString(v any) string {
19
+ if s, ok := v.(string); ok {
20
+ return s
21
+ }
22
+ return ""
23
+ }
@@ -0,0 +1,28 @@
1
+ package middleware
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ const TraceIDKey = "traceId"
11
+
12
+ func TraceID() gin.HandlerFunc {
13
+ return func(c *gin.Context) {
14
+ traceID := c.GetHeader("X-Trace-Id")
15
+ if traceID == "" {
16
+ traceID = generateTraceID()
17
+ }
18
+ c.Set(TraceIDKey, traceID)
19
+ c.Writer.Header().Set("X-Trace-Id", traceID)
20
+ c.Next()
21
+ }
22
+ }
23
+
24
+ func generateTraceID() string {
25
+ b := make([]byte, 8)
26
+ _, _ = rand.Read(b)
27
+ return hex.EncodeToString(b)
28
+ }
@@ -0,0 +1,11 @@
1
+ package repository
2
+
3
+ type PingRepo struct{}
4
+
5
+ func NewPingRepository() *PingRepo {
6
+ return &PingRepo{}
7
+ }
8
+
9
+ func (r *PingRepo) Message() string {
10
+ return "pong"
11
+ }
@@ -0,0 +1,17 @@
1
+ package service
2
+
3
+ type PingRepository interface {
4
+ Message() string
5
+ }
6
+
7
+ type PingService struct {
8
+ repo PingRepository
9
+ }
10
+
11
+ func NewPingService(repo PingRepository) *PingService {
12
+ return &PingService{repo: repo}
13
+ }
14
+
15
+ func (s *PingService) Ping() string {
16
+ return s.repo.Message()
17
+ }
@@ -0,0 +1,35 @@
1
+ package response
2
+
3
+ import (
4
+ "time"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ type Payload struct {
10
+ Code int `json:"code"`
11
+ Message string `json:"message"`
12
+ Data interface{} `json:"data"`
13
+ TraceID string `json:"traceId"`
14
+ Timestamp string `json:"timestamp"`
15
+ }
16
+
17
+ func Success(c *gin.Context, status int, data interface{}, traceID string) {
18
+ c.JSON(status, Payload{
19
+ Code: 0,
20
+ Message: "success",
21
+ Data: data,
22
+ TraceID: traceID,
23
+ Timestamp: time.Now().UTC().Format(time.RFC3339),
24
+ })
25
+ }
26
+
27
+ func Error(c *gin.Context, status int, code int, message string, traceID string) {
28
+ c.JSON(status, Payload{
29
+ Code: code,
30
+ Message: message,
31
+ Data: nil,
32
+ TraceID: traceID,
33
+ Timestamp: time.Now().UTC().Format(time.RFC3339),
34
+ })
35
+ }
@@ -0,0 +1,21 @@
1
+ package response
2
+
3
+ import (
4
+ "net/http"
5
+ "net/http/httptest"
6
+ "testing"
7
+
8
+ "github.com/gin-gonic/gin"
9
+ )
10
+
11
+ func TestSuccess(t *testing.T) {
12
+ gin.SetMode(gin.TestMode)
13
+ w := httptest.NewRecorder()
14
+ c, _ := gin.CreateTestContext(w)
15
+
16
+ Success(c, http.StatusOK, gin.H{"x": 1}, "tid")
17
+
18
+ if w.Code != http.StatusOK {
19
+ t.Fatalf("unexpected status: %d", w.Code)
20
+ }
21
+ }
@@ -0,0 +1,44 @@
1
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
3
+ <modelVersion>4.0.0</modelVersion>
4
+ <parent>
5
+ <groupId>org.springframework.boot</groupId>
6
+ <artifactId>spring-boot-starter-parent</artifactId>
7
+ <version>3.3.5</version>
8
+ <relativePath/>
9
+ </parent>
10
+
11
+ <groupId>com.scaffold</groupId>
12
+ <artifactId>api</artifactId>
13
+ <version>0.0.1-SNAPSHOT</version>
14
+ <name>{{PROJECT_NAME}}-api</name>
15
+ <description>Spring Boot API generated by scaffold</description>
16
+ <properties>
17
+ <java.version>17</java.version>
18
+ </properties>
19
+
20
+ <dependencies>
21
+ <dependency>
22
+ <groupId>org.springframework.boot</groupId>
23
+ <artifactId>spring-boot-starter-web</artifactId>
24
+ </dependency>
25
+ <dependency>
26
+ <groupId>org.springframework.boot</groupId>
27
+ <artifactId>spring-boot-starter-actuator</artifactId>
28
+ </dependency>
29
+ <dependency>
30
+ <groupId>org.springframework.boot</groupId>
31
+ <artifactId>spring-boot-starter-test</artifactId>
32
+ <scope>test</scope>
33
+ </dependency>
34
+ </dependencies>
35
+
36
+ <build>
37
+ <plugins>
38
+ <plugin>
39
+ <groupId>org.springframework.boot</groupId>
40
+ <artifactId>spring-boot-maven-plugin</artifactId>
41
+ </plugin>
42
+ </plugins>
43
+ </build>
44
+ </project>
@@ -0,0 +1,11 @@
1
+ package com.scaffold.api;
2
+
3
+ import org.springframework.boot.SpringApplication;
4
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+
6
+ @SpringBootApplication
7
+ public class Application {
8
+ public static void main(String[] args) {
9
+ SpringApplication.run(Application.class, args);
10
+ }
11
+ }
@@ -0,0 +1,19 @@
1
+ package com.scaffold.api.common;
2
+
3
+ import java.time.Instant;
4
+
5
+ public record ApiResponse<T>(
6
+ int code,
7
+ String message,
8
+ T data,
9
+ String traceId,
10
+ String timestamp
11
+ ) {
12
+ public static <T> ApiResponse<T> success(T data, String traceId) {
13
+ return new ApiResponse<>(0, "success", data, traceId, Instant.now().toString());
14
+ }
15
+
16
+ public static <T> ApiResponse<T> error(int code, String message, String traceId) {
17
+ return new ApiResponse<>(code, message, null, traceId, Instant.now().toString());
18
+ }
19
+ }