create-nexgen 1.0.4

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 (240) hide show
  1. package/package.json +26 -0
  2. package/src/index.js +108 -0
  3. package/template/.dockerignore +14 -0
  4. package/template/.env +58 -0
  5. package/template/.env.example +59 -0
  6. package/template/.prettierignore +5 -0
  7. package/template/.prettierrc +8 -0
  8. package/template/README.md +447 -0
  9. package/template/drizzle.config.ts +29 -0
  10. package/template/eslint.config.js +52 -0
  11. package/template/gitignore-stub +24 -0
  12. package/template/package.json +96 -0
  13. package/template/public/assets/AuthLayout-CbswhpjJ.js +1 -0
  14. package/template/public/assets/Button-_7aQ7gHL.js +1 -0
  15. package/template/public/assets/Input-CLNJXmKc.css +1 -0
  16. package/template/public/assets/Input-z8GI8Aqo.js +1 -0
  17. package/template/public/assets/InputPasswordToggle-BxlzVGp3.js +1 -0
  18. package/template/public/assets/InputPasswordToggle-C77FI9Eg.css +1 -0
  19. package/template/public/assets/Layout-DotR1sQC.js +1 -0
  20. package/template/public/assets/Refresh-BdqsPPBC.js +1 -0
  21. package/template/public/assets/admin-ui-CU34rLdN.js +1 -0
  22. package/template/public/assets/bootstrap-icons-BeopsB42.woff +0 -0
  23. package/template/public/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  24. package/template/public/assets/dashboard-CwybEyLc.js +1 -0
  25. package/template/public/assets/dashboard-Dc4d-Pi7.css +1 -0
  26. package/template/public/assets/forgetPassword-CKEJaXsq.js +1 -0
  27. package/template/public/assets/index-Bleyx5dm.js +64 -0
  28. package/template/public/assets/index-DUw8E6Yg.css +1 -0
  29. package/template/public/assets/login-DC7PTlQF.js +1 -0
  30. package/template/public/assets/realtime-test-BPQdrFym.css +1 -0
  31. package/template/public/assets/realtime-test-tQZ0rBEJ.js +1 -0
  32. package/template/public/assets/register-3O7Qs28C.js +1 -0
  33. package/template/public/assets/resetPassword-A5AzMWKs.js +1 -0
  34. package/template/public/assets/verifyEmail-DDBEQHOv.js +1 -0
  35. package/template/public/index.html +17 -0
  36. package/template/src/database/migrations/mysql/0000_init.sql +73 -0
  37. package/template/src/database/migrations/mysql/meta/0000_snapshot.json +484 -0
  38. package/template/src/database/migrations/mysql/meta/_journal.json +13 -0
  39. package/template/src/database/schema.ts +4 -0
  40. package/template/src/env.ts +107 -0
  41. package/template/src/framework/cache/cache.ts +81 -0
  42. package/template/src/framework/database/connection.ts +168 -0
  43. package/template/src/framework/database/optional-db-drivers.d.ts +9 -0
  44. package/template/src/framework/database/paginate.ts +200 -0
  45. package/template/src/framework/database/schema.ts +26 -0
  46. package/template/src/framework/database/seed.ts +33 -0
  47. package/template/src/framework/events/dispatcher.ts +57 -0
  48. package/template/src/framework/facade.ts +27 -0
  49. package/template/src/framework/http/app.ts +61 -0
  50. package/template/src/framework/http/cors.ts +19 -0
  51. package/template/src/framework/http/logger.ts +85 -0
  52. package/template/src/framework/http/openapi.ts +34 -0
  53. package/template/src/framework/http/ratelimiter.ts +13 -0
  54. package/template/src/framework/http/router.ts +76 -0
  55. package/template/src/framework/http/static.ts +33 -0
  56. package/template/src/framework/http/validation.ts +24 -0
  57. package/template/src/framework/kernel.ts +40 -0
  58. package/template/src/framework/maker-cli/src/index.mjs +51 -0
  59. package/template/src/framework/maker-cli/src/levels/level-1/env-db.mjs +57 -0
  60. package/template/src/framework/maker-cli/src/levels/level-1/file-ops.mjs +30 -0
  61. package/template/src/framework/maker-cli/src/levels/level-1/flags.mjs +16 -0
  62. package/template/src/framework/maker-cli/src/levels/level-1/help.mjs +24 -0
  63. package/template/src/framework/maker-cli/src/levels/level-1/naming.mjs +13 -0
  64. package/template/src/framework/maker-cli/src/levels/level-1/process.mjs +47 -0
  65. package/template/src/framework/maker-cli/src/levels/level-2/db/core.mjs +299 -0
  66. package/template/src/framework/maker-cli/src/levels/level-2/db/index.mjs +177 -0
  67. package/template/src/framework/maker-cli/src/levels/level-2/deploy/core.mjs +635 -0
  68. package/template/src/framework/maker-cli/src/levels/level-2/deploy/index.mjs +145 -0
  69. package/template/src/framework/maker-cli/src/levels/level-2/module/core.mjs +707 -0
  70. package/template/src/framework/maker-cli/src/levels/level-2/module/index.mjs +116 -0
  71. package/template/src/framework/maker-cli/src/levels/level-2/runtime/build-frontend.mjs +16 -0
  72. package/template/src/framework/maker-cli/src/levels/level-2/runtime/core.mjs +311 -0
  73. package/template/src/framework/maker-cli/src/levels/level-2/runtime/index.mjs +71 -0
  74. package/template/src/framework/maker-cli/stubs/controller/openapi.ts.stub +55 -0
  75. package/template/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +56 -0
  76. package/template/src/framework/maker-cli/stubs/controller/plain.ts.stub +57 -0
  77. package/template/src/framework/maker-cli/stubs/controller/schema.plain.ts.stub +13 -0
  78. package/template/src/framework/maker-cli/stubs/controller/schema.ts.stub +32 -0
  79. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +49 -0
  80. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +53 -0
  81. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +49 -0
  82. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +53 -0
  83. package/template/src/framework/maker-cli/stubs/deploy/README.stub +55 -0
  84. package/template/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +29 -0
  85. package/template/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +29 -0
  86. package/template/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +29 -0
  87. package/template/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +73 -0
  88. package/template/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +73 -0
  89. package/template/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +72 -0
  90. package/template/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +15 -0
  91. package/template/src/framework/maker-cli/stubs/deploy/server/README.stub +77 -0
  92. package/template/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +118 -0
  93. package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +131 -0
  94. package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +129 -0
  95. package/template/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +10 -0
  96. package/template/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +24 -0
  97. package/template/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +24 -0
  98. package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +15 -0
  99. package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +12 -0
  100. package/template/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +13 -0
  101. package/template/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +6 -0
  102. package/template/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +53 -0
  103. package/template/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +69 -0
  104. package/template/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +24 -0
  105. package/template/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +20 -0
  106. package/template/src/framework/maker-cli/stubs/example/console.ts.stub +33 -0
  107. package/template/src/framework/maker-cli/stubs/example/controller.ts.stub +503 -0
  108. package/template/src/framework/maker-cli/stubs/example/job.ts.stub +74 -0
  109. package/template/src/framework/maker-cli/stubs/example/route.api.ts.stub +206 -0
  110. package/template/src/framework/maker-cli/stubs/example/schema.ts.stub +41 -0
  111. package/template/src/framework/maker-cli/stubs/job/name.ts.stub +24 -0
  112. package/template/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +8 -0
  113. package/template/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +8 -0
  114. package/template/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +8 -0
  115. package/template/src/framework/maker-cli/stubs/notification/NotificationBell.vue.stub +218 -0
  116. package/template/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -0
  117. package/template/src/framework/maker-cli/stubs/notification/index.vue.stub +211 -0
  118. package/template/src/framework/maker-cli/stubs/notification/job.ts.stub +12 -0
  119. package/template/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -0
  120. package/template/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -0
  121. package/template/src/framework/maker-cli/stubs/route/api.ts.stub +79 -0
  122. package/template/src/framework/maker-cli/stubs/route/plain.ts.stub +10 -0
  123. package/template/src/framework/maker-cli/stubs/schedule/name.ts.stub +35 -0
  124. package/template/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -0
  125. package/template/src/framework/modules/discover.ts +54 -0
  126. package/template/src/framework/modules/routes.ts +26 -0
  127. package/template/src/framework/notification/index.ts +109 -0
  128. package/template/src/framework/queue/clear.ts +20 -0
  129. package/template/src/framework/queue/queue.ts +213 -0
  130. package/template/src/framework/queue/ui.ts +104 -0
  131. package/template/src/framework/queue/worker.ts +33 -0
  132. package/template/src/framework/realtime/broadcast.ts +27 -0
  133. package/template/src/framework/realtime/index.ts +1 -0
  134. package/template/src/framework/realtime/socket-cookie.ts +65 -0
  135. package/template/src/framework/realtime/socket.ts +132 -0
  136. package/template/src/framework/realtime/types.ts +6 -0
  137. package/template/src/framework/realtime/ui.ts +16 -0
  138. package/template/src/framework/redis/client.ts +126 -0
  139. package/template/src/framework/scheduler/lock.ts +124 -0
  140. package/template/src/framework/scheduler/run.ts +26 -0
  141. package/template/src/framework/scheduler/scheduler.ts +82 -0
  142. package/template/src/framework/server.ts +147 -0
  143. package/template/src/framework/session/session.ts +116 -0
  144. package/template/src/framework/storage/storage.ts +743 -0
  145. package/template/src/framework/support/cookie.ts +78 -0
  146. package/template/src/framework/support/jwt.ts +45 -0
  147. package/template/src/framework/support/lifecycle.ts +35 -0
  148. package/template/src/framework/support/logger.ts +102 -0
  149. package/template/src/framework/support/mail.ts +43 -0
  150. package/template/src/framework/support/password.ts +23 -0
  151. package/template/src/framework/support/url.ts +25 -0
  152. package/template/src/middlewares/auth-middleware.ts +98 -0
  153. package/template/src/middlewares/role-middleware.ts +24 -0
  154. package/template/src/modules/auth/controllers/auth.controller.ts +445 -0
  155. package/template/src/modules/auth/controllers/auth.helpers.ts +110 -0
  156. package/template/src/modules/auth/controllers/auth.schema.ts +102 -0
  157. package/template/src/modules/auth/controllers/role.controller.ts +25 -0
  158. package/template/src/modules/auth/database/models/notifications.ts +22 -0
  159. package/template/src/modules/auth/database/models/role.ts +14 -0
  160. package/template/src/modules/auth/database/models/user.ts +46 -0
  161. package/template/src/modules/auth/database/seeders/role.ts +19 -0
  162. package/template/src/modules/auth/database/seeders/user.ts +33 -0
  163. package/template/src/modules/auth/jobs/forgetpass.ts +18 -0
  164. package/template/src/modules/auth/jobs/registeruser.ts +31 -0
  165. package/template/src/modules/auth/jobs/verifyemail.ts +18 -0
  166. package/template/src/modules/auth/routes/api.ts +151 -0
  167. package/template/src/modules/auth/routes/role.ts +39 -0
  168. package/template/src/modules/welcome/controllers/welcome.controller.ts +14 -0
  169. package/template/src/modules/welcome/controllers/welcome.schema.ts +6 -0
  170. package/template/src/modules/welcome/database/models/welcome.ts +6 -0
  171. package/template/src/modules/welcome/routes/api.ts +20 -0
  172. package/template/src/resources/index.html +16 -0
  173. package/template/src/resources/src/App.vue +5 -0
  174. package/template/src/resources/src/assets/css/styles.css +14934 -0
  175. package/template/src/resources/src/assets/css/styles.css.map +1 -0
  176. package/template/src/resources/src/assets/images/favicon/favicon.ico +0 -0
  177. package/template/src/resources/src/assets/images/favicon/favicon1.ico +0 -0
  178. package/template/src/resources/src/assets/images/logo-1.png +0 -0
  179. package/template/src/resources/src/assets/images/logo-dark-sm.png +0 -0
  180. package/template/src/resources/src/assets/images/logo-dark.png +0 -0
  181. package/template/src/resources/src/assets/images/logo-dark1.png +0 -0
  182. package/template/src/resources/src/assets/images/logo-sm.png +0 -0
  183. package/template/src/resources/src/assets/images/logo1.png +0 -0
  184. package/template/src/resources/src/assets/images/logo2.png +0 -0
  185. package/template/src/resources/src/assets/scss/custom.css +217 -0
  186. package/template/src/resources/src/assets/scss/custom.css.map +1 -0
  187. package/template/src/resources/src/assets/scss/custom.scss +1100 -0
  188. package/template/src/resources/src/components/Button.vue +35 -0
  189. package/template/src/resources/src/components/Checkbox.vue +29 -0
  190. package/template/src/resources/src/components/FloatButton.vue +36 -0
  191. package/template/src/resources/src/components/Href.vue +32 -0
  192. package/template/src/resources/src/components/Input.vue +227 -0
  193. package/template/src/resources/src/components/InputGroup.vue +153 -0
  194. package/template/src/resources/src/components/InputPasswordToggle.vue +226 -0
  195. package/template/src/resources/src/components/Modal.vue +102 -0
  196. package/template/src/resources/src/components/Pagebar.vue +28 -0
  197. package/template/src/resources/src/components/Refresh.vue +26 -0
  198. package/template/src/resources/src/components/Select.vue +390 -0
  199. package/template/src/resources/src/components/Spinner.vue +42 -0
  200. package/template/src/resources/src/components/Switch.vue +65 -0
  201. package/template/src/resources/src/components/TextArea.vue +121 -0
  202. package/template/src/resources/src/components/Toast.vue +56 -0
  203. package/template/src/resources/src/components/datatable/DataTableSkeleton.vue +99 -0
  204. package/template/src/resources/src/components/datatable/Pagination.vue +161 -0
  205. package/template/src/resources/src/components/datatable/SelectOpption.vue +54 -0
  206. package/template/src/resources/src/components/datatable/index.vue +237 -0
  207. package/template/src/resources/src/composables/useAuth.ts +52 -0
  208. package/template/src/resources/src/composables/useBrowserDetect.ts +5 -0
  209. package/template/src/resources/src/composables/useDialog.ts +5 -0
  210. package/template/src/resources/src/composables/useGum.ts +3 -0
  211. package/template/src/resources/src/composables/usePulse.ts +5 -0
  212. package/template/src/resources/src/env.d.ts +20 -0
  213. package/template/src/resources/src/helpers/nformatter.ts +10 -0
  214. package/template/src/resources/src/helpers/utils.ts +68 -0
  215. package/template/src/resources/src/layouts/AuthLayout.vue +20 -0
  216. package/template/src/resources/src/layouts/Layout/Footer.vue +23 -0
  217. package/template/src/resources/src/layouts/Layout/Header.vue +90 -0
  218. package/template/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
  219. package/template/src/resources/src/layouts/Layout/index.vue +76 -0
  220. package/template/src/resources/src/main.ts +27 -0
  221. package/template/src/resources/src/pages/auth/forgetPassword.vue +76 -0
  222. package/template/src/resources/src/pages/auth/login.vue +93 -0
  223. package/template/src/resources/src/pages/auth/register.vue +130 -0
  224. package/template/src/resources/src/pages/auth/resetPassword.vue +119 -0
  225. package/template/src/resources/src/pages/auth/verifyEmail.vue +60 -0
  226. package/template/src/resources/src/pages/dashboard/index.vue +76 -0
  227. package/template/src/resources/src/plugins/axios.ts +33 -0
  228. package/template/src/resources/src/plugins/browserDetect.ts +55 -0
  229. package/template/src/resources/src/plugins/dialog.ts +167 -0
  230. package/template/src/resources/src/plugins/gum.ts +343 -0
  231. package/template/src/resources/src/plugins/pulse.ts +141 -0
  232. package/template/src/resources/src/plugins/routeProgress.ts +87 -0
  233. package/template/src/resources/src/router/index.ts +85 -0
  234. package/template/src/resources/src/stores/admin-ui.ts +148 -0
  235. package/template/src/resources/src/stores/auth.ts +151 -0
  236. package/template/src/resources/tsconfig.json +19 -0
  237. package/template/src/resources/vite.config.ts +43 -0
  238. package/template/src/storage/logs/app.log +20179 -0
  239. package/template/src/storage/logs/fatal.log +727 -0
  240. package/template/tsconfig.json +20 -0
@@ -0,0 +1,635 @@
1
+ import os from "node:os";
2
+ import { spawn } from "node:child_process";
3
+ import fsSync from "node:fs";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { detectDeployDatabase, redisEnabled } from "../../level-1/env-db.mjs";
7
+ import { getOption, hasFlag, stripWorkflowFlags } from "../../level-1/flags.mjs";
8
+ import { writeFileAlways, writeFileIfMissing } from "../../level-1/file-ops.mjs";
9
+ import { runCommand } from "../../level-1/process.mjs";
10
+
11
+ /** Expand ~ to the user's home directory in a file path. */
12
+ function expandHome(inputPath = "") {
13
+ if (!inputPath || !inputPath.startsWith("~")) return inputPath;
14
+ return `${os.homedir()}${inputPath.slice(1)}`;
15
+ }
16
+
17
+ const stubsRoot = path.resolve(import.meta.dirname, "../../../..", "stubs");
18
+
19
+ /** Template stubs used for deploy file generation. */
20
+ const STUBS = {
21
+ deploy: {
22
+ dockerfile: {
23
+ node: {
24
+ npm: "deploy/Dockerfile.stub",
25
+ pnpm: "deploy/Dockerfile.pnpm.stub",
26
+ yarn: "deploy/Dockerfile.yarn.stub"
27
+ },
28
+ bun: "deploy/Dockerfile.bun.stub"
29
+ },
30
+ composeByDatabase: {
31
+ mysql: "deploy/compose/mysql.server.stub",
32
+ postgres: "deploy/compose/postgres.server.stub",
33
+ sqlite: "deploy/compose/sqlite.stub"
34
+ },
35
+ envByDatabase: {
36
+ mysql: "deploy/env/mysql.server.stub",
37
+ postgres: "deploy/env/postgres.server.stub",
38
+ sqlite: "deploy/env/sqlite.stub"
39
+ },
40
+ readme: "deploy/README.stub",
41
+ supervisor: {
42
+ redis: "deploy/supervisor/redis.stub",
43
+ noredis: "deploy/supervisor/noredis.stub"
44
+ },
45
+ autoMigrateScript: "deploy/scripts/auto-migrate.sh.stub",
46
+ server: {
47
+ compose: {
48
+ redis: "deploy/server/compose/redis.stub",
49
+ redisDev: "deploy/server/compose/redis.dev.stub",
50
+ noredis: "deploy/server/compose/noredis.stub"
51
+ },
52
+ env: {
53
+ redis: "deploy/server/env/redis.stub",
54
+ noredis: "deploy/server/env/noredis.stub",
55
+ localExample: "deploy/server/env/local.example.stub"
56
+ },
57
+ pgadminServers: "deploy/server/pgadmin/servers.stub",
58
+ readme: "deploy/server/README.stub",
59
+ nginxReadme: "deploy/server/nginx-vhost/README.stub",
60
+ nginxHost: "deploy/server/nginx-vhost/app.example.com.stub",
61
+ redisConfig: "deploy/server/redis/redis.conf.stub"
62
+ },
63
+ workflow: {
64
+ local: "deploy/workflow/local.json.stub",
65
+ remote: "deploy/workflow/remote.json.stub"
66
+ }
67
+ },
68
+ controller: {
69
+ openapi: "controller/openapi.ts.stub",
70
+ openapiWithModel: "controller/openapi.with-model.ts.stub",
71
+ plain: "controller/plain.ts.stub",
72
+ schema: {
73
+ openapi: "controller/schema.ts.stub",
74
+ plain: "controller/schema.plain.ts.stub"
75
+ }
76
+ },
77
+ route: {
78
+ api: "route/api.ts.stub",
79
+ plain: "route/plain.ts.stub"
80
+ },
81
+ model: {
82
+ named: {
83
+ mysql: "model/name.mysql.ts.stub",
84
+ postgresql: "model/name.postgresql.ts.stub",
85
+ sqlite: "model/name.sqlite.ts.stub"
86
+ }
87
+ },
88
+ example: {
89
+ schema: "example/schema.ts.stub",
90
+ controller: "example/controller.ts.stub",
91
+ routeApi: "example/route.api.ts.stub",
92
+ job: "example/job.ts.stub",
93
+ console: "example/console.ts.stub"
94
+ },
95
+ seeder: { named: "seeder/name.ts.stub" },
96
+ job: { named: "job/name.ts.stub" },
97
+ schedule: { named: "schedule/name.ts.stub" }
98
+ };
99
+
100
+ /** Read a stub file from the stubs directory. */
101
+ async function readStubRaw(name) {
102
+ const file = path.join(stubsRoot, name);
103
+ return fs.readFile(file, "utf8");
104
+ }
105
+
106
+ /** Read a stub file and replace {{key}} placeholders with the given values. */
107
+ async function renderStub(name, values = {}) {
108
+ let content = await readStubRaw(name);
109
+ for (const [key, value] of Object.entries(values)) content = content.replaceAll(`{{${key}}}`, String(value));
110
+ return content.replace(/\{\{[A-Z0-9_]+\}\}/g, "");
111
+ }
112
+
113
+ /** Pick the redis or noredis stub based on the redisEnabled flag. */
114
+ function pickRedisStub(redisOn, redisStub, noRedisStub) {
115
+ return redisOn ? redisStub : noRedisStub;
116
+ }
117
+
118
+ /** Render multiple stubs and write them to the filesystem. */
119
+ async function writeRenderedFiles(root, entries = []) {
120
+ for (const entry of entries) {
121
+ const content = await renderStub(entry.stub, entry.values || {});
122
+ await writeFileAlways(path.join(root, entry.output), content);
123
+ }
124
+ }
125
+
126
+ /** Read a specific key from a .env file. Returns empty string if not found. */
127
+ async function readEnvValue(envPath, key) {
128
+ let raw = "";
129
+ try {
130
+ raw = await fs.readFile(envPath, "utf8");
131
+ } catch {
132
+ return "";
133
+ }
134
+ for (const line of raw.split(/\r?\n/)) {
135
+ const clean = line.trim();
136
+ if (!clean || clean.startsWith("#")) continue;
137
+ const idx = clean.indexOf("=");
138
+ if (idx <= 0) continue;
139
+ if (clean.slice(0, idx).trim() !== key) continue;
140
+ return clean.slice(idx + 1).trim().replace(/^['"]|['"]$/g, "");
141
+ }
142
+ return "";
143
+ }
144
+
145
+ /** Ensure a .env file exists, copying from .example if missing. */
146
+ async function ensureEnvFile(envPath, examplePath) {
147
+ try {
148
+ await fs.access(envPath);
149
+ } catch {
150
+ try {
151
+ await fs.copyFile(examplePath, envPath);
152
+ console.log(`Created ${path.relative(process.cwd(), envPath)} from ${path.relative(process.cwd(), examplePath)}.`);
153
+ } catch {
154
+ throw new Error(`Missing env file: ${path.relative(process.cwd(), envPath)}. Create it first.`);
155
+ }
156
+ }
157
+ }
158
+
159
+ /** Ensure a Docker network exists, creating it if missing. */
160
+ async function ensureDockerNetwork(name) {
161
+ try {
162
+ await runCommand("docker", ["network", "inspect", name], { silent: true });
163
+ } catch {
164
+ await runCommand("docker", ["network", "create", name]);
165
+ }
166
+ }
167
+
168
+ /** Replace the database name in a connection URL with a given name. */
169
+ function updateDatabaseNameInUrl(rawUrl, nextDbName) {
170
+ try {
171
+ const url = new URL(rawUrl);
172
+ if (!nextDbName) return rawUrl;
173
+ url.pathname = `/${nextDbName}`;
174
+ return url.toString();
175
+ } catch {
176
+ return rawUrl;
177
+ }
178
+ }
179
+
180
+ /** Replace ${variable} placeholders in a string with provided values. */
181
+ function expandEnvTemplate(value, vars = {}) {
182
+ let out = String(value || "");
183
+ for (const [key, val] of Object.entries(vars)) out = out.replaceAll(`\${${key}}`, String(val ?? ""));
184
+ return out;
185
+ }
186
+
187
+ /** Ensure a bind-mount source file exists as a file (not a directory), creating from stub if needed. */
188
+ async function ensureBindMountFile(filePath, stubName) {
189
+ let stat;
190
+ try { stat = await fs.stat(filePath); } catch { /* doesn't exist */ }
191
+
192
+ if (stat) {
193
+ if (stat.isFile()) return;
194
+ if (stat.isDirectory()) {
195
+ console.log(`Fixing: ${path.relative(process.cwd(), filePath)} was a directory, replacing with file.`);
196
+ await fs.rm(filePath, { recursive: true, force: true });
197
+ }
198
+ }
199
+
200
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
201
+ const content = await renderStub(stubName);
202
+ await fs.writeFile(filePath, content, "utf8");
203
+ }
204
+
205
+ /** Parse remote workflow config file. */
206
+ async function readRemoteConfig(configPath) {
207
+ const resolved = path.resolve(process.cwd(), configPath);
208
+ const parsed = JSON.parse(await fs.readFile(resolved, "utf8"));
209
+ const remote = parsed.remote || {};
210
+ return {
211
+ host: String(remote.host || "").trim(),
212
+ user: String(remote.user || "").trim(),
213
+ port: String(remote.port || 22).trim(),
214
+ keyPath: expandHome(String(remote.keyPath || "").trim()),
215
+ targetPath: String(remote.targetPath || "").trim()
216
+ };
217
+ }
218
+
219
+ /** Build SSH base args from config. */
220
+ function buildSshArgs(config) {
221
+ const sshArgs = ["-p", config.port];
222
+ if (config.keyPath) sshArgs.push("-i", config.keyPath);
223
+ return { sshArgs, remoteHost: `${config.user}@${config.host}` };
224
+ }
225
+
226
+ /** Sync pgAdmin servers.json into the running pgAdmin container. */
227
+ async function syncPgAdminServers(deployRoot, envPath, localEnvPath) {
228
+ const serversPath = path.join(deployRoot, "server", "pgadmin", "servers.json");
229
+ try { await fs.access(serversPath); } catch { return; }
230
+ const email = (await readEnvValue(localEnvPath, "PGADMIN_DEFAULT_EMAIL")) || (await readEnvValue(envPath, "PGADMIN_DEFAULT_EMAIL"));
231
+ if (!email) return;
232
+ try {
233
+ await runCommand("docker", ["exec", "pgadmin", "sh", "-lc", `/venv/bin/python /pgadmin4/setup.py load-servers /pgadmin4/servers.json --user \"${email.replace(/"/g, '\\"')}\" --replace`]);
234
+ console.log(`pgAdmin servers synced for ${email}.`);
235
+ } catch {
236
+ console.log("Warning: could not auto-sync pgAdmin servers.");
237
+ }
238
+ }
239
+
240
+ /** Update DATABASE_URL in the deploy env to match the server's actual database name. */
241
+ async function syncDeployDatabaseUrlWithServerEnv(deployEnvPath, serverEnvPath) {
242
+ const appDatabaseUrlRaw = await readEnvValue(deployEnvPath, "DATABASE_URL");
243
+ if (!appDatabaseUrlRaw) return;
244
+ const appDatabaseUrl = decodeURIComponent(appDatabaseUrlRaw);
245
+ const serverLocalEnvPath = `${serverEnvPath}.local`;
246
+ const readServerValue = async (key) => (await readEnvValue(serverLocalEnvPath, key)) || (await readEnvValue(serverEnvPath, key));
247
+ const expandedDatabaseUrl = expandEnvTemplate(appDatabaseUrl, {
248
+ MYSQL_ROOT_PASSWORD: await readServerValue("MYSQL_ROOT_PASSWORD"),
249
+ MYSQL_DATABASE: await readServerValue("MYSQL_DATABASE"),
250
+ POSTGRES_USER: await readServerValue("POSTGRES_USER"),
251
+ POSTGRES_PASSWORD: await readServerValue("POSTGRES_PASSWORD"),
252
+ POSTGRES_DB: await readServerValue("POSTGRES_DB")
253
+ });
254
+ const lower = appDatabaseUrl.toLowerCase();
255
+ const serverDbName = lower.startsWith("mysql") ? await readServerValue("MYSQL_DATABASE") : (lower.startsWith("postgres") || lower.startsWith("postgresql")) ? await readServerValue("POSTGRES_DB") : "";
256
+ if (!serverDbName) return;
257
+ const nextUrl = updateDatabaseNameInUrl(expandedDatabaseUrl, serverDbName);
258
+ if (nextUrl === appDatabaseUrl) return;
259
+ const content = await fs.readFile(deployEnvPath, "utf8");
260
+ const updated = content.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${nextUrl}`);
261
+ if (updated !== content) await fs.writeFile(deployEnvPath, updated);
262
+ }
263
+
264
+ /** Ensure the target database exists inside the running MySQL container. */
265
+ async function ensureDeployDatabaseReady(deployEnvPath, serverEnvPath) {
266
+ const rawUrl = await readEnvValue(deployEnvPath, "DATABASE_URL");
267
+ if (!rawUrl) return;
268
+ const databaseUrl = decodeURIComponent(rawUrl);
269
+ const lower = databaseUrl.toLowerCase();
270
+ const serverLocalEnvPath = `${serverEnvPath}.local`;
271
+ const readServerValue = async (key) => (await readEnvValue(serverLocalEnvPath, key)) || (await readEnvValue(serverEnvPath, key));
272
+ if (lower.startsWith("mysql") || lower.startsWith("mariadb")) {
273
+ const password = await readServerValue("MYSQL_ROOT_PASSWORD");
274
+ if (!password) return;
275
+ const dbName = (() => { try { const u = new URL(databaseUrl); return decodeURIComponent((u.pathname || "").replace(/^\/+/, "")); } catch { return ""; } })();
276
+ if (!dbName) return;
277
+ await runCommand("docker", ["exec", "mysql-global", "mysql", "-uroot", `-p${password}`, "-e", `CREATE DATABASE IF NOT EXISTS \`${dbName.replace(/`/g, "``")}\`;`]);
278
+ return;
279
+ }
280
+ }
281
+
282
+ /** Generate deploy scaffolding files (Dockerfile, compose, env, etc.). */
283
+ export async function createDeploy(flags = []) {
284
+ const deployRoot = path.resolve(process.cwd(), "deploy");
285
+ const force = hasFlag(flags, "--force");
286
+ const appOnly = hasFlag(flags, "--app-only");
287
+ const serverOnly = hasFlag(flags, "--server-only");
288
+ const isDev = hasFlag(flags, "--dev");
289
+ const doApp = !serverOnly;
290
+ const doServer = !appOnly;
291
+ const runtimeFlag = flags.find((flag) => flag.startsWith("--runtime="));
292
+ const runtime = runtimeFlag ? runtimeFlag.split("=")[1].trim().toLowerCase() : "node";
293
+ const pmFlag = flags.find((flag) => flag.startsWith("--pm="));
294
+ const nodePm = pmFlag ? pmFlag.split("=")[1].trim().toLowerCase() : "npm";
295
+ const database = detectDeployDatabase(process.env.DATABASE_URL || await readEnvValue(path.resolve(process.cwd(), ".env"), "DATABASE_URL"));
296
+ let redisOn = redisEnabled();
297
+ if (!redisOn && process.env.REDIS === undefined) {
298
+ const rootRedis = (await readEnvValue(path.resolve(process.cwd(), ".env"), "REDIS")).toLowerCase();
299
+ redisOn = rootRedis === "true" || rootRedis === "1" || rootRedis === "yes";
300
+ }
301
+ if (!force) { try { await fs.access(deployRoot); throw new Error("deploy folder already exists. Re-run with --force"); } catch { } }
302
+ const runtimeExec = runtime === "bun" ? "bun" : "node";
303
+ const nodeDockerStub = STUBS.deploy.dockerfile.node[nodePm] || STUBS.deploy.dockerfile.node.npm;
304
+ const composeStub = STUBS.deploy.composeByDatabase[database] || STUBS.deploy.composeByDatabase.sqlite;
305
+ const envStub = STUBS.deploy.envByDatabase[database] || STUBS.deploy.envByDatabase.sqlite;
306
+ if (doApp) {
307
+ await writeRenderedFiles(deployRoot, [
308
+ {
309
+ output: "Dockerfile",
310
+ stub: runtime === "bun" ? STUBS.deploy.dockerfile.bun : nodeDockerStub
311
+ },
312
+ {
313
+ output: "docker-compose.yml",
314
+ stub: composeStub,
315
+ values: { REDIS_ENV_LINE: redisOn ? " REDIS_URL: redis://redis-global:6379\n" : "" }
316
+ },
317
+ {
318
+ output: ".env.example",
319
+ stub: envStub,
320
+ values: {
321
+ REDIS_ENABLED: redisOn ? "true" : "false",
322
+ FRONTEND: process.env.FRONTEND !== "false" ? "true" : "false",
323
+ SOCKET: process.env.SOCKET !== "false" ? "true" : "false",
324
+ OPEN_API: process.env.OPEN_API || "true",
325
+ LOG_LEVEL: process.env.LOG_LEVEL || "info",
326
+ CORS_ORIGIN: process.env.CORS_ORIGIN || "*",
327
+ COOKIE_NAME: process.env.COOKIE_NAME || "nexgen",
328
+ SESSION_COOKIE: process.env.SESSION_COOKIE || "nexgen_session",
329
+ SESSION_TTL_SECONDS: process.env.SESSION_TTL_SECONDS || "7200",
330
+ CACHE_TTL_SECONDS: process.env.CACHE_TTL_SECONDS || "3600",
331
+ STORAGE_DRIVER: process.env.STORAGE_DRIVER || "local",
332
+ STORAGE_DISK: process.env.STORAGE_DISK || "public",
333
+ AUTH_REQUIRE_EMAIL_VERIFICATION: process.env.AUTH_REQUIRE_EMAIL_VERIFICATION || "false",
334
+ MAIL_FAIL_SILENT: "true",
335
+ REDIS_PREFIX: process.env.REDIS_PREFIX || "nexgen",
336
+ JWT_ACCESS_SECRET: process.env.JWT_ACCESS_SECRET || (await readEnvValue(path.resolve(process.cwd(), ".env"), "JWT_ACCESS_SECRET")) || "",
337
+ JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || (await readEnvValue(path.resolve(process.cwd(), ".env"), "JWT_REFRESH_SECRET")) || "",
338
+ COOKIE_SECRET: process.env.COOKIE_SECRET || (await readEnvValue(path.resolve(process.cwd(), ".env"), "COOKIE_SECRET")) || ""
339
+ }
340
+ },
341
+ {
342
+ output: "README.md",
343
+ stub: STUBS.deploy.readme,
344
+ values: { DATABASE_MODE: database, REDIS_STATUS: redisOn ? "yes" : "no", RUNTIME_NAME: runtime }
345
+ },
346
+ {
347
+ output: "supervisor/supervisord.conf",
348
+ stub: pickRedisStub(redisOn, STUBS.deploy.supervisor.redis, STUBS.deploy.supervisor.noredis),
349
+ values: { RUNTIME_EXEC: runtimeExec, RUNTIME_NAME: runtime }
350
+ },
351
+ {
352
+ output: "scripts/auto-migrate.sh",
353
+ stub: STUBS.deploy.autoMigrateScript
354
+ }
355
+ ]);
356
+ }
357
+ if (doServer) {
358
+ await writeRenderedFiles(deployRoot, [
359
+ {
360
+ output: "server/docker-compose.yml",
361
+ stub: pickRedisStub(redisOn, isDev ? STUBS.deploy.server.compose.redisDev : STUBS.deploy.server.compose.redis, STUBS.deploy.server.compose.noredis)
362
+ },
363
+ {
364
+ output: "server/.env.example",
365
+ stub: pickRedisStub(redisOn, STUBS.deploy.server.env.redis, STUBS.deploy.server.env.noredis)
366
+ },
367
+ {
368
+ output: "server/.env.local.example",
369
+ stub: STUBS.deploy.server.env.localExample
370
+ },
371
+ {
372
+ output: "server/pgadmin/servers.json",
373
+ stub: STUBS.deploy.server.pgadminServers
374
+ },
375
+ {
376
+ output: "server/README.md",
377
+ stub: STUBS.deploy.server.readme
378
+ },
379
+ {
380
+ output: "server/nginx-vhost/README.md",
381
+ stub: STUBS.deploy.server.nginxHost
382
+ },
383
+ {
384
+ output: "server/nginx-vhost/app.example.com",
385
+ stub: STUBS.deploy.server.nginxHost
386
+ },
387
+ ...(isDev ? [] : [
388
+ { output: "workflow.local.json", stub: STUBS.deploy.workflow.local },
389
+ { output: "workflow.remote.json", stub: STUBS.deploy.workflow.remote }
390
+ ])
391
+ ]);
392
+ if (redisOn) await writeRenderedFiles(deployRoot, [{ output: "server/redis/redis.conf", stub: STUBS.deploy.server.redisConfig }]);
393
+ }
394
+ if (!doServer) {
395
+ try { await fs.rm(path.join(deployRoot, "server"), { recursive: true, force: true }); } catch {}
396
+ }
397
+ }
398
+
399
+ /** Start the shared server infra (proxy, db, redis) or the app stack via Docker Compose. */
400
+ export async function runDeploy(commandName) {
401
+ const deployRoot = path.resolve(process.cwd(), "deploy");
402
+ if (commandName === "deploy:server") {
403
+ const composePath = path.join(deployRoot, "server", "docker-compose.yml");
404
+ const envPath = path.join(deployRoot, "server", ".env");
405
+ const localEnvPath = path.join(deployRoot, "server", ".env.local");
406
+ const localEnvExamplePath = path.join(deployRoot, "server", ".env.local.example");
407
+ const envExamplePath = path.join(deployRoot, "server", ".env.example");
408
+ try {
409
+ await fs.access(composePath);
410
+ } catch {
411
+ console.log("Generating deploy/server/ files from stubs...");
412
+ let redisOn = redisEnabled();
413
+ if (!redisOn && process.env.REDIS === undefined) {
414
+ const rootRedis = (await readEnvValue(path.resolve(process.cwd(), ".env"), "REDIS")).toLowerCase();
415
+ redisOn = rootRedis === "true" || rootRedis === "1" || rootRedis === "yes";
416
+ }
417
+ const serverDir = path.join(deployRoot, "server");
418
+ await fs.mkdir(serverDir, { recursive: true });
419
+ await writeRenderedFiles(serverDir, [
420
+ {
421
+ output: "docker-compose.yml",
422
+ stub: redisOn ? STUBS.deploy.server.compose.redisDev : STUBS.deploy.server.compose.noredis
423
+ },
424
+ {
425
+ output: ".env.example",
426
+ stub: redisOn ? STUBS.deploy.server.env.redis : STUBS.deploy.server.env.noredis
427
+ },
428
+ {
429
+ output: ".env.local.example",
430
+ stub: STUBS.deploy.server.env.localExample
431
+ }
432
+ ]);
433
+ }
434
+ let redisOn = redisEnabled();
435
+ if (!redisOn && process.env.REDIS === undefined) {
436
+ const rootRedis = (await readEnvValue(path.resolve(process.cwd(), ".env"), "REDIS")).toLowerCase();
437
+ redisOn = rootRedis === "true" || rootRedis === "1" || rootRedis === "yes";
438
+ }
439
+ await ensureEnvFile(envPath, envExamplePath);
440
+ await ensureEnvFile(localEnvPath, localEnvExamplePath);
441
+ await ensureDockerNetwork("nginx-proxy");
442
+ await ensureDockerNetwork("infra");
443
+ await ensureBindMountFile(path.join(deployRoot, "server", "pgadmin", "servers.json"), STUBS.deploy.server.pgadminServers);
444
+ if (redisOn) await ensureBindMountFile(path.join(deployRoot, "server", "redis", "redis.conf"), STUBS.deploy.server.redisConfig);
445
+ await runCommand("docker", ["compose", "--env-file", envPath, "--env-file", localEnvPath, "-f", composePath, "up", "-d"]);
446
+ await syncPgAdminServers(deployRoot, envPath, localEnvPath);
447
+ return;
448
+ }
449
+ if (commandName === "deploy:app") {
450
+ const composePath = path.join(deployRoot, "docker-compose.yml");
451
+ const envPath = path.join(deployRoot, ".env");
452
+ const envExamplePath = path.join(deployRoot, ".env.example");
453
+ const serverEnvPath = path.join(deployRoot, "server", ".env");
454
+ const serverEnvExamplePath = path.join(deployRoot, "server", ".env.example");
455
+ await ensureEnvFile(envPath, envExamplePath);
456
+ try { await fs.access(serverEnvExamplePath); await ensureEnvFile(serverEnvPath, serverEnvExamplePath); } catch { }
457
+ try { await fs.access(serverEnvPath); await syncDeployDatabaseUrlWithServerEnv(envPath, serverEnvPath); await ensureDeployDatabaseReady(envPath, serverEnvPath); } catch { }
458
+ const composeArgs = ["compose"]; try { await fs.access(serverEnvPath); composeArgs.push("--env-file", serverEnvPath); } catch { }
459
+ composeArgs.push("--env-file", envPath, "-f", composePath, "up", "-d", "--build", "--force-recreate");
460
+ await runCommand("docker", composeArgs);
461
+ }
462
+ }
463
+
464
+ /** Start shared server infra on remote host (via SSH). */
465
+ export async function runRemoteServer(flags = []) {
466
+ const configFlag = flags.find((f) => f.startsWith("--config="));
467
+ const configPath = configFlag ? configFlag.split("=")[1] : "deploy/workflow.remote.json";
468
+ const config = await readRemoteConfig(configPath);
469
+ const { sshArgs, remoteHost } = buildSshArgs(config);
470
+ const cd = `cd "${config.targetPath}"`;
471
+
472
+ console.log("Ensuring remote Docker networks...");
473
+ await runCommand("ssh", [...sshArgs, remoteHost, `docker network inspect nginx-proxy >/dev/null 2>&1 || docker network create nginx-proxy`]);
474
+ await runCommand("ssh", [...sshArgs, remoteHost, `docker network inspect infra >/dev/null 2>&1 || docker network create infra`]);
475
+
476
+ console.log("Starting server infra containers...");
477
+ await runCommand("ssh", [...sshArgs, remoteHost, `${cd} && docker compose --env-file deploy/server/.env -f deploy/server/docker-compose.yml up -d`]);
478
+
479
+ const localEmail = await readEnvValue(path.resolve(process.cwd(), "deploy/server/.env"), "PGADMIN_DEFAULT_EMAIL");
480
+ if (localEmail) {
481
+ console.log("Syncing pgAdmin servers...");
482
+ await runCommand("ssh", [...sshArgs, remoteHost, `${cd} && docker exec pgadmin /venv/bin/python /pgadmin4/setup.py load-servers /pgadmin4/servers.json --user "${localEmail}" --replace 2>/dev/null || true`]);
483
+ }
484
+ }
485
+
486
+ /** Build and start app stack on remote host (via SSH). */
487
+ export async function runRemoteApp(flags = []) {
488
+ const configFlag = flags.find((f) => f.startsWith("--config="));
489
+ const configPath = configFlag ? configFlag.split("=")[1] : "deploy/workflow.remote.json";
490
+ const config = await readRemoteConfig(configPath);
491
+ const { sshArgs, remoteHost } = buildSshArgs(config);
492
+ const cd = `cd "${config.targetPath}"`;
493
+
494
+ console.log("Building and starting app containers...");
495
+ await runCommand("ssh", [...sshArgs, remoteHost, `${cd} && docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -d --build --force-recreate`]);
496
+ }
497
+
498
+ /** Import a MySQL dump file into the local MySQL container. */
499
+ export async function importMysqlDumpLocal(flags = []) {
500
+ const deployRoot = path.resolve(process.cwd(), "deploy");
501
+ const sqlFile = path.resolve(process.cwd(), getOption(flags, "--file", "deploy/nexgen.sql"));
502
+ const database = getOption(flags, "--database", "nexgen");
503
+ const container = getOption(flags, "--container", "mysql-global");
504
+ const user = getOption(flags, "--user", "root");
505
+ let password = getOption(flags, "--password", "");
506
+ if (!password) password = await readEnvValue(path.join(deployRoot, "server", ".env"), "MYSQL_ROOT_PASSWORD");
507
+ if (!password) {
508
+ const dbUrl = await readEnvValue(path.join(deployRoot, ".env"), "DATABASE_URL");
509
+ try { if (dbUrl) password = decodeURIComponent(new URL(dbUrl).password); } catch {}
510
+ }
511
+ await runCommand("docker", ["exec", container, "mysql", `-u${user}`, `-p${password}`, "-e", `CREATE DATABASE IF NOT EXISTS ${database};`]);
512
+ await new Promise((resolve, reject) => {
513
+ const child = spawn("docker", ["exec", "-i", container, "mysql", `-u${user}`, `-p${password}`, database], { shell: false, stdio: ["pipe", "inherit", "inherit"] });
514
+ const input = fsSync.createReadStream(sqlFile);
515
+ input.on("error", reject); child.on("error", reject); child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`MySQL import failed with code ${code}`))));
516
+ input.pipe(child.stdin);
517
+ });
518
+ }
519
+
520
+ /** Import a MySQL dump file into a remote MySQL container via SSH. */
521
+ export async function importMysqlDumpRemote(flags = []) {
522
+ const configFlag = flags.find((flag) => flag.startsWith("--config="));
523
+ const configPath = configFlag ? configFlag.replace("--config=", "") : "deploy/workflow.remote.json";
524
+ const parsed = JSON.parse(await fs.readFile(path.resolve(process.cwd(), configPath), "utf8"));
525
+ const remote = parsed.remote || {};
526
+ const host = String(remote.host || "").trim();
527
+ const userHost = String(remote.user || "").trim();
528
+ const port = String(remote.port || 22).trim();
529
+ const keyPath = expandHome(String(remote.keyPath || "").trim());
530
+ const targetPath = String(remote.targetPath || "").trim();
531
+ const dbImport = parsed.databaseImport || {};
532
+ const file = getOption(flags, "--file", String(dbImport.file || "deploy/nexgen.sql"));
533
+ const database = getOption(flags, "--database", String(dbImport.database || "nexgen"));
534
+ const container = getOption(flags, "--container", String(dbImport.container || "mysql-global"));
535
+ const dbUser = getOption(flags, "--user", String(dbImport.user || "root"));
536
+ const passwordFlag = getOption(flags, "--password", "");
537
+ const passwordCmd = passwordFlag
538
+ ? `MYSQL_ROOT_PASSWORD='${passwordFlag.replace(/'/g, "'\\''")}'`
539
+ : `MYSQL_ROOT_PASSWORD=$(grep '^MYSQL_ROOT_PASSWORD=' deploy/server/.env 2>/dev/null | head -1 | cut -d '=' -f2-)`;
540
+ const sshBaseArgs = ["-p", port]; if (keyPath) sshBaseArgs.push("-i", keyPath);
541
+ const remoteHost = `${userHost}@${host}`;
542
+ await runCommand("ssh", [...sshBaseArgs, remoteHost, `cd \"${targetPath}\" && [ -f \"${file}\" ] || (echo \"Missing SQL file: ${file}\" && exit 1)`]);
543
+ await runCommand("ssh", [...sshBaseArgs, remoteHost, `cd \"${targetPath}\" && ${passwordCmd} && docker exec ${container} mysql -u${dbUser} -p\"$MYSQL_ROOT_PASSWORD\" -e \"CREATE DATABASE IF NOT EXISTS ${database};\"`]);
544
+ await runCommand("ssh", [...sshBaseArgs, remoteHost, `cd \"${targetPath}\" && ${passwordCmd} && docker exec -i ${container} mysql -u${dbUser} -p\"$MYSQL_ROOT_PASSWORD\" ${database} < \"${file}\"`]);
545
+ }
546
+
547
+ /** Run a deploy workflow from a config file or inline flags. */
548
+ export async function runDeployWorkflow(flags = []) {
549
+ const serverOnly = hasFlag(flags, "--server-only");
550
+ const appOnly = hasFlag(flags, "--app-only");
551
+ const refresh = hasFlag(flags, "--refresh");
552
+ const dryRun = hasFlag(flags, "--dry-run");
553
+ const runtimeFlag = flags.find((flag) => flag.startsWith("--runtime="));
554
+ const pmFlag = flags.find((flag) => flag.startsWith("--pm="));
555
+ const configFlag = flags.find((flag) => flag.startsWith("--config="));
556
+ const configPath = configFlag ? configFlag.replace("--config=", "") : "";
557
+ if (configPath) {
558
+ const parsed = JSON.parse(await fs.readFile(path.resolve(process.cwd(), configPath), "utf8"));
559
+ const steps = Array.isArray(parsed.steps) ? parsed.steps : [];
560
+ for (const step of steps) {
561
+ if (step.enabled === false) continue;
562
+ const run = String(step.run || "").trim();
563
+ if (!run) continue;
564
+ const [subcommand, ...subArgs] = run.split(/\s+/).filter(Boolean);
565
+ if (subcommand === "deploy:server" || subcommand === "deploy:app") await runDeploy(subcommand);
566
+ else if (subcommand === "deploy:db:import") await importMysqlDumpLocal(subArgs);
567
+ else if (subcommand === "deploy:create") await createDeploy(subArgs);
568
+ }
569
+ return;
570
+ }
571
+ if (refresh) await createDeploy(["--force", ...(runtimeFlag ? [runtimeFlag] : []), ...(pmFlag ? [pmFlag] : [])]);
572
+ if (!appOnly && !dryRun) await runDeploy("deploy:server");
573
+ if (!serverOnly && !dryRun) await runDeploy("deploy:app");
574
+ }
575
+
576
+ /** Initialize a local workflow config file. */
577
+ export async function initDeployWorkflow() {
578
+ const filePath = path.resolve(process.cwd(), "deploy/workflow.local.json");
579
+ const template = await readStubRaw(STUBS.deploy.workflow.local);
580
+ await writeFileIfMissing(filePath, template);
581
+ }
582
+
583
+ /** Initialize a remote workflow config file. */
584
+ export async function initRemoteDeployWorkflow() {
585
+ const filePath = path.resolve(process.cwd(), "deploy/workflow.remote.json");
586
+ const template = await readStubRaw(STUBS.deploy.workflow.remote);
587
+ await writeFileIfMissing(filePath, template);
588
+ }
589
+
590
+ /** Run a remote workflow: upload project files and deploy on remote Docker host. */
591
+ export async function runRemoteWorkflow(flags = []) {
592
+ const configFlag = flags.find((flag) => flag.startsWith("--config="));
593
+ const configPath = configFlag ? configFlag.replace("--config=", "") : "deploy/workflow.remote.json";
594
+ const config = await readRemoteConfig(configPath);
595
+ const { sshArgs, remoteHost } = buildSshArgs(config);
596
+ const serverOnly = hasFlag(flags, "--server-only");
597
+ const appOnly = hasFlag(flags, "--app-only");
598
+
599
+ console.log("Creating remote target directory...");
600
+ await runCommand("ssh", [...sshArgs, remoteHost, `mkdir -p "${config.targetPath}"`]);
601
+
602
+ console.log("Uploading project files (excluding node_modules, .git, dist)...");
603
+ const source = `${path.resolve(process.cwd())}/`;
604
+ const target = `${config.user}@${config.host}:${config.targetPath}/`;
605
+ await runCommand("rsync", [
606
+ "-avz", "--delete",
607
+ "--exclude=node_modules",
608
+ "--exclude=.git",
609
+ "--exclude=dist",
610
+ "--exclude=/.env*",
611
+ "-e", `ssh -p ${config.port}${config.keyPath ? ` -i "${config.keyPath}"` : ""}`,
612
+ source, target
613
+ ]);
614
+
615
+ if (!appOnly) await runRemoteServer(flags);
616
+ if (!serverOnly) await runRemoteApp(flags);
617
+ }
618
+
619
+ /** Run a local workflow (delegates to runDeployWorkflow). */
620
+ export async function runLocalWorkflow(flags = []) {
621
+ await runDeployWorkflow(flags);
622
+ }
623
+
624
+ /** Promote: run local workflow then remote workflow with appropriate config files. */
625
+ export async function runPromoteWorkflow(flags = []) {
626
+ const configFlag = flags.find((flag) => flag.startsWith("--config="));
627
+ const configValue = configFlag ? configFlag.replace("--config=", "") : "";
628
+ const normalized = configValue.toLowerCase();
629
+ const localConfig = !configValue ? "deploy/workflow.local.json" : normalized.includes("remote") ? "deploy/workflow.local.json" : configValue;
630
+ const remoteConfig = !configValue ? "deploy/workflow.remote.json" : normalized.includes("local") ? "deploy/workflow.remote.json" : configValue;
631
+ const localFlags = [...flags.filter((flag) => !flag.startsWith("--config=")), `--config=${localConfig}`];
632
+ const remoteFlags = [...flags.filter((flag) => flag !== "--refresh" && !flag.startsWith("--config=")), `--config=${remoteConfig}`];
633
+ await runLocalWorkflow(localFlags);
634
+ await runRemoteWorkflow(remoteFlags);
635
+ }