@cicore/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.
Files changed (337) hide show
  1. package/bin/ci.js +13 -0
  2. package/dist/commands/addon/api-actions.d.ts +45 -0
  3. package/dist/commands/addon/api-actions.d.ts.map +1 -0
  4. package/dist/commands/addon/api-actions.js +281 -0
  5. package/dist/commands/addon/api-actions.js.map +1 -0
  6. package/dist/commands/addon/build.d.ts +11 -0
  7. package/dist/commands/addon/build.d.ts.map +1 -0
  8. package/dist/commands/addon/build.js +182 -0
  9. package/dist/commands/addon/build.js.map +1 -0
  10. package/dist/commands/addon/create.d.ts +11 -0
  11. package/dist/commands/addon/create.d.ts.map +1 -0
  12. package/dist/commands/addon/create.js +1186 -0
  13. package/dist/commands/addon/create.js.map +1 -0
  14. package/dist/commands/addon/delete.d.ts +13 -0
  15. package/dist/commands/addon/delete.d.ts.map +1 -0
  16. package/dist/commands/addon/delete.js +83 -0
  17. package/dist/commands/addon/delete.js.map +1 -0
  18. package/dist/commands/addon/deploy.d.ts +27 -0
  19. package/dist/commands/addon/deploy.d.ts.map +1 -0
  20. package/dist/commands/addon/deploy.js +459 -0
  21. package/dist/commands/addon/deploy.js.map +1 -0
  22. package/dist/commands/addon/dev-deploy.d.ts +31 -0
  23. package/dist/commands/addon/dev-deploy.d.ts.map +1 -0
  24. package/dist/commands/addon/dev-deploy.js +128 -0
  25. package/dist/commands/addon/dev-deploy.js.map +1 -0
  26. package/dist/commands/addon/dev.d.ts +36 -0
  27. package/dist/commands/addon/dev.d.ts.map +1 -0
  28. package/dist/commands/addon/dev.js +323 -0
  29. package/dist/commands/addon/dev.js.map +1 -0
  30. package/dist/commands/addon/extract-classes.d.ts +23 -0
  31. package/dist/commands/addon/extract-classes.d.ts.map +1 -0
  32. package/dist/commands/addon/extract-classes.js +281 -0
  33. package/dist/commands/addon/extract-classes.js.map +1 -0
  34. package/dist/commands/addon/generate-safelist.d.ts +24 -0
  35. package/dist/commands/addon/generate-safelist.d.ts.map +1 -0
  36. package/dist/commands/addon/generate-safelist.js +276 -0
  37. package/dist/commands/addon/generate-safelist.js.map +1 -0
  38. package/dist/commands/addon/index.d.ts +19 -0
  39. package/dist/commands/addon/index.d.ts.map +1 -0
  40. package/dist/commands/addon/index.js +296 -0
  41. package/dist/commands/addon/index.js.map +1 -0
  42. package/dist/commands/addon/init-repo.d.ts +25 -0
  43. package/dist/commands/addon/init-repo.d.ts.map +1 -0
  44. package/dist/commands/addon/init-repo.js +171 -0
  45. package/dist/commands/addon/init-repo.js.map +1 -0
  46. package/dist/commands/addon/install.d.ts +23 -0
  47. package/dist/commands/addon/install.d.ts.map +1 -0
  48. package/dist/commands/addon/install.js +84 -0
  49. package/dist/commands/addon/install.js.map +1 -0
  50. package/dist/commands/addon/list.d.ts +10 -0
  51. package/dist/commands/addon/list.d.ts.map +1 -0
  52. package/dist/commands/addon/list.js +102 -0
  53. package/dist/commands/addon/list.js.map +1 -0
  54. package/dist/commands/addon/manifest-refresh.d.ts +17 -0
  55. package/dist/commands/addon/manifest-refresh.d.ts.map +1 -0
  56. package/dist/commands/addon/manifest-refresh.js +48 -0
  57. package/dist/commands/addon/manifest-refresh.js.map +1 -0
  58. package/dist/commands/addon/migrate.d.ts +40 -0
  59. package/dist/commands/addon/migrate.d.ts.map +1 -0
  60. package/dist/commands/addon/migrate.js +236 -0
  61. package/dist/commands/addon/migrate.js.map +1 -0
  62. package/dist/commands/addon/publish.d.ts +33 -0
  63. package/dist/commands/addon/publish.d.ts.map +1 -0
  64. package/dist/commands/addon/publish.js +236 -0
  65. package/dist/commands/addon/publish.js.map +1 -0
  66. package/dist/commands/addon/scaffold-quality.d.ts +21 -0
  67. package/dist/commands/addon/scaffold-quality.d.ts.map +1 -0
  68. package/dist/commands/addon/scaffold-quality.js +90 -0
  69. package/dist/commands/addon/scaffold-quality.js.map +1 -0
  70. package/dist/commands/addon/sign.d.ts +9 -0
  71. package/dist/commands/addon/sign.d.ts.map +1 -0
  72. package/dist/commands/addon/sign.js +83 -0
  73. package/dist/commands/addon/sign.js.map +1 -0
  74. package/dist/commands/addon/toggle.d.ts +6 -0
  75. package/dist/commands/addon/toggle.d.ts.map +1 -0
  76. package/dist/commands/addon/toggle.js +46 -0
  77. package/dist/commands/addon/toggle.js.map +1 -0
  78. package/dist/commands/agent/index.d.ts +34 -0
  79. package/dist/commands/agent/index.d.ts.map +1 -0
  80. package/dist/commands/agent/index.js +564 -0
  81. package/dist/commands/agent/index.js.map +1 -0
  82. package/dist/commands/brand/index.d.ts +54 -0
  83. package/dist/commands/brand/index.d.ts.map +1 -0
  84. package/dist/commands/brand/index.js +367 -0
  85. package/dist/commands/brand/index.js.map +1 -0
  86. package/dist/commands/build/index.d.ts +53 -0
  87. package/dist/commands/build/index.d.ts.map +1 -0
  88. package/dist/commands/build/index.js +726 -0
  89. package/dist/commands/build/index.js.map +1 -0
  90. package/dist/commands/cache/flush-local.d.ts +31 -0
  91. package/dist/commands/cache/flush-local.d.ts.map +1 -0
  92. package/dist/commands/cache/flush-local.js +161 -0
  93. package/dist/commands/cache/flush-local.js.map +1 -0
  94. package/dist/commands/cache/index.d.ts +14 -0
  95. package/dist/commands/cache/index.d.ts.map +1 -0
  96. package/dist/commands/cache/index.js +453 -0
  97. package/dist/commands/cache/index.js.map +1 -0
  98. package/dist/commands/check/index.d.ts +8 -0
  99. package/dist/commands/check/index.d.ts.map +1 -0
  100. package/dist/commands/check/index.js +1316 -0
  101. package/dist/commands/check/index.js.map +1 -0
  102. package/dist/commands/cloudflare/index.d.ts +8 -0
  103. package/dist/commands/cloudflare/index.d.ts.map +1 -0
  104. package/dist/commands/cloudflare/index.js +453 -0
  105. package/dist/commands/cloudflare/index.js.map +1 -0
  106. package/dist/commands/core/create.d.ts +12 -0
  107. package/dist/commands/core/create.d.ts.map +1 -0
  108. package/dist/commands/core/create.js +206 -0
  109. package/dist/commands/core/create.js.map +1 -0
  110. package/dist/commands/core/delete.d.ts +11 -0
  111. package/dist/commands/core/delete.d.ts.map +1 -0
  112. package/dist/commands/core/delete.js +64 -0
  113. package/dist/commands/core/delete.js.map +1 -0
  114. package/dist/commands/core/env.d.ts +12 -0
  115. package/dist/commands/core/env.d.ts.map +1 -0
  116. package/dist/commands/core/env.js +95 -0
  117. package/dist/commands/core/env.js.map +1 -0
  118. package/dist/commands/core/health.d.ts +6 -0
  119. package/dist/commands/core/health.d.ts.map +1 -0
  120. package/dist/commands/core/health.js +215 -0
  121. package/dist/commands/core/health.js.map +1 -0
  122. package/dist/commands/core/index.d.ts +15 -0
  123. package/dist/commands/core/index.d.ts.map +1 -0
  124. package/dist/commands/core/index.js +86 -0
  125. package/dist/commands/core/index.js.map +1 -0
  126. package/dist/commands/core/list.d.ts +11 -0
  127. package/dist/commands/core/list.d.ts.map +1 -0
  128. package/dist/commands/core/list.js +58 -0
  129. package/dist/commands/core/list.js.map +1 -0
  130. package/dist/commands/core/rebuild.d.ts +13 -0
  131. package/dist/commands/core/rebuild.d.ts.map +1 -0
  132. package/dist/commands/core/rebuild.js +119 -0
  133. package/dist/commands/core/rebuild.js.map +1 -0
  134. package/dist/commands/db/index.d.ts +23 -0
  135. package/dist/commands/db/index.d.ts.map +1 -0
  136. package/dist/commands/db/index.js +355 -0
  137. package/dist/commands/db/index.js.map +1 -0
  138. package/dist/commands/db/promote-silo.d.ts +320 -0
  139. package/dist/commands/db/promote-silo.d.ts.map +1 -0
  140. package/dist/commands/db/promote-silo.js +930 -0
  141. package/dist/commands/db/promote-silo.js.map +1 -0
  142. package/dist/commands/db/relocate.d.ts +41 -0
  143. package/dist/commands/db/relocate.d.ts.map +1 -0
  144. package/dist/commands/db/relocate.js +482 -0
  145. package/dist/commands/db/relocate.js.map +1 -0
  146. package/dist/commands/db/rollback-silo.d.ts +44 -0
  147. package/dist/commands/db/rollback-silo.d.ts.map +1 -0
  148. package/dist/commands/db/rollback-silo.js +402 -0
  149. package/dist/commands/db/rollback-silo.js.map +1 -0
  150. package/dist/commands/deploy/index.d.ts +26 -0
  151. package/dist/commands/deploy/index.d.ts.map +1 -0
  152. package/dist/commands/deploy/index.js +107 -0
  153. package/dist/commands/deploy/index.js.map +1 -0
  154. package/dist/commands/devops/index.d.ts +6 -0
  155. package/dist/commands/devops/index.d.ts.map +1 -0
  156. package/dist/commands/devops/index.js +220 -0
  157. package/dist/commands/devops/index.js.map +1 -0
  158. package/dist/commands/domain/index.d.ts +8 -0
  159. package/dist/commands/domain/index.d.ts.map +1 -0
  160. package/dist/commands/domain/index.js +386 -0
  161. package/dist/commands/domain/index.js.map +1 -0
  162. package/dist/commands/image/index.d.ts +8 -0
  163. package/dist/commands/image/index.d.ts.map +1 -0
  164. package/dist/commands/image/index.js +308 -0
  165. package/dist/commands/image/index.js.map +1 -0
  166. package/dist/commands/install/factory-reset.d.ts +21 -0
  167. package/dist/commands/install/factory-reset.d.ts.map +1 -0
  168. package/dist/commands/install/factory-reset.js +83 -0
  169. package/dist/commands/install/factory-reset.js.map +1 -0
  170. package/dist/commands/install/index.d.ts +17 -0
  171. package/dist/commands/install/index.d.ts.map +1 -0
  172. package/dist/commands/install/index.js +44 -0
  173. package/dist/commands/install/index.js.map +1 -0
  174. package/dist/commands/install/install.d.ts +35 -0
  175. package/dist/commands/install/install.d.ts.map +1 -0
  176. package/dist/commands/install/install.js +171 -0
  177. package/dist/commands/install/install.js.map +1 -0
  178. package/dist/commands/login/index.d.ts +15 -0
  179. package/dist/commands/login/index.d.ts.map +1 -0
  180. package/dist/commands/login/index.js +58 -0
  181. package/dist/commands/login/index.js.map +1 -0
  182. package/dist/commands/nginx/index.d.ts +11 -0
  183. package/dist/commands/nginx/index.d.ts.map +1 -0
  184. package/dist/commands/nginx/index.js +580 -0
  185. package/dist/commands/nginx/index.js.map +1 -0
  186. package/dist/commands/server/bootstrap.d.ts +25 -0
  187. package/dist/commands/server/bootstrap.d.ts.map +1 -0
  188. package/dist/commands/server/bootstrap.js +260 -0
  189. package/dist/commands/server/bootstrap.js.map +1 -0
  190. package/dist/commands/server/index.d.ts +8 -0
  191. package/dist/commands/server/index.d.ts.map +1 -0
  192. package/dist/commands/server/index.js +2524 -0
  193. package/dist/commands/server/index.js.map +1 -0
  194. package/dist/commands/setup/index.d.ts +34 -0
  195. package/dist/commands/setup/index.d.ts.map +1 -0
  196. package/dist/commands/setup/index.js +423 -0
  197. package/dist/commands/setup/index.js.map +1 -0
  198. package/dist/commands/ssl/index.d.ts +8 -0
  199. package/dist/commands/ssl/index.d.ts.map +1 -0
  200. package/dist/commands/ssl/index.js +275 -0
  201. package/dist/commands/ssl/index.js.map +1 -0
  202. package/dist/commands/superadmin/index.d.ts +16 -0
  203. package/dist/commands/superadmin/index.d.ts.map +1 -0
  204. package/dist/commands/superadmin/index.js +81 -0
  205. package/dist/commands/superadmin/index.js.map +1 -0
  206. package/dist/commands/tenant/index.d.ts +6 -0
  207. package/dist/commands/tenant/index.d.ts.map +1 -0
  208. package/dist/commands/tenant/index.js +192 -0
  209. package/dist/commands/tenant/index.js.map +1 -0
  210. package/dist/index.d.ts +11 -0
  211. package/dist/index.d.ts.map +1 -0
  212. package/dist/index.js +107 -0
  213. package/dist/index.js.map +1 -0
  214. package/dist/lib/addon-sign.d.ts +23 -0
  215. package/dist/lib/addon-sign.d.ts.map +1 -0
  216. package/dist/lib/addon-sign.js +39 -0
  217. package/dist/lib/addon-sign.js.map +1 -0
  218. package/dist/lib/addon-sign.test.d.ts +2 -0
  219. package/dist/lib/addon-sign.test.d.ts.map +1 -0
  220. package/dist/lib/addon-sign.test.js +27 -0
  221. package/dist/lib/addon-sign.test.js.map +1 -0
  222. package/dist/lib/cdn.d.ts +25 -0
  223. package/dist/lib/cdn.d.ts.map +1 -0
  224. package/dist/lib/cdn.js +131 -0
  225. package/dist/lib/cdn.js.map +1 -0
  226. package/dist/lib/cloudflare.d.ts +133 -0
  227. package/dist/lib/cloudflare.d.ts.map +1 -0
  228. package/dist/lib/cloudflare.js +435 -0
  229. package/dist/lib/cloudflare.js.map +1 -0
  230. package/dist/lib/config.d.ts +96 -0
  231. package/dist/lib/config.d.ts.map +1 -0
  232. package/dist/lib/config.js +132 -0
  233. package/dist/lib/config.js.map +1 -0
  234. package/dist/lib/env.d.ts +8 -0
  235. package/dist/lib/env.d.ts.map +1 -0
  236. package/dist/lib/env.js +64 -0
  237. package/dist/lib/env.js.map +1 -0
  238. package/dist/lib/hosts.d.ts +194 -0
  239. package/dist/lib/hosts.d.ts.map +1 -0
  240. package/dist/lib/hosts.js +183 -0
  241. package/dist/lib/hosts.js.map +1 -0
  242. package/dist/lib/logger.d.ts +68 -0
  243. package/dist/lib/logger.d.ts.map +1 -0
  244. package/dist/lib/logger.js +130 -0
  245. package/dist/lib/logger.js.map +1 -0
  246. package/dist/lib/nginx-config.d.ts +78 -0
  247. package/dist/lib/nginx-config.d.ts.map +1 -0
  248. package/dist/lib/nginx-config.js +736 -0
  249. package/dist/lib/nginx-config.js.map +1 -0
  250. package/dist/lib/ops/addon-dev.d.ts +93 -0
  251. package/dist/lib/ops/addon-dev.d.ts.map +1 -0
  252. package/dist/lib/ops/addon-dev.js +237 -0
  253. package/dist/lib/ops/addon-dev.js.map +1 -0
  254. package/dist/lib/ops/addon-quality.d.ts +38 -0
  255. package/dist/lib/ops/addon-quality.d.ts.map +1 -0
  256. package/dist/lib/ops/addon-quality.js +338 -0
  257. package/dist/lib/ops/addon-quality.js.map +1 -0
  258. package/dist/lib/ops/addon-routes.d.ts +49 -0
  259. package/dist/lib/ops/addon-routes.d.ts.map +1 -0
  260. package/dist/lib/ops/addon-routes.js +189 -0
  261. package/dist/lib/ops/addon-routes.js.map +1 -0
  262. package/dist/lib/ops/addon.d.ts +120 -0
  263. package/dist/lib/ops/addon.d.ts.map +1 -0
  264. package/dist/lib/ops/addon.js +260 -0
  265. package/dist/lib/ops/addon.js.map +1 -0
  266. package/dist/lib/ops/cdn.d.ts +87 -0
  267. package/dist/lib/ops/cdn.d.ts.map +1 -0
  268. package/dist/lib/ops/cdn.js +170 -0
  269. package/dist/lib/ops/cdn.js.map +1 -0
  270. package/dist/lib/ops/cf.d.ts +36 -0
  271. package/dist/lib/ops/cf.d.ts.map +1 -0
  272. package/dist/lib/ops/cf.js +114 -0
  273. package/dist/lib/ops/cf.js.map +1 -0
  274. package/dist/lib/ops/compose.d.ts +95 -0
  275. package/dist/lib/ops/compose.d.ts.map +1 -0
  276. package/dist/lib/ops/compose.js +165 -0
  277. package/dist/lib/ops/compose.js.map +1 -0
  278. package/dist/lib/ops/core.d.ts +117 -0
  279. package/dist/lib/ops/core.d.ts.map +1 -0
  280. package/dist/lib/ops/core.js +322 -0
  281. package/dist/lib/ops/core.js.map +1 -0
  282. package/dist/lib/ops/db.d.ts +116 -0
  283. package/dist/lib/ops/db.d.ts.map +1 -0
  284. package/dist/lib/ops/db.js +351 -0
  285. package/dist/lib/ops/db.js.map +1 -0
  286. package/dist/lib/ops/dns.d.ts +111 -0
  287. package/dist/lib/ops/dns.d.ts.map +1 -0
  288. package/dist/lib/ops/dns.js +306 -0
  289. package/dist/lib/ops/dns.js.map +1 -0
  290. package/dist/lib/ops/image.d.ts +94 -0
  291. package/dist/lib/ops/image.d.ts.map +1 -0
  292. package/dist/lib/ops/image.js +159 -0
  293. package/dist/lib/ops/image.js.map +1 -0
  294. package/dist/lib/ops/nginx.d.ts +114 -0
  295. package/dist/lib/ops/nginx.d.ts.map +1 -0
  296. package/dist/lib/ops/nginx.js +388 -0
  297. package/dist/lib/ops/nginx.js.map +1 -0
  298. package/dist/lib/ops/redis.d.ts +7 -0
  299. package/dist/lib/ops/redis.d.ts.map +1 -0
  300. package/dist/lib/ops/redis.js +35 -0
  301. package/dist/lib/ops/redis.js.map +1 -0
  302. package/dist/lib/ops/ssh.d.ts +127 -0
  303. package/dist/lib/ops/ssh.d.ts.map +1 -0
  304. package/dist/lib/ops/ssh.js +269 -0
  305. package/dist/lib/ops/ssh.js.map +1 -0
  306. package/dist/lib/prompts.d.ts +46 -0
  307. package/dist/lib/prompts.d.ts.map +1 -0
  308. package/dist/lib/prompts.js +113 -0
  309. package/dist/lib/prompts.js.map +1 -0
  310. package/dist/lib/sast.d.ts +43 -0
  311. package/dist/lib/sast.d.ts.map +1 -0
  312. package/dist/lib/sast.js +79 -0
  313. package/dist/lib/sast.js.map +1 -0
  314. package/dist/lib/sast.test.d.ts +2 -0
  315. package/dist/lib/sast.test.d.ts.map +1 -0
  316. package/dist/lib/sast.test.js +33 -0
  317. package/dist/lib/sast.test.js.map +1 -0
  318. package/dist/lib/shell.d.ts +61 -0
  319. package/dist/lib/shell.d.ts.map +1 -0
  320. package/dist/lib/shell.js +183 -0
  321. package/dist/lib/shell.js.map +1 -0
  322. package/dist/lib/ssh-config.d.ts +37 -0
  323. package/dist/lib/ssh-config.d.ts.map +1 -0
  324. package/dist/lib/ssh-config.js +122 -0
  325. package/dist/lib/ssh-config.js.map +1 -0
  326. package/dist/lib/tenant-scope.d.ts +38 -0
  327. package/dist/lib/tenant-scope.d.ts.map +1 -0
  328. package/dist/lib/tenant-scope.js +129 -0
  329. package/dist/lib/tenant-scope.js.map +1 -0
  330. package/dist/lib/tenant-scope.test.d.ts +2 -0
  331. package/dist/lib/tenant-scope.test.d.ts.map +1 -0
  332. package/dist/lib/tenant-scope.test.js +223 -0
  333. package/dist/lib/tenant-scope.test.js.map +1 -0
  334. package/package.json +58 -0
  335. package/templates/bootstrap/.env.template +54 -0
  336. package/templates/bootstrap/docker-compose.yml +145 -0
  337. package/templates/vhost.conf.tmpl +446 -0
@@ -0,0 +1,2524 @@
1
+ /**
2
+ * CiCore CLI - Server Management Commands
3
+ *
4
+ * Full server setup, installation, and management
5
+ */
6
+ import { log, spinner, formatTable } from '../../lib/logger.js';
7
+ import { exec } from '../../lib/shell.js';
8
+ import { paths, getDockerRegistry } from '../../lib/config.js';
9
+ import { confirm, input, select } from '../../lib/prompts.js';
10
+ import { getSSHHost, listSSHHosts, buildSSHCommand, buildSCPCommand } from '../../lib/ssh-config.js';
11
+ import { getCloudflareToken, setupDomainForVuCore } from '../../lib/cloudflare.js';
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+ export function registerServerCommands(program) {
15
+ const server = program
16
+ .command('server')
17
+ .description('Server management commands');
18
+ // ========================================
19
+ // PHASE 1: vu server setup - Docker & Services
20
+ // ========================================
21
+ server
22
+ .command('setup')
23
+ .description('Phase 1: Install Docker, pull images, start base services')
24
+ .option('-h, --host <host>', 'SSH host name from ~/.ssh/config')
25
+ .option('--local', 'Run directly on this server (no SSH)')
26
+ .option('--skip-docker', 'Skip Docker installation (if already installed)')
27
+ .action(async (options) => {
28
+ await serverSetup(options);
29
+ });
30
+ // ========================================
31
+ // PHASE 2: vu server deploy - ciCore Files
32
+ // ========================================
33
+ server
34
+ .command('deploy')
35
+ .description('Phase 2: Deploy ciCore files to server (templates, configs)')
36
+ .option('-h, --host <host>', 'SSH host name from ~/.ssh/config')
37
+ .option('--local', 'Run directly on this server (no SSH)')
38
+ .option('-c, --core <name>', 'Core name to create', 'core1')
39
+ .action(async (options) => {
40
+ await serverDeploy(options);
41
+ });
42
+ // ========================================
43
+ // PHASE 3: vu server domain - Domain Setup
44
+ // ========================================
45
+ server
46
+ .command('domain')
47
+ .description('Phase 3: Configure domain for a core (nginx config)')
48
+ .option('-h, --host <host>', 'SSH host name from ~/.ssh/config')
49
+ .option('--local', 'Run directly on this server (no SSH)')
50
+ .option('-c, --core <name>', 'Core name', 'core1')
51
+ .option('-d, --domain <domain>', 'Root domain')
52
+ .action(async (options) => {
53
+ await serverDomain(options);
54
+ });
55
+ // ========================================
56
+ // PHASE 4: vu server ssl - SSL Certificate
57
+ // ========================================
58
+ server
59
+ .command('ssl')
60
+ .description('Phase 4: Setup SSL certificate for domain')
61
+ .option('-h, --host <host>', 'SSH host name from ~/.ssh/config')
62
+ .option('--local', 'Run directly on this server (no SSH)')
63
+ .option('-d, --domain <domain>', 'Domain for SSL')
64
+ .option('--cloudflare', 'Use Cloudflare Origin Certificate')
65
+ .option('--letsencrypt', 'Use Let\'s Encrypt certificate')
66
+ .option('-e, --email <email>', 'Email for Let\'s Encrypt')
67
+ .action(async (options) => {
68
+ await serverSSL(options);
69
+ });
70
+ // ========================================
71
+ // FULL INSTALL (runs all phases)
72
+ // ========================================
73
+ server
74
+ .command('install')
75
+ .description('Full installation: setup + deploy + domain + ssl')
76
+ .option('-h, --host <host>', 'SSH host name from ~/.ssh/config')
77
+ .option('--local', 'Run directly on this server (no SSH)')
78
+ .option('-c, --core <name>', 'Core name to create', 'core1')
79
+ .option('-d, --domain <domain>', 'Root domain')
80
+ .option('--skip-docker', 'Skip Docker installation')
81
+ .option('--skip-ssl', 'Skip SSL certificate setup')
82
+ .option('--cloudflare', 'Use Cloudflare for SSL')
83
+ .action(async (options) => {
84
+ await serverInstallFull(options);
85
+ });
86
+ // ========================================
87
+ // UTILITY COMMANDS
88
+ // ========================================
89
+ server
90
+ .command('list')
91
+ .alias('ls')
92
+ .description('List configured SSH hosts')
93
+ .action(async () => {
94
+ await listServers();
95
+ });
96
+ server
97
+ .command('status')
98
+ .description('Check server status')
99
+ .option('-h, --host <host>', 'SSH host name', 'cicore')
100
+ .action(async (options) => {
101
+ await serverStatus(options);
102
+ });
103
+ server
104
+ .command('logs')
105
+ .description('View server logs')
106
+ .option('-h, --host <host>', 'SSH host name', 'cicore')
107
+ .option('-s, --service <service>', 'Service name (nuxt, php, nginx)')
108
+ .option('-f, --follow', 'Follow log output')
109
+ .option('-n, --lines <n>', 'Number of lines', '100')
110
+ .action(async (options) => {
111
+ await serverLogs(options);
112
+ });
113
+ server
114
+ .command('reset')
115
+ .alias('factory-reset')
116
+ .description('Factory reset server - removes all ciCore data and containers')
117
+ .option('-h, --host <host>', 'SSH host name')
118
+ .option('-f, --force', 'Skip confirmation prompts')
119
+ .option('--keep-images', 'Keep Docker images (only remove containers and data)')
120
+ .action(async (options) => {
121
+ await factoryReset(options);
122
+ });
123
+ // ========================================
124
+ // PHASE 0: vu server prepare - System Requirements
125
+ // ========================================
126
+ server
127
+ .command('prepare')
128
+ .description('Phase 0: Install system requirements (Node.js 20, Docker, tree)')
129
+ .option('-h, --host <host>', 'SSH host name from ~/.ssh/config')
130
+ .action(async (options) => {
131
+ await serverPrepare(options);
132
+ });
133
+ // ========================================
134
+ // SYSTEM CHECK
135
+ // ========================================
136
+ server
137
+ .command('check')
138
+ .description('Check server health: services, SSL, API, CORS')
139
+ .option('-h, --host <host>', 'SSH host name from ~/.ssh/config')
140
+ .option('--fix', 'Attempt to fix issues automatically')
141
+ .action(async (options) => {
142
+ await serverCheck(options);
143
+ });
144
+ // ========================================
145
+ // CLI REMOTE EXECUTION
146
+ // ========================================
147
+ server
148
+ .command('cli')
149
+ .description('Install and run CLI on remote server')
150
+ .option('-h, --host <host>', 'SSH host name')
151
+ .option('--install', 'Install/update CLI on server')
152
+ .option('--run <command>', 'Run CLI command on server')
153
+ .action(async (options) => {
154
+ await serverCLI(options);
155
+ });
156
+ server
157
+ .command('remote')
158
+ .description('Run any vu command on remote server')
159
+ .option('-h, --host <host>', 'SSH host name')
160
+ .argument('<cmd...>', 'Command to run (e.g., "server deploy --core core1")')
161
+ .action(async (cmd, options) => {
162
+ await runRemoteCLI(options.host, cmd.join(' '));
163
+ });
164
+ }
165
+ // ============================================
166
+ // HELPER: Get SSH Host
167
+ // ============================================
168
+ async function getHost(hostOption) {
169
+ if (hostOption) {
170
+ const host = await getSSHHost(hostOption);
171
+ if (!host) {
172
+ log.error(`SSH host "${hostOption}" not found in ~/.ssh/config`);
173
+ return null;
174
+ }
175
+ return host;
176
+ }
177
+ const hosts = await listSSHHosts();
178
+ if (hosts.length === 0) {
179
+ log.error('No SSH hosts found in ~/.ssh/config');
180
+ return null;
181
+ }
182
+ const hostName = await select('Select SSH host:', hosts.map(h => ({ name: `${h.name} (${h.hostname})`, value: h.name })));
183
+ return hosts.find(h => h.name === hostName) || null;
184
+ }
185
+ // ============================================
186
+ // LOCAL COMMAND EXECUTION HELPER
187
+ // ============================================
188
+ async function runLocal(command, label) {
189
+ if (label)
190
+ log.info(` → ${label}`);
191
+ try {
192
+ const { execSync } = await import('child_process');
193
+ const output = execSync(command, {
194
+ encoding: 'utf-8',
195
+ timeout: 300000, // 5 min timeout
196
+ stdio: ['pipe', 'pipe', 'pipe'],
197
+ shell: '/bin/sh'
198
+ });
199
+ return { success: true, stdout: output || '', stderr: '' };
200
+ }
201
+ catch (error) {
202
+ // execSync throws on non-zero exit, but we still want the output
203
+ const stdout = error.stdout?.toString() || '';
204
+ const stderr = error.stderr?.toString() || error.message || '';
205
+ // If command produced output, consider it partially successful
206
+ return { success: error.status === 0, stdout, stderr };
207
+ }
208
+ }
209
+ // ============================================
210
+ // PHASE 1: SERVER SETUP (Docker & Services)
211
+ // ============================================
212
+ async function serverSetup(options) {
213
+ log.title('🔧 Phase 1: Server Setup');
214
+ log.info('Docker installation, image pull, base services');
215
+ log.blank();
216
+ // Check if running locally on server (no host specified and no SSH hosts available)
217
+ const isLocal = options.local || (!options.host && (await listSSHHosts()).length === 0);
218
+ let runCmd;
219
+ if (isLocal) {
220
+ log.info('Running in LOCAL mode (directly on this server)');
221
+ runCmd = runLocal;
222
+ }
223
+ else {
224
+ const host = await getHost(options.host);
225
+ if (!host)
226
+ return;
227
+ log.info(`Target: ${host.name} (${host.user}@${host.hostname})`);
228
+ runCmd = (cmd, label) => runSSHLive(host, cmd, label);
229
+ }
230
+ log.blank();
231
+ const shouldContinue = await confirm('Start server setup?', true);
232
+ if (!shouldContinue) {
233
+ log.warn('Cancelled');
234
+ return;
235
+ }
236
+ const startTime = Date.now();
237
+ // Step 1: System check
238
+ log.step(1, 5, 'Checking system...');
239
+ const testResult = await runCmd('uname -a', 'System info:');
240
+ if (!testResult.success) {
241
+ log.error('❌ System check failed');
242
+ return;
243
+ }
244
+ log.success('✅ System OK');
245
+ // Step 2: System Update
246
+ log.blank();
247
+ log.step(2, 5, 'Updating system packages...');
248
+ const updateResult = await runCmd('export DEBIAN_FRONTEND=noninteractive ; apt-get update ; apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade', 'Updating...');
249
+ if (!updateResult.success) {
250
+ log.error('❌ System update failed');
251
+ return;
252
+ }
253
+ log.success('✅ System updated');
254
+ // Step 3: Install Dependencies
255
+ log.blank();
256
+ log.step(3, 5, 'Installing dependencies...');
257
+ const depsResult = await runCmd('export DEBIAN_FRONTEND=noninteractive ; apt-get install -y curl git tree openssl apt-transport-https ca-certificates software-properties-common', 'Installing...');
258
+ if (!depsResult.success) {
259
+ log.error('❌ Dependencies installation failed');
260
+ return;
261
+ }
262
+ log.success('✅ Dependencies installed');
263
+ // Step 4: Install Docker
264
+ if (!options.skipDocker) {
265
+ log.blank();
266
+ log.step(4, 5, 'Installing Docker...');
267
+ // Check if Docker exists
268
+ const dockerCheck = await runCmd('docker --version 2>/dev/null || echo NOT_INSTALLED');
269
+ if (dockerCheck.success && !dockerCheck.stdout.includes('NOT_INSTALLED')) {
270
+ log.info('Docker already installed');
271
+ }
272
+ else {
273
+ const dockerResult = await runCmd('curl -fsSL https://get.docker.com | sh ; systemctl enable docker ; systemctl start docker', 'Installing Docker...');
274
+ if (!dockerResult.success) {
275
+ log.error('❌ Docker installation failed');
276
+ return;
277
+ }
278
+ }
279
+ log.success('✅ Docker ready');
280
+ }
281
+ // Step 5: Create networks, pull images, start base services
282
+ log.blank();
283
+ log.step(5, 5, 'Setting up Docker environment...');
284
+ // Create networks
285
+ log.info('Creating Docker networks...');
286
+ await runCmd('docker network create vucore_global 2>/dev/null || true', 'Creating vucore_global...');
287
+ await runCmd('docker network create vucore_proxy 2>/dev/null || true', 'Creating vucore_proxy...');
288
+ // Verify networks
289
+ const netCheck = await runCmd('docker network ls --format "{{.Name}}" | grep vucore || true');
290
+ log.info(` Networks: ${netCheck.stdout.trim().replace(/\n/g, ', ') || 'creating...'}`);
291
+ if (!netCheck.stdout.includes('vucore_global')) {
292
+ await runCmd('docker network create vucore_global');
293
+ }
294
+ if (!netCheck.stdout.includes('vucore_proxy')) {
295
+ await runCmd('docker network create vucore_proxy');
296
+ }
297
+ log.success('✅ Docker networks ready');
298
+ // Create directories
299
+ log.info('Creating directories...');
300
+ await runCmd('mkdir -p /home/cores /home/services/nginx/conf.d /home/services/nginx/ssl /home/services/postgres/data /home/services/postgres/backups /home/services/redis/data /home/services/scripts/addons');
301
+ // Pull images
302
+ log.blank();
303
+ log.info('Pulling Docker images (this may take a few minutes)...');
304
+ const registry = getDockerRegistry();
305
+ await runCmd(`docker pull ${registry}/vucore-nuxt:latest`, 'Pulling vucore-nuxt...');
306
+ await runCmd(`docker pull ${registry}/vucore-php:latest`, 'Pulling vucore-php...');
307
+ await runCmd('docker pull postgres:16-alpine', 'Pulling postgres...');
308
+ await runCmd('docker pull redis:7-alpine', 'Pulling redis...');
309
+ await runCmd('docker pull nginx:alpine', 'Pulling nginx...');
310
+ log.success('✅ Docker images pulled');
311
+ // Summary
312
+ const elapsed = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
313
+ log.blank();
314
+ log.box('✅ Phase 1 Complete', [
315
+ `Mode: ${isLocal ? 'LOCAL' : 'REMOTE'}`,
316
+ `Time: ${elapsed} minutes`,
317
+ '',
318
+ 'Next: vu server deploy --core core1',
319
+ ]);
320
+ process.exit(0);
321
+ }
322
+ // ============================================
323
+ // PHASE 2: SERVER DEPLOY (ciCore Files)
324
+ // ============================================
325
+ async function serverDeploy(options) {
326
+ log.title('📦 Phase 2: Deploy VuCore');
327
+ log.info('Upload templates, configs, shared files');
328
+ log.blank();
329
+ // Check if running locally
330
+ const isLocal = options.local || (!options.host && (await listSSHHosts()).length === 0);
331
+ let host = null;
332
+ let runCmd;
333
+ if (isLocal) {
334
+ log.info('Running in LOCAL mode (directly on this server)');
335
+ runCmd = runLocal;
336
+ }
337
+ else {
338
+ host = await getHost(options.host);
339
+ if (!host)
340
+ return;
341
+ log.info(`Target: ${host.name} (${host.user}@${host.hostname})`);
342
+ runCmd = (cmd, label) => runSSHLive(host, cmd, label);
343
+ }
344
+ const coreName = options.core || await input('Core name:', 'core1');
345
+ log.info(`Core: ${coreName}`);
346
+ log.blank();
347
+ const shouldContinue = await confirm('Start deployment?', true);
348
+ if (!shouldContinue) {
349
+ log.warn('Cancelled');
350
+ return;
351
+ }
352
+ const startTime = Date.now();
353
+ // Step 1: Test connection
354
+ log.step(1, 4, isLocal ? 'Checking system...' : 'Testing SSH connection...');
355
+ const testResult = await runCmd('uname -a');
356
+ if (!testResult.success) {
357
+ log.error(isLocal ? '❌ System check failed' : '❌ SSH connection failed');
358
+ return;
359
+ }
360
+ log.success(isLocal ? '✅ System OK' : '✅ SSH connected');
361
+ // Step 2: Prepare templates
362
+ log.blank();
363
+ log.step(2, 4, 'Preparing template files...');
364
+ // Templates path differs between local (server) and remote (dev machine) modes
365
+ // Local: /opt/vucore-cli/templates/server (CLI installed on server)
366
+ // Remote: {dev_root}/cli/templates/server (dev machine)
367
+ let templatesPath;
368
+ if (isLocal) {
369
+ templatesPath = '/opt/vucore-cli/templates/server';
370
+ }
371
+ else {
372
+ templatesPath = path.join(paths.dev.root, 'cli', 'templates', 'server');
373
+ }
374
+ if (!await fs.pathExists(templatesPath)) {
375
+ log.error(`Templates path not found: ${templatesPath}`);
376
+ return;
377
+ }
378
+ const tempDir = isLocal ? '/tmp/vucore-deploy' : path.join(paths.dev.root, '.tmp-server-deploy');
379
+ await fs.ensureDir(tempDir);
380
+ // Copy templates
381
+ await fs.copy(path.join(templatesPath, 'services'), path.join(tempDir, 'services'));
382
+ await fs.copy(path.join(templatesPath, 'cores', 'core-template'), path.join(tempDir, 'core'));
383
+ // Generate passwords and process .env files
384
+ const postgresPassword = generateRandomString(48);
385
+ const redisPassword = generateRandomString(48);
386
+ const dbPassword = generateRandomString(48);
387
+ const servicesEnvPath = path.join(tempDir, 'services', '.env');
388
+ const coreEnvPath = path.join(tempDir, 'core', '.env');
389
+ let servicesEnv = await fs.readFile(servicesEnvPath, 'utf-8');
390
+ servicesEnv = servicesEnv
391
+ .replace(/\{\{POSTGRES_USER\}\}/g, 'vucore_admin')
392
+ .replace(/\{\{POSTGRES_PASSWORD\}\}/g, postgresPassword)
393
+ .replace(/\{\{POSTGRES_DB\}\}/g, 'vucore_global')
394
+ .replace(/\{\{REDIS_PASSWORD\}\}/g, redisPassword)
395
+ // NUXT variables - will be properly set by 'vu server domain' command
396
+ .replace(/\{\{NUXT_PUBLIC_API_BASE\}\}/g, 'https://api.localhost')
397
+ .replace(/\{\{NUXT_PUBLIC_WS_URL\}\}/g, 'wss://localhost:2346')
398
+ .replace(/\{\{NUXT_PUBLIC_HOST\}\}/g, 'localhost');
399
+ await fs.writeFile(servicesEnvPath, servicesEnv);
400
+ let coreEnv = await fs.readFile(coreEnvPath, 'utf-8');
401
+ coreEnv = coreEnv
402
+ .replace(/\{\{CORE_NAME\}\}/g, coreName)
403
+ .replace(/\{\{DOMAIN\}\}/g, 'localhost')
404
+ .replace(/\{\{DB_NAME\}\}/g, `vucore_${coreName}`)
405
+ .replace(/\{\{DB_USER\}\}/g, 'vucore_admin')
406
+ .replace(/\{\{DB_PASS\}\}/g, dbPassword)
407
+ .replace(/\{\{DB_GATEWAY\}\}/g, 'local');
408
+ await fs.writeFile(coreEnvPath, coreEnv);
409
+ log.success('✅ Templates prepared');
410
+ // Step 3: Upload/Copy files
411
+ log.blank();
412
+ log.step(3, 4, isLocal ? 'Copying files...' : 'Uploading files to server...');
413
+ if (isLocal) {
414
+ // LOCAL MODE: Direct file copy
415
+ log.info('📤 Copying /home/services...');
416
+ await runCmd('rm -rf /home/services/* 2>/dev/null ; mkdir -p /home/services');
417
+ await fs.copy(path.join(tempDir, 'services'), '/home/services');
418
+ log.success('✅ Services copied');
419
+ log.info(`📤 Copying /home/cores/${coreName}...`);
420
+ await runCmd(`rm -rf /home/cores/${coreName} ; mkdir -p /home/cores/${coreName}`);
421
+ await fs.copy(path.join(tempDir, 'core'), `/home/cores/${coreName}`);
422
+ log.success('✅ Core copied');
423
+ }
424
+ else {
425
+ // REMOTE MODE: SCP upload
426
+ log.info('📤 Uploading /home/services...');
427
+ await runCmd('rm -rf /home/services/* 2>/dev/null ; mkdir -p /home/services');
428
+ const servicesResult = await runSCP(host, path.join(tempDir, 'services'), '/home/', true);
429
+ if (!servicesResult.success) {
430
+ log.error('❌ Failed to upload services');
431
+ await fs.remove(tempDir);
432
+ return;
433
+ }
434
+ log.success('✅ Services uploaded');
435
+ log.info(`📤 Uploading /home/cores/${coreName}...`);
436
+ await runCmd(`rm -rf /home/cores/${coreName}`);
437
+ const coreSourcePath = path.join(tempDir, 'core');
438
+ const coreRenamedPath = path.join(tempDir, coreName);
439
+ await fs.rename(coreSourcePath, coreRenamedPath);
440
+ const coreResult = await runSCP(host, coreRenamedPath, '/home/cores/', true);
441
+ if (!coreResult.success) {
442
+ log.error('❌ Failed to upload core');
443
+ await fs.remove(tempDir);
444
+ return;
445
+ }
446
+ log.success('✅ Core uploaded');
447
+ }
448
+ // Verify files exist
449
+ log.info('🔍 Verifying file structure...');
450
+ const verifyFiles = [
451
+ `/home/cores/${coreName}/.env`,
452
+ `/home/cores/${coreName}/shared`,
453
+ `/home/cores/${coreName}/addons`,
454
+ '/home/services/.env',
455
+ '/home/services/docker-core-php.yml',
456
+ '/home/services/docker-core-nuxt.yml',
457
+ '/home/services/docker-server-nginx.yml',
458
+ '/home/services/nginx/conf.d',
459
+ '/home/services/nginx/ssl',
460
+ ];
461
+ let verifyErrors = [];
462
+ for (const file of verifyFiles) {
463
+ const check = await runCmd(`test -e ${file} && echo "OK" || echo "MISSING"`);
464
+ if (check.stdout.includes('MISSING')) {
465
+ verifyErrors.push(file);
466
+ log.error(` ❌ Missing: ${file}`);
467
+ }
468
+ }
469
+ if (verifyErrors.length > 0) {
470
+ log.error('❌ File verification failed! Missing files:');
471
+ verifyErrors.forEach(f => log.error(` - ${f}`));
472
+ await fs.remove(tempDir);
473
+ return;
474
+ }
475
+ log.success('✅ Files verified');
476
+ // Cleanup temp
477
+ await fs.remove(tempDir);
478
+ // Step 4: Start services
479
+ log.blank();
480
+ log.step(4, 4, 'Starting services...');
481
+ // Set permissions
482
+ log.info('Setting permissions...');
483
+ await runCmd('chmod +x /home/services/scripts/*.sh /home/services/scripts/addons/*.sh 2>/dev/null || true');
484
+ await runCmd('chmod -R 755 /home/cores /home/services');
485
+ await runCmd(`chmod 666 /home/cores/${coreName}/.env`);
486
+ await runCmd('chmod -R 777 /home/services/postgres/data /home/services/redis/data');
487
+ await runCmd('chown -R 999:999 /home/services/postgres/data 2>/dev/null || true');
488
+ await runCmd('chown -R 999:999 /home/services/redis/data 2>/dev/null || true');
489
+ // Start PostgreSQL
490
+ log.info('▶ Starting PostgreSQL...');
491
+ await runCmd('cd /home/services ; docker compose -f docker-postgresql.yml up -d');
492
+ // Start Redis
493
+ log.info('▶ Starting Redis...');
494
+ await runCmd('cd /home/services ; docker compose -f docker-redis.yml up -d');
495
+ // Wait for DB
496
+ log.info('⏳ Waiting for database (5s)...');
497
+ await runCmd('sleep 5');
498
+ // Start PHP
499
+ log.info('▶ Starting PHP...');
500
+ await runCmd('cd /home/services ; docker compose -f docker-core-php.yml up -d');
501
+ // Create storage directories
502
+ log.info('📁 Creating storage directories...');
503
+ await runCmd('sleep 2');
504
+ await runCmd('docker exec -u root cicore_php mkdir -p /var/www/storage/cache /var/www/storage/logs /var/www/storage/sessions /var/www/storage/uploads');
505
+ await runCmd('docker exec -u root cicore_php chmod -R 777 /var/www/storage');
506
+ await runCmd('docker exec -u root cicore_php chown -R www-data:www-data /var/www/storage');
507
+ // Start Nuxt
508
+ log.info('▶ Starting Nuxt...');
509
+ await runCmd('cd /home/services ; docker compose -f docker-core-nuxt.yml up -d');
510
+ // Start Nginx
511
+ log.info('▶ Starting Nginx...');
512
+ await runCmd('cd /home/services ; docker compose -f docker-server-nginx.yml up -d');
513
+ // Wait for containers
514
+ log.info('⏳ Waiting for containers (3s)...');
515
+ await runCmd('sleep 3');
516
+ // Show status
517
+ log.blank();
518
+ log.info('📊 Container Status:');
519
+ const statusResult = await runCmd('docker ps --format "table {{.Names}}\t{{.Status}}"');
520
+ if (statusResult.success) {
521
+ console.log(statusResult.stdout);
522
+ }
523
+ // Summary
524
+ const elapsed = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
525
+ log.blank();
526
+ log.box('✅ Phase 2 Complete', [
527
+ `Mode: ${isLocal ? 'LOCAL' : host.hostname}`,
528
+ `Core: ${coreName}`,
529
+ `Time: ${elapsed} minutes`,
530
+ '',
531
+ `Next: vu server domain ${isLocal ? '--local' : '--host ' + host.name} --core ${coreName} --domain yourdomain.com`,
532
+ ]);
533
+ process.exit(0);
534
+ }
535
+ // ============================================
536
+ // PHASE 3: SERVER DOMAIN (Nginx Config)
537
+ // ============================================
538
+ async function serverDomain(options) {
539
+ log.title('🌐 Phase 3: Domain Setup');
540
+ log.info('Configure nginx for domain');
541
+ log.blank();
542
+ // Check if running locally
543
+ const isLocal = options.local || (!options.host && (await listSSHHosts()).length === 0);
544
+ let runCmd;
545
+ let host = null;
546
+ if (isLocal) {
547
+ log.info('Running in LOCAL mode');
548
+ runCmd = runLocal;
549
+ }
550
+ else {
551
+ host = await getHost(options.host);
552
+ if (!host)
553
+ return;
554
+ log.info(`Target: ${host.name} (${host.user}@${host.hostname})`);
555
+ runCmd = (cmd, label) => runSSHLive(host, cmd, label);
556
+ }
557
+ const coreName = options.core || await input('Core name:', 'core1');
558
+ const domain = options.domain || await input('Domain (e.g., cicore.com.tr):');
559
+ if (!domain) {
560
+ log.error('Domain is required');
561
+ return;
562
+ }
563
+ log.info(`Core: ${coreName}`);
564
+ log.info(`Domain: ${domain}`);
565
+ log.blank();
566
+ const shouldContinue = await confirm('Configure domain?', true);
567
+ if (!shouldContinue) {
568
+ log.warn('Cancelled');
569
+ return;
570
+ }
571
+ // Step 1: Update core .env with domain and NUXT settings
572
+ log.step(1, 4, 'Updating core configuration...');
573
+ await runCmd(`sed -i 's|DOMAIN=.*|DOMAIN=${domain}|' /home/cores/${coreName}/.env`);
574
+ await runCmd(`sed -i 's|NUXT_PUBLIC_API_BASE=.*|NUXT_PUBLIC_API_BASE=https://api.${domain}|' /home/cores/${coreName}/.env`);
575
+ await runCmd(`sed -i 's|NUXT_PUBLIC_HOST=.*|NUXT_PUBLIC_HOST=${domain}|' /home/cores/${coreName}/.env`);
576
+ await runCmd(`sed -i 's|NUXT_PUBLIC_WS_URL=.*|NUXT_PUBLIC_WS_URL=wss://${domain}:2346|' /home/cores/${coreName}/.env`);
577
+ await runCmd(`sed -i 's|CORS_ALLOWED_ORIGINS=.*|CORS_ALLOWED_ORIGINS=https://${domain},https://www.${domain},https://api.${domain}|' /home/cores/${coreName}/.env`);
578
+ log.success('✅ Core .env updated');
579
+ // Step 1b: Update services .env for Nuxt container
580
+ log.info('Updating services .env for Nuxt...');
581
+ await runCmd(`sed -i 's|NUXT_PUBLIC_API_BASE=.*|NUXT_PUBLIC_API_BASE=https://api.${domain}|' /home/services/.env`);
582
+ await runCmd(`sed -i 's|NUXT_PUBLIC_HOST=.*|NUXT_PUBLIC_HOST=${domain}|' /home/services/.env`);
583
+ await runCmd(`sed -i 's|NUXT_PUBLIC_WS_URL=.*|NUXT_PUBLIC_WS_URL=wss://${domain}:2346|' /home/services/.env`);
584
+ // Recreate Nuxt container to pick up new env vars
585
+ log.info('Restarting Nuxt container with new settings...');
586
+ await runCmd('cd /home/services && docker compose -f docker-core-nuxt.yml down && docker compose -f docker-core-nuxt.yml up -d');
587
+ log.success('✅ Nuxt container restarted');
588
+ // Step 2: Generate nginx config (HTTP only - SSL added in Phase 4)
589
+ log.blank();
590
+ log.step(2, 4, 'Generating nginx configuration...');
591
+ const configName = domain.replace(/\./g, '_');
592
+ const escapedDomain = domain.replace(/\./g, '\\\\.');
593
+ const certName = domain.replace(/\./g, '_');
594
+ // Generate full nginx config with API subdomain, web server, and tenant subdomains
595
+ const nginxConfig = `# ciCore Domain Config - ${domain} -> ${coreName}
596
+ # Generated by: vu server domain
597
+ # API: api.${domain} -> PHP backend
598
+ # Web: ${domain}, www.${domain} -> Nuxt frontend
599
+ # Tenants: *.${domain} (except api) -> Nuxt frontend
600
+
601
+ # ======================================================
602
+ # 1. API SUBDOMAIN SERVER (api.${domain})
603
+ # ======================================================
604
+ server {
605
+ listen 80;
606
+ server_name api.${domain};
607
+
608
+ client_max_body_size 25m;
609
+
610
+ location / {
611
+ # CORS Preflight
612
+ if (\$request_method = 'OPTIONS') {
613
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
614
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
615
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Cache-Control, Accept' always;
616
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
617
+ add_header 'Access-Control-Max-Age' '86400' always;
618
+ add_header 'Content-Type' 'text/plain; charset=utf-8' always;
619
+ add_header 'Content-Length' '0' always;
620
+ return 204;
621
+ }
622
+
623
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
624
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
625
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Accept' always;
626
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
627
+ add_header 'Access-Control-Expose-Headers' 'X-CSRF-Token' always;
628
+
629
+ fastcgi_pass php_backend;
630
+ fastcgi_param SCRIPT_FILENAME /var/www/public/index.php;
631
+ fastcgi_param CORE_PATH /var/www/cores/${coreName};
632
+ include fastcgi_params;
633
+ fastcgi_param REQUEST_URI \$request_uri;
634
+ fastcgi_param SCRIPT_NAME /index.php;
635
+ fastcgi_param HTTP_HOST \$host;
636
+ fastcgi_param HTTP_TENANT_HOST \$host;
637
+
638
+ fastcgi_connect_timeout 300s;
639
+ fastcgi_send_timeout 300s;
640
+ fastcgi_read_timeout 300s;
641
+ fastcgi_buffer_size 128k;
642
+ fastcgi_buffers 4 256k;
643
+ fastcgi_busy_buffers_size 256k;
644
+ }
645
+ }
646
+
647
+ # ======================================================
648
+ # 2. TENANT SUBDOMAIN SERVER (*.${domain} except api)
649
+ # ======================================================
650
+ server {
651
+ listen 80;
652
+ server_name ~^(?!api\\.)(?<subdomain>[^.]+)\\.${escapedDomain}\$;
653
+
654
+ client_max_body_size 25m;
655
+
656
+ # Health Check
657
+ location /health {
658
+ access_log off;
659
+ return 200 "healthy\\n";
660
+ add_header Content-Type text/plain;
661
+ }
662
+
663
+ # Shared API Files
664
+ location ^~ /shared/ {
665
+ alias /home/cores/${coreName}/shared/;
666
+ autoindex off;
667
+ types { application/javascript js mjs; application/json json; }
668
+ default_type application/javascript;
669
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
670
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
671
+ add_header 'Cache-Control' 'public, max-age=3600' always;
672
+ try_files \$uri =404;
673
+ }
674
+
675
+ # Addon ESM Files
676
+ location ^~ /cores/ {
677
+ alias /home/cores/;
678
+ autoindex off;
679
+ types { application/javascript js mjs; application/json json; text/css css; }
680
+ default_type application/javascript;
681
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
682
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
683
+ try_files \$uri =404;
684
+ }
685
+
686
+ # Frontend (Nuxt SSR)
687
+ location / {
688
+ proxy_pass http://nuxt_frontend;
689
+ proxy_set_header Host \$host;
690
+ proxy_set_header X-Real-IP \$remote_addr;
691
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
692
+ proxy_set_header X-Forwarded-Proto \$scheme;
693
+ proxy_set_header X-Core-Path /var/www/cores/${coreName};
694
+ proxy_set_header X-Tenant-Host \$host;
695
+ proxy_set_header X-Tenant-Subdomain \$subdomain;
696
+ proxy_http_version 1.1;
697
+ proxy_set_header Upgrade \$http_upgrade;
698
+ proxy_set_header Connection \$connection_upgrade;
699
+ proxy_buffering off;
700
+ }
701
+ }
702
+
703
+ # ======================================================
704
+ # 3. MAIN WEB SERVER (${domain}, www.${domain})
705
+ # ======================================================
706
+ server {
707
+ listen 80;
708
+ server_name ${domain} www.${domain};
709
+
710
+ client_max_body_size 25m;
711
+
712
+ # Health Check
713
+ location /health {
714
+ access_log off;
715
+ return 200 "healthy\\n";
716
+ add_header Content-Type text/plain;
717
+ }
718
+
719
+ # Shared API Files
720
+ location ^~ /shared/ {
721
+ alias /home/cores/${coreName}/shared/;
722
+ autoindex off;
723
+ types { application/javascript js mjs; application/json json; }
724
+ default_type application/javascript;
725
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
726
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
727
+ add_header 'Cache-Control' 'public, max-age=3600' always;
728
+ try_files \$uri =404;
729
+ }
730
+
731
+ # Addon ESM Files
732
+ location ^~ /cores/ {
733
+ alias /home/cores/;
734
+ autoindex off;
735
+ types { application/javascript js mjs; application/json json; text/css css; }
736
+ default_type application/javascript;
737
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
738
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
739
+ try_files \$uri =404;
740
+ }
741
+
742
+ # API Endpoints (on web domain)
743
+ location ~ ^/(api|v1|v2|auth|system|system-settings|upload|addons|core_addons|core|analyzer|summary|metrics|jobs|worker|setup|tenant-manager) {
744
+ # CORS Preflight
745
+ if (\$request_method = 'OPTIONS') {
746
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
747
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
748
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Cache-Control, Accept' always;
749
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
750
+ add_header 'Access-Control-Max-Age' '86400' always;
751
+ add_header 'Content-Type' 'text/plain; charset=utf-8' always;
752
+ add_header 'Content-Length' '0' always;
753
+ return 204;
754
+ }
755
+
756
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
757
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
758
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Accept' always;
759
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
760
+ add_header 'Access-Control-Expose-Headers' 'X-CSRF-Token' always;
761
+
762
+ fastcgi_pass php_backend;
763
+ fastcgi_param SCRIPT_FILENAME /var/www/public/index.php;
764
+ fastcgi_param CORE_PATH /var/www/cores/${coreName};
765
+ include fastcgi_params;
766
+ fastcgi_param REQUEST_URI \$request_uri;
767
+ fastcgi_param SCRIPT_NAME /index.php;
768
+ fastcgi_param HTTP_HOST \$host;
769
+ fastcgi_param HTTP_TENANT_HOST \$host;
770
+
771
+ fastcgi_connect_timeout 300s;
772
+ fastcgi_send_timeout 300s;
773
+ fastcgi_read_timeout 300s;
774
+ fastcgi_buffer_size 128k;
775
+ fastcgi_buffers 4 256k;
776
+ fastcgi_busy_buffers_size 256k;
777
+ }
778
+
779
+ # Frontend (Nuxt SSR)
780
+ location / {
781
+ proxy_pass http://nuxt_frontend;
782
+ proxy_set_header Host \$host;
783
+ proxy_set_header X-Real-IP \$remote_addr;
784
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
785
+ proxy_set_header X-Forwarded-Proto \$scheme;
786
+ proxy_set_header X-Core-Path /var/www/cores/${coreName};
787
+ proxy_set_header X-Tenant-Host \$host;
788
+ proxy_http_version 1.1;
789
+ proxy_set_header Upgrade \$http_upgrade;
790
+ proxy_set_header Connection \$connection_upgrade;
791
+ proxy_buffering off;
792
+ }
793
+ }`;
794
+ // Write nginx config
795
+ await runCmd(`cat > /home/services/nginx/conf.d/${configName}.conf << 'NGINX_EOF'
796
+ ${nginxConfig}
797
+ NGINX_EOF`);
798
+ log.success('✅ Nginx config created');
799
+ log.info(` - API: api.${domain} -> PHP`);
800
+ log.info(` - Web: ${domain}, www.${domain} -> Nuxt`);
801
+ log.info(` - Tenants: *.${domain} -> Nuxt`);
802
+ // Step 3: Test and reload nginx
803
+ log.blank();
804
+ log.step(3, 4, 'Testing nginx configuration...');
805
+ const nginxTest = await runCmd('docker exec cicore_global_nginx nginx -t');
806
+ if (!nginxTest.success) {
807
+ log.error('❌ Nginx config test failed');
808
+ return;
809
+ }
810
+ log.success('✅ Nginx config valid');
811
+ // Step 4: Reload nginx
812
+ log.step(4, 4, 'Reloading nginx...');
813
+ await runCmd('docker exec cicore_global_nginx nginx -s reload');
814
+ log.success('✅ Nginx reloaded');
815
+ log.blank();
816
+ log.box('✅ Phase 3 Complete', [
817
+ `Domain: ${domain}`,
818
+ `Core: ${coreName}`,
819
+ '',
820
+ `Next: vu server ssl --domain ${domain}`,
821
+ ]);
822
+ }
823
+ // ============================================
824
+ // PHASE 4: SERVER SSL (Certificate)
825
+ // ============================================
826
+ async function serverSSL(options) {
827
+ log.title('🔒 Phase 4: SSL Setup');
828
+ log.info('Configure SSL certificate');
829
+ log.blank();
830
+ // Check if running locally
831
+ const isLocal = options.local || (!options.host && (await listSSHHosts()).length === 0);
832
+ let runCmd;
833
+ let host = null;
834
+ if (isLocal) {
835
+ log.info('Running in LOCAL mode');
836
+ runCmd = runLocal;
837
+ }
838
+ else {
839
+ host = await getHost(options.host);
840
+ if (!host)
841
+ return;
842
+ log.info(`Target: ${host.name} (${host.user}@${host.hostname})`);
843
+ runCmd = (cmd, label) => runSSHLive(host, cmd, label);
844
+ }
845
+ const domain = options.domain || await input('Domain:');
846
+ if (!domain) {
847
+ log.error('Domain is required');
848
+ return;
849
+ }
850
+ const useCloudflare = options.letsencrypt ? false : (options.cloudflare || await confirm('Use Cloudflare Origin Certificate?', true));
851
+ log.info(`Domain: ${domain}`);
852
+ log.info(`Method: ${useCloudflare ? 'Cloudflare' : "Let's Encrypt"}`);
853
+ log.blank();
854
+ const shouldContinue = await confirm('Setup SSL?', true);
855
+ if (!shouldContinue) {
856
+ log.warn('Cancelled');
857
+ return;
858
+ }
859
+ const certName = domain.split('.')[0];
860
+ const certPath = `/home/services/nginx/ssl/${certName}.crt`;
861
+ const keyPath = `/home/services/nginx/ssl/${certName}.key`;
862
+ // Check if SSL exists
863
+ const sslCheck = await runCmd(`test -f ${certPath} && echo "EXISTS" || echo "MISSING"`);
864
+ if (sslCheck.stdout.includes('EXISTS')) {
865
+ log.info(`✅ SSL certificate already exists: ${certPath}`);
866
+ const overwrite = await confirm('Overwrite existing certificate?', false);
867
+ if (!overwrite) {
868
+ log.warn('Cancelled');
869
+ return;
870
+ }
871
+ }
872
+ if (useCloudflare) {
873
+ // Cloudflare Origin Certificate
874
+ log.step(1, 2, 'Creating Cloudflare Origin Certificate...');
875
+ const cfToken = await getCloudflareToken();
876
+ if (!cfToken) {
877
+ log.error('Cloudflare token not configured. Run: vu cloudflare auth');
878
+ return;
879
+ }
880
+ // Get server IP for Cloudflare
881
+ const serverIP = host?.hostname || '127.0.0.1';
882
+ const cfResult = await setupDomainForVuCore(domain, serverIP, {
883
+ createWildcard: true,
884
+ createApiSubdomain: true,
885
+ sslMode: 'strict',
886
+ });
887
+ if (!cfResult.success || !cfResult.certificate) {
888
+ log.error('❌ Failed to create Cloudflare certificate');
889
+ log.error(cfResult.error || 'Unknown error');
890
+ return;
891
+ }
892
+ // Upload certificate
893
+ log.blank();
894
+ log.step(2, 2, 'Uploading certificate to server...');
895
+ await runCmd(`mkdir -p /home/services/nginx/ssl`);
896
+ await runCmd(`cat > ${certPath} << 'CERT_EOF'
897
+ ${cfResult.certificate.certificate}
898
+ CERT_EOF`);
899
+ await runCmd(`cat > ${keyPath} << 'KEY_EOF'
900
+ ${cfResult.certificate.private_key}
901
+ KEY_EOF`);
902
+ await runCmd(`chmod 600 ${certPath} ${keyPath}`);
903
+ log.success('✅ Cloudflare SSL configured');
904
+ log.kv('Certificate', certPath);
905
+ log.kv('Expires', cfResult.certificate.expires_on);
906
+ }
907
+ else {
908
+ // Let's Encrypt
909
+ const email = options.email || await input('Email for Let\'s Encrypt:', 'admin@' + domain);
910
+ log.step(1, 2, 'Obtaining Let\'s Encrypt certificate...');
911
+ // First install certbot if not present
912
+ await runCmd('which certbot || apt-get install -y certbot', 'Installing certbot...');
913
+ // Stop nginx temporarily for standalone mode
914
+ await runCmd('docker stop cicore_global_nginx 2>/dev/null || true', 'Stopping nginx...');
915
+ const certbotResult = await runCmd(`certbot certonly --standalone -d ${domain} -d www.${domain} --email ${email} --agree-tos --non-interactive`, 'Getting certificate...');
916
+ // Restart nginx
917
+ await runCmd('docker start cicore_global_nginx 2>/dev/null || true', 'Starting nginx...');
918
+ if (!certbotResult.success) {
919
+ log.error('❌ Let\'s Encrypt certificate failed');
920
+ return;
921
+ }
922
+ log.blank();
923
+ log.step(2, 2, 'Copying certificate...');
924
+ await runCmd(`mkdir -p /home/services/nginx/ssl`);
925
+ await runCmd(`cp /etc/letsencrypt/live/${domain}/fullchain.pem ${certPath}`);
926
+ await runCmd(`cp /etc/letsencrypt/live/${domain}/privkey.pem ${keyPath}`);
927
+ log.success('✅ Let\'s Encrypt SSL configured');
928
+ }
929
+ // Update nginx config for SSL - get coreName from existing config
930
+ log.blank();
931
+ log.info('Updating nginx for HTTPS...');
932
+ const configName = domain.replace(/\./g, '_');
933
+ const escapedDomain = domain.replace(/\./g, '\\\\.');
934
+ // Try to detect coreName from existing config
935
+ const coreDetect = await runCmd(`grep -oP 'CORE_PATH /var/www/cores/\\K[^;]+' /home/services/nginx/conf.d/${configName}.conf 2>/dev/null | head -1`);
936
+ const coreName = coreDetect.stdout.trim() || 'core1';
937
+ // Read CDN URL from core's .env file (dynamic per-core CDN support)
938
+ const cdnUrlResult = await runCmd(`grep -oP 'NUXT_PUBLIC_CORE_CDN_URL=\\K.*' /home/cores/${coreName}/.env 2>/dev/null | head -1`);
939
+ const cdnUrl = cdnUrlResult.stdout.trim() || 'https://cdn.cicore.com.tr/CoreUI/latest';
940
+ log.info(`CDN URL: ${cdnUrl}`);
941
+ // Generate comprehensive HTTPS nginx config with API subdomain, wildcard SSL, and CORS
942
+ const sslNginxConfig = `# ciCore Domain Config - ${domain} -> ${coreName}
943
+ # CDN URL: ${cdnUrl}
944
+ # Generated by: vu server ssl
945
+ # API: api.${domain} -> PHP backend (HTTPS)
946
+ # Web: ${domain}, www.${domain} -> Nuxt frontend (HTTPS)
947
+ # Tenants: *.${domain} (except api) -> Nuxt frontend (HTTPS)
948
+ # Wildcard SSL: Cloudflare Origin Certificate covers *.${domain}
949
+
950
+ # ======================================================
951
+ # 1. HTTP -> HTTPS Redirect (All domains)
952
+ # ======================================================
953
+ server {
954
+ listen 80;
955
+ server_name ${domain} *.${domain};
956
+
957
+ location /health {
958
+ access_log off;
959
+ return 200 "healthy\\n";
960
+ add_header Content-Type text/plain;
961
+ }
962
+
963
+ location / {
964
+ return 301 https://\$host\$request_uri;
965
+ }
966
+ }
967
+
968
+ # ======================================================
969
+ # 2. API SUBDOMAIN SERVER (api.${domain}) - HTTPS
970
+ # ======================================================
971
+ server {
972
+ listen 443 ssl;
973
+ http2 on;
974
+ server_name api.${domain};
975
+
976
+ ssl_certificate /etc/nginx/ssl/${certName}.crt;
977
+ ssl_certificate_key /etc/nginx/ssl/${certName}.key;
978
+ ssl_protocols TLSv1.2 TLSv1.3;
979
+ ssl_prefer_server_ciphers on;
980
+ ssl_session_cache shared:SSL:10m;
981
+ ssl_session_timeout 10m;
982
+
983
+ client_max_body_size 25m;
984
+ server_tokens off;
985
+
986
+ # Security Headers
987
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
988
+ add_header X-Content-Type-Options "nosniff" always;
989
+ add_header X-Frame-Options "SAMEORIGIN" always;
990
+ add_header X-XSS-Protection "1; mode=block" always;
991
+
992
+ location / {
993
+ # CORS Preflight
994
+ if (\$request_method = 'OPTIONS') {
995
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
996
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
997
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Cache-Control, Accept' always;
998
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
999
+ add_header 'Access-Control-Max-Age' '86400' always;
1000
+ add_header 'Content-Type' 'text/plain; charset=utf-8' always;
1001
+ add_header 'Content-Length' '0' always;
1002
+ return 204;
1003
+ }
1004
+
1005
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1006
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
1007
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Accept' always;
1008
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1009
+ add_header 'Access-Control-Expose-Headers' 'X-CSRF-Token' always;
1010
+
1011
+ fastcgi_pass php_backend;
1012
+ fastcgi_param SCRIPT_FILENAME /var/www/public/index.php;
1013
+ fastcgi_param CORE_PATH /var/www/cores/${coreName};
1014
+ include fastcgi_params;
1015
+ fastcgi_param REQUEST_URI \$request_uri;
1016
+ fastcgi_param SCRIPT_NAME /index.php;
1017
+ fastcgi_param HTTP_HOST \$host;
1018
+ fastcgi_param HTTP_TENANT_HOST \$host;
1019
+ fastcgi_param HTTPS on;
1020
+ fastcgi_param HTTP_X_FORWARDED_PROTO https;
1021
+
1022
+ fastcgi_connect_timeout 300s;
1023
+ fastcgi_send_timeout 300s;
1024
+ fastcgi_read_timeout 300s;
1025
+ fastcgi_buffer_size 128k;
1026
+ fastcgi_buffers 4 256k;
1027
+ fastcgi_busy_buffers_size 256k;
1028
+ fastcgi_temp_file_write_size 256k;
1029
+ }
1030
+ }
1031
+
1032
+ # ======================================================
1033
+ # 3. TENANT SUBDOMAIN SERVER (*.${domain} except api) - HTTPS
1034
+ # Wildcard subdomain - her tenant kendi subdomain'inde çalışır
1035
+ # ======================================================
1036
+ server {
1037
+ listen 443 ssl;
1038
+ http2 on;
1039
+ server_name ~^(?!api\\.)(?<subdomain>[^.]+)\\.${escapedDomain}\$;
1040
+
1041
+ ssl_certificate /etc/nginx/ssl/${certName}.crt;
1042
+ ssl_certificate_key /etc/nginx/ssl/${certName}.key;
1043
+ ssl_protocols TLSv1.2 TLSv1.3;
1044
+ ssl_prefer_server_ciphers on;
1045
+ ssl_session_cache shared:SSL:10m;
1046
+ ssl_session_timeout 10m;
1047
+
1048
+ client_max_body_size 25m;
1049
+ client_body_timeout 60s;
1050
+ client_header_timeout 60s;
1051
+ server_tokens off;
1052
+
1053
+ # Security Headers
1054
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
1055
+ add_header X-Content-Type-Options "nosniff" always;
1056
+ add_header X-Frame-Options "SAMEORIGIN" always;
1057
+ add_header X-XSS-Protection "1; mode=block" always;
1058
+
1059
+ # Gzip Compression
1060
+ gzip on;
1061
+ gzip_vary on;
1062
+ gzip_proxied any;
1063
+ gzip_comp_level 6;
1064
+ gzip_min_length 1000;
1065
+ gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
1066
+
1067
+ # Health Check
1068
+ location /health {
1069
+ access_log off;
1070
+ return 200 "healthy\\n";
1071
+ add_header Content-Type text/plain;
1072
+ }
1073
+
1074
+ # CDN Proxy for Nuxt Assets (/_nuxt/ -> CDN)
1075
+ location ^~ /_nuxt/ {
1076
+ proxy_pass ${cdnUrl}/_nuxt/;
1077
+ proxy_ssl_server_name on;
1078
+ proxy_cache_valid 200 365d;
1079
+ proxy_cache_valid 404 1m;
1080
+ add_header 'Cache-Control' 'public, max-age=31536000, immutable' always;
1081
+ add_header 'Access-Control-Allow-Origin' '*' always;
1082
+ proxy_hide_header 'Access-Control-Allow-Origin';
1083
+ proxy_intercept_errors on;
1084
+ error_page 404 = @nuxt_fallback_tenant;
1085
+ }
1086
+
1087
+ # Fallback to Nuxt if CDN file not found
1088
+ location @nuxt_fallback_tenant {
1089
+ proxy_pass http://nuxt_frontend;
1090
+ proxy_set_header Host \$host;
1091
+ proxy_set_header X-Real-IP \$remote_addr;
1092
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
1093
+ proxy_set_header X-Forwarded-Proto https;
1094
+ }
1095
+
1096
+ # Shared API Files
1097
+ location ^~ /shared/ {
1098
+ alias /home/cores/${coreName}/shared/;
1099
+ autoindex off;
1100
+ types { application/javascript js mjs; application/json json; }
1101
+ default_type application/javascript;
1102
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1103
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1104
+ add_header 'Cache-Control' 'public, max-age=3600, immutable' always;
1105
+ try_files \$uri =404;
1106
+ }
1107
+
1108
+ # Vendor Libs
1109
+ location ^~ /vendor/ {
1110
+ alias /home/cores/${coreName}/shared/vendor/;
1111
+ autoindex off;
1112
+ types { application/javascript js mjs; }
1113
+ default_type application/javascript;
1114
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1115
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1116
+ add_header 'Cache-Control' 'public, max-age=31536000, immutable' always;
1117
+ try_files \$uri =404;
1118
+ }
1119
+
1120
+ # Addon ESM Files
1121
+ location ^~ /cores/ {
1122
+ alias /home/cores/;
1123
+ autoindex off;
1124
+ types { application/javascript js mjs; application/json json; text/css css; text/html html vue; image/svg+xml svg; }
1125
+ default_type application/javascript;
1126
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1127
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1128
+ add_header 'X-Content-Type-Options' 'nosniff' always;
1129
+ open_file_cache off;
1130
+ try_files \$uri =404;
1131
+ }
1132
+
1133
+ # Frontend (Nuxt SSR) - Tenant subdomain olarak yönlendir
1134
+ location / {
1135
+ proxy_pass http://nuxt_frontend;
1136
+ proxy_set_header Host \$host;
1137
+ proxy_set_header X-Real-IP \$remote_addr;
1138
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
1139
+ proxy_set_header X-Forwarded-Proto https;
1140
+ proxy_set_header X-Forwarded-Host \$host;
1141
+ proxy_set_header X-Core-Path /var/www/cores/${coreName};
1142
+ proxy_set_header X-Tenant-Host \$host;
1143
+ proxy_set_header X-Tenant-Subdomain \$subdomain;
1144
+ proxy_http_version 1.1;
1145
+ proxy_set_header Upgrade \$http_upgrade;
1146
+ proxy_set_header Connection \$connection_upgrade;
1147
+ proxy_connect_timeout 300s;
1148
+ proxy_send_timeout 300s;
1149
+ proxy_read_timeout 300s;
1150
+ proxy_buffering off;
1151
+ proxy_cache_bypass \$http_upgrade;
1152
+ }
1153
+
1154
+ # Deny Hidden Files
1155
+ location ~ /\\.(?!well-known) {
1156
+ deny all;
1157
+ access_log off;
1158
+ log_not_found off;
1159
+ }
1160
+ }
1161
+
1162
+ # ======================================================
1163
+ # 4. MAIN WEB SERVER (${domain}, www.${domain}) - HTTPS
1164
+ # ======================================================
1165
+ server {
1166
+ listen 443 ssl;
1167
+ http2 on;
1168
+ server_name ${domain} www.${domain};
1169
+
1170
+ ssl_certificate /etc/nginx/ssl/${certName}.crt;
1171
+ ssl_certificate_key /etc/nginx/ssl/${certName}.key;
1172
+ ssl_protocols TLSv1.2 TLSv1.3;
1173
+ ssl_prefer_server_ciphers on;
1174
+ ssl_session_cache shared:SSL:10m;
1175
+ ssl_session_timeout 10m;
1176
+
1177
+ client_max_body_size 25m;
1178
+ client_body_timeout 60s;
1179
+ client_header_timeout 60s;
1180
+ server_tokens off;
1181
+
1182
+ # Security Headers
1183
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
1184
+ add_header X-Content-Type-Options "nosniff" always;
1185
+ add_header X-Frame-Options "SAMEORIGIN" always;
1186
+ add_header X-XSS-Protection "1; mode=block" always;
1187
+
1188
+ # Gzip Compression
1189
+ gzip on;
1190
+ gzip_vary on;
1191
+ gzip_proxied any;
1192
+ gzip_comp_level 6;
1193
+ gzip_min_length 1000;
1194
+ gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
1195
+
1196
+ # Health Check
1197
+ location /health {
1198
+ access_log off;
1199
+ return 200 "healthy\\n";
1200
+ add_header Content-Type text/plain;
1201
+ }
1202
+
1203
+ # CDN Proxy for Nuxt Assets (/_nuxt/ -> CDN)
1204
+ # Each core can use its own CDN URL from .env (NUXT_PUBLIC_CORE_CDN_URL)
1205
+ location ^~ /_nuxt/ {
1206
+ proxy_pass ${cdnUrl}/_nuxt/;
1207
+ proxy_ssl_server_name on;
1208
+ proxy_cache_valid 200 365d;
1209
+ proxy_cache_valid 404 1m;
1210
+ add_header 'Cache-Control' 'public, max-age=31536000, immutable' always;
1211
+ add_header 'Access-Control-Allow-Origin' '*' always;
1212
+ proxy_hide_header 'Access-Control-Allow-Origin';
1213
+ proxy_intercept_errors on;
1214
+ error_page 404 = @nuxt_fallback;
1215
+ }
1216
+
1217
+ # Fallback to Nuxt if CDN file not found
1218
+ location @nuxt_fallback {
1219
+ proxy_pass http://nuxt_frontend;
1220
+ proxy_set_header Host \$host;
1221
+ proxy_set_header X-Real-IP \$remote_addr;
1222
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
1223
+ proxy_set_header X-Forwarded-Proto https;
1224
+ }
1225
+
1226
+ # Shared API Files
1227
+ location ^~ /shared/ {
1228
+ alias /home/cores/${coreName}/shared/;
1229
+ autoindex off;
1230
+ types { application/javascript js mjs; application/json json; }
1231
+ default_type application/javascript;
1232
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1233
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1234
+ add_header 'Cache-Control' 'public, max-age=3600, immutable' always;
1235
+ try_files \$uri =404;
1236
+ }
1237
+
1238
+ # Vendor Libs
1239
+ location ^~ /vendor/ {
1240
+ alias /home/cores/${coreName}/shared/vendor/;
1241
+ autoindex off;
1242
+ types { application/javascript js mjs; }
1243
+ default_type application/javascript;
1244
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1245
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1246
+ add_header 'Cache-Control' 'public, max-age=31536000, immutable' always;
1247
+ try_files \$uri =404;
1248
+ }
1249
+
1250
+ # Addon ESM Files
1251
+ location ^~ /cores/ {
1252
+ alias /home/cores/;
1253
+ autoindex off;
1254
+ types { application/javascript js mjs; application/json json; text/css css; text/html html vue; image/svg+xml svg; }
1255
+ default_type application/javascript;
1256
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1257
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1258
+ add_header 'X-Content-Type-Options' 'nosniff' always;
1259
+ open_file_cache off;
1260
+ try_files \$uri =404;
1261
+ }
1262
+
1263
+ # API Endpoints (on web domain)
1264
+ location ~ ^/(api|v1|v2|auth|system|system-settings|upload|addons|core_addons|core|analyzer|summary|metrics|jobs|worker|setup|tenant-manager|login|logout|me|public|status) {
1265
+ # CORS Preflight
1266
+ if (\$request_method = 'OPTIONS') {
1267
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1268
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
1269
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Cache-Control, Accept' always;
1270
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1271
+ add_header 'Access-Control-Max-Age' '86400' always;
1272
+ add_header 'Content-Type' 'text/plain; charset=utf-8' always;
1273
+ add_header 'Content-Length' '0' always;
1274
+ return 204;
1275
+ }
1276
+
1277
+ add_header 'Access-Control-Allow-Origin' \$http_origin always;
1278
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
1279
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-Core-Path, X-Tenant-Host, Accept' always;
1280
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
1281
+ add_header 'Access-Control-Expose-Headers' 'X-CSRF-Token' always;
1282
+
1283
+ fastcgi_pass php_backend;
1284
+ fastcgi_param SCRIPT_FILENAME /var/www/public/index.php;
1285
+ fastcgi_param CORE_PATH /var/www/cores/${coreName};
1286
+ include fastcgi_params;
1287
+ fastcgi_param REQUEST_URI \$request_uri;
1288
+ fastcgi_param SCRIPT_NAME /index.php;
1289
+ fastcgi_param HTTP_HOST \$host;
1290
+ fastcgi_param HTTP_TENANT_HOST \$host;
1291
+ fastcgi_param HTTPS on;
1292
+ fastcgi_param HTTP_X_FORWARDED_PROTO https;
1293
+
1294
+ fastcgi_connect_timeout 300s;
1295
+ fastcgi_send_timeout 300s;
1296
+ fastcgi_read_timeout 300s;
1297
+ fastcgi_buffer_size 128k;
1298
+ fastcgi_buffers 4 256k;
1299
+ fastcgi_busy_buffers_size 256k;
1300
+ fastcgi_temp_file_write_size 256k;
1301
+ }
1302
+
1303
+ # Frontend (Nuxt SSR)
1304
+ location / {
1305
+ proxy_pass http://nuxt_frontend;
1306
+ proxy_set_header Host \$host;
1307
+ proxy_set_header X-Real-IP \$remote_addr;
1308
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
1309
+ proxy_set_header X-Forwarded-Proto https;
1310
+ proxy_set_header X-Forwarded-Host \$host;
1311
+ proxy_set_header X-Core-Path /var/www/cores/${coreName};
1312
+ proxy_set_header X-Tenant-Host \$host;
1313
+ proxy_http_version 1.1;
1314
+ proxy_set_header Upgrade \$http_upgrade;
1315
+ proxy_set_header Connection \$connection_upgrade;
1316
+ proxy_connect_timeout 300s;
1317
+ proxy_send_timeout 300s;
1318
+ proxy_read_timeout 300s;
1319
+ proxy_buffering off;
1320
+ proxy_cache_bypass \$http_upgrade;
1321
+ }
1322
+
1323
+ # Deny Hidden Files
1324
+ location ~ /\\.(?!well-known) {
1325
+ deny all;
1326
+ access_log off;
1327
+ log_not_found off;
1328
+ }
1329
+ }`;
1330
+ await runCmd(`cat > /home/services/nginx/conf.d/${configName}.conf << 'NGINX_EOF'
1331
+ ${sslNginxConfig}
1332
+ NGINX_EOF`);
1333
+ // Recreate ALL containers with no-cache to ensure fresh state
1334
+ // This ensures SSL certificates and all configs are properly loaded
1335
+ log.info('Recreating all containers (no-cache)...');
1336
+ // Stop all containers
1337
+ await runCmd('cd /home/services && docker compose -f docker-server-nginx.yml down 2>/dev/null || true');
1338
+ await runCmd('cd /home/services && docker compose -f docker-core-nuxt.yml down 2>/dev/null || true');
1339
+ await runCmd('cd /home/services && docker compose -f docker-core-php.yml down 2>/dev/null || true');
1340
+ await runCmd('cd /home/services && docker compose -f docker-redis.yml down 2>/dev/null || true');
1341
+ await runCmd('cd /home/services && docker compose -f docker-postgresql.yml down 2>/dev/null || true');
1342
+ // Remove any orphan containers
1343
+ await runCmd('docker container prune -f 2>/dev/null || true');
1344
+ // Start all containers fresh (in correct order)
1345
+ log.info('▶ Starting PostgreSQL...');
1346
+ await runCmd('cd /home/services && docker compose -f docker-postgresql.yml up -d --force-recreate');
1347
+ log.info('▶ Starting Redis...');
1348
+ await runCmd('cd /home/services && docker compose -f docker-redis.yml up -d --force-recreate');
1349
+ log.info('⏳ Waiting for database (5s)...');
1350
+ await runCmd('sleep 5');
1351
+ log.info('▶ Starting PHP...');
1352
+ await runCmd('cd /home/services && docker compose -f docker-core-php.yml up -d --force-recreate');
1353
+ log.info('▶ Starting Nuxt...');
1354
+ await runCmd('cd /home/services && docker compose -f docker-core-nuxt.yml up -d --force-recreate');
1355
+ log.info('▶ Starting Nginx...');
1356
+ await runCmd('cd /home/services && docker compose -f docker-server-nginx.yml up -d --force-recreate');
1357
+ // Wait for containers to stabilize
1358
+ log.info('⏳ Waiting for containers (5s)...');
1359
+ await runCmd('sleep 5');
1360
+ // Create storage directories in PHP container (required after recreate)
1361
+ log.info('📁 Creating storage directories...');
1362
+ await runCmd('docker exec -u root cicore_php mkdir -p /var/www/storage/cache /var/www/storage/logs /var/www/storage/sessions /var/www/storage/uploads');
1363
+ await runCmd('docker exec -u root cicore_php chmod -R 777 /var/www/storage');
1364
+ await runCmd('docker exec -u root cicore_php chown -R www-data:www-data /var/www/storage');
1365
+ // Test nginx config
1366
+ const nginxTest = await runCmd('docker exec cicore_global_nginx nginx -t');
1367
+ if (!nginxTest.success) {
1368
+ log.error('❌ Nginx config test failed');
1369
+ return;
1370
+ }
1371
+ log.success('✅ All containers recreated');
1372
+ // Show container status
1373
+ log.blank();
1374
+ log.info('📊 Container Status:');
1375
+ const statusResult = await runCmd('docker ps --format "table {{.Names}}\t{{.Status}}"');
1376
+ if (statusResult.success) {
1377
+ console.log(statusResult.stdout);
1378
+ }
1379
+ log.blank();
1380
+ log.box('✅ Phase 4 Complete', [
1381
+ `Domain: ${domain}`,
1382
+ `SSL: ${useCloudflare ? 'Cloudflare Origin' : "Let's Encrypt"}`,
1383
+ '',
1384
+ `Visit: https://${domain}`,
1385
+ ]);
1386
+ }
1387
+ // ============================================
1388
+ // FULL INSTALL (All Phases)
1389
+ // ============================================
1390
+ async function serverInstallFull(options) {
1391
+ log.title('🚀 ciCore Full Installation');
1392
+ log.info('Running all phases: setup → deploy → domain → ssl');
1393
+ log.blank();
1394
+ const host = await getHost(options.host);
1395
+ if (!host)
1396
+ return;
1397
+ const coreName = options.core || await input('Core name:', 'core1');
1398
+ const domain = options.domain || await input('Root domain:');
1399
+ if (!domain) {
1400
+ log.error('Domain is required');
1401
+ return;
1402
+ }
1403
+ log.info('Configuration:');
1404
+ log.kv('Host', `${host.name} (${host.hostname})`);
1405
+ log.kv('Core', coreName);
1406
+ log.kv('Domain', domain);
1407
+ log.blank();
1408
+ const shouldContinue = await confirm('Start full installation?', true);
1409
+ if (!shouldContinue) {
1410
+ log.warn('Cancelled');
1411
+ return;
1412
+ }
1413
+ // Phase 1: Setup
1414
+ log.blank();
1415
+ await serverSetup({ host: host.name, skipDocker: options.skipDocker });
1416
+ // Phase 2: Deploy
1417
+ log.blank();
1418
+ await serverDeploy({ host: host.name, core: coreName });
1419
+ // Phase 3: Domain
1420
+ log.blank();
1421
+ await serverDomain({ host: host.name, core: coreName, domain });
1422
+ // Phase 4: SSL (if not skipped)
1423
+ if (!options.skipSsl) {
1424
+ log.blank();
1425
+ await serverSSL({ host: host.name, domain, cloudflare: options.cloudflare });
1426
+ }
1427
+ log.blank();
1428
+ log.box('🎉 Installation Complete!', [
1429
+ `Server: ${host.hostname}`,
1430
+ `Core: ${coreName}`,
1431
+ `Domain: ${domain}`,
1432
+ '',
1433
+ `Visit: https://${domain}`,
1434
+ ]);
1435
+ process.exit(0);
1436
+ }
1437
+ // ============================================
1438
+ // SERVER CLI - Install/Run CLI on Remote Server
1439
+ // ============================================
1440
+ async function serverCLI(options) {
1441
+ log.title('🖥️ Remote CLI Management');
1442
+ log.blank();
1443
+ const host = await getHost(options.host);
1444
+ if (!host)
1445
+ return;
1446
+ if (options.install) {
1447
+ await installCLIOnServer(host);
1448
+ }
1449
+ else if (options.run) {
1450
+ await runRemoteCLI(host.name, options.run);
1451
+ }
1452
+ else {
1453
+ // Default: check if CLI is installed, if not install it
1454
+ log.info('Checking CLI installation on server...');
1455
+ const checkResult = await runSSH(host, 'which vu 2>/dev/null || echo "NOT_INSTALLED"');
1456
+ if (checkResult.stdout.includes('NOT_INSTALLED')) {
1457
+ log.warn('CLI not installed on server');
1458
+ const shouldInstall = await confirm('Install CLI on server?', true);
1459
+ if (shouldInstall) {
1460
+ await installCLIOnServer(host);
1461
+ }
1462
+ }
1463
+ else {
1464
+ log.success('✅ CLI is installed on server');
1465
+ const versionResult = await runSSH(host, 'vu --version');
1466
+ if (versionResult.success) {
1467
+ log.info(`Version: ${versionResult.stdout.trim()}`);
1468
+ }
1469
+ }
1470
+ }
1471
+ process.exit(0);
1472
+ }
1473
+ async function installCLIOnServer(host) {
1474
+ const startTime = Date.now();
1475
+ // Helper for live SSH - runs command and shows output in real-time
1476
+ const sshLive = async (cmd, label) => {
1477
+ log.info(` → ${label}`);
1478
+ const args = buildSSHCommand(host, cmd);
1479
+ const result = await exec('ssh', args, { silent: false, stream: true });
1480
+ return result.success;
1481
+ };
1482
+ log.blank();
1483
+ log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1484
+ log.info(' CiCore CLI Remote Installation');
1485
+ log.info(` Target: ${host.user}@${host.hostname}`);
1486
+ log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1487
+ // Step 1: Check Node.js
1488
+ log.blank();
1489
+ log.step(1, 6, 'Checking Node.js runtime...');
1490
+ await sshLive('node --version ; npm --version', 'Checking versions...');
1491
+ log.success(' ✅ Node.js check complete');
1492
+ // Step 2: Prepare directory
1493
+ log.blank();
1494
+ log.step(2, 6, 'Preparing directory...');
1495
+ // Use semicolon instead of && for Windows SSH compatibility
1496
+ await sshLive('rm -rf /opt/vucore-cli ; mkdir -p /opt/vucore-cli ; ls -la /opt/vucore-cli', 'Creating /opt/vucore-cli...');
1497
+ log.success(' ✅ Directory ready');
1498
+ // Step 3: Package and upload
1499
+ log.blank();
1500
+ log.step(3, 6, 'Packaging and uploading...');
1501
+ const cliPath = path.join(paths.dev.root, 'cli');
1502
+ const tempTar = path.join(paths.dev.root, '.tmp-cli.tar.gz');
1503
+ log.info(' → Creating local package (including pre-built dist and templates)...');
1504
+ // Include dist folder and templates so we don't need to build on server
1505
+ // Templates are needed for 'vu server deploy --local'
1506
+ await exec('tar', ['-czf', tempTar, '-C', cliPath, '--exclude=node_modules', '--exclude=.git', '.'], { silent: true });
1507
+ const stats = await fs.stat(tempTar);
1508
+ log.info(` → Package size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
1509
+ log.info(' → Uploading to server...');
1510
+ const uploadResult = await runSCP(host, tempTar, '/opt/vucore-cli/cli.tar.gz', false);
1511
+ await fs.remove(tempTar);
1512
+ if (!uploadResult.success) {
1513
+ log.error(' ❌ Upload failed');
1514
+ return;
1515
+ }
1516
+ log.success(' ✅ Uploaded');
1517
+ // Step 4: Extract and install
1518
+ log.blank();
1519
+ log.step(4, 6, 'Installing on server (live output)...');
1520
+ await sshLive('cd /opt/vucore-cli ; tar -xzf cli.tar.gz ; rm cli.tar.gz ; ls -la', 'Extracting package...');
1521
+ log.blank();
1522
+ log.info(' → Running npm install (this will show live output)...');
1523
+ log.blank();
1524
+ if (!await sshLive('cd /opt/vucore-cli ; npm install --production 2>&1', 'npm install')) {
1525
+ log.error(' ❌ npm install failed');
1526
+ return;
1527
+ }
1528
+ log.success(' ✅ Dependencies installed');
1529
+ // Skip build - we already have dist folder from local build
1530
+ await sshLive('ls -la /opt/vucore-cli/dist/', 'Checking dist folder...');
1531
+ // Step 5: Create command
1532
+ log.blank();
1533
+ log.step(5, 6, 'Creating global command...');
1534
+ // Use printf to create wrapper script (works reliably across platforms)
1535
+ const createCmd = `printf '#!/bin/bash\\nnode /opt/vucore-cli/dist/index.js "$$@"\\n' > /usr/local/bin/vu`;
1536
+ await sshLive(createCmd, 'Creating wrapper script...');
1537
+ await sshLive('chmod +x /usr/local/bin/vu', 'Setting permissions...');
1538
+ await sshLive('cat /usr/local/bin/vu', 'Verifying script content...');
1539
+ await sshLive('vu --version', 'Testing vu command...');
1540
+ log.success(' ✅ /usr/local/bin/vu created');
1541
+ // Verify
1542
+ log.blank();
1543
+ log.info('Verifying installation...');
1544
+ await sshLive('node /opt/vucore-cli/dist/index.js --version', 'Running CLI...');
1545
+ // Step 6: Copy config files (Cloudflare token, etc.)
1546
+ log.blank();
1547
+ log.step(6, 6, 'Copying config files...');
1548
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
1549
+ const vucoreConfigDir = path.join(homeDir, '.vucore');
1550
+ const cloudflareConfig = path.join(vucoreConfigDir, 'cloudflare.json');
1551
+ // Create .vucore directory on server
1552
+ await sshLive('mkdir -p /root/.vucore', 'Creating config directory...');
1553
+ // Copy Cloudflare token if exists
1554
+ if (await fs.pathExists(cloudflareConfig)) {
1555
+ log.info(' → Copying Cloudflare token...');
1556
+ const cfUpload = await runSCP(host, cloudflareConfig, '/root/.vucore/cloudflare.json', false);
1557
+ if (cfUpload.success) {
1558
+ log.success(' ✅ Cloudflare token copied');
1559
+ }
1560
+ else {
1561
+ log.warn(' ⚠️ Failed to copy Cloudflare token');
1562
+ }
1563
+ }
1564
+ else {
1565
+ log.info(' → No Cloudflare token found locally (skipping)');
1566
+ }
1567
+ // Copy any other config files
1568
+ const configFile = path.join(vucoreConfigDir, 'config.json');
1569
+ if (await fs.pathExists(configFile)) {
1570
+ log.info(' → Copying config.json...');
1571
+ await runSCP(host, configFile, '/root/.vucore/config.json', false);
1572
+ log.success(' ✅ Config copied');
1573
+ }
1574
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1575
+ log.blank();
1576
+ log.box('✅ CLI Installation Complete', [
1577
+ `Server: ${host.hostname}`,
1578
+ `Time: ${elapsed}s`,
1579
+ '',
1580
+ 'Installed:',
1581
+ ' ✅ CiCore CLI (/usr/local/bin/vu)',
1582
+ ' ✅ Config files (/root/.vucore/)',
1583
+ ]);
1584
+ log.blank();
1585
+ log.info('Next: SSH to server and run "vu server setup"');
1586
+ }
1587
+ async function runRemoteCLI(hostName, command) {
1588
+ const host = await getHost(hostName);
1589
+ if (!host)
1590
+ return;
1591
+ log.info(`Running on ${host.hostname}: vu ${command}`);
1592
+ log.blank();
1593
+ // Run the command on server
1594
+ const result = await runSSH(host, `vu ${command}`);
1595
+ if (result.stdout) {
1596
+ console.log(result.stdout);
1597
+ }
1598
+ if (result.stderr) {
1599
+ console.error(result.stderr);
1600
+ }
1601
+ process.exit(result.success ? 0 : 1);
1602
+ }
1603
+ // ============================================
1604
+ // LIST SERVERS
1605
+ // ============================================
1606
+ async function listServers() {
1607
+ log.title('SSH Hosts');
1608
+ const hosts = await listSSHHosts();
1609
+ if (hosts.length === 0) {
1610
+ log.info('No SSH hosts found in ~/.ssh/config');
1611
+ return;
1612
+ }
1613
+ formatTable(['Name', 'Hostname', 'User', 'Port'], hosts.map(h => [h.name, h.hostname, h.user, h.port.toString()]));
1614
+ }
1615
+ // ============================================
1616
+ // SERVER STATUS
1617
+ // ============================================
1618
+ async function serverStatus(options) {
1619
+ log.title('Server Status');
1620
+ const host = await getSSHHost(options.host);
1621
+ if (!host) {
1622
+ log.error(`SSH host "${options.host}" not found`);
1623
+ return;
1624
+ }
1625
+ log.kv('Host', `${host.name} (${host.hostname})`);
1626
+ log.blank();
1627
+ // Check containers
1628
+ const containerSpinner = spinner('Checking containers...').start();
1629
+ const containerResult = await runSSH(host, 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"');
1630
+ containerSpinner.stop();
1631
+ if (containerResult.success) {
1632
+ console.log(containerResult.stdout);
1633
+ }
1634
+ // Check disk
1635
+ log.blank();
1636
+ const diskSpinner = spinner('Checking disk usage...').start();
1637
+ const diskResult = await runSSH(host, 'df -h / | tail -1');
1638
+ diskSpinner.stop();
1639
+ if (diskResult.success) {
1640
+ const parts = diskResult.stdout.trim().split(/\s+/);
1641
+ log.kv('Disk Usage', `${parts[2]} / ${parts[1]} (${parts[4]})`);
1642
+ }
1643
+ // Check memory
1644
+ const memSpinner = spinner('Checking memory...').start();
1645
+ const memResult = await runSSH(host, 'free -h | grep Mem');
1646
+ memSpinner.stop();
1647
+ if (memResult.success) {
1648
+ const parts = memResult.stdout.trim().split(/\s+/);
1649
+ log.kv('Memory', `${parts[2]} / ${parts[1]} used`);
1650
+ }
1651
+ }
1652
+ // ============================================
1653
+ // SERVER LOGS
1654
+ // ============================================
1655
+ async function serverLogs(options) {
1656
+ const host = await getSSHHost(options.host);
1657
+ if (!host) {
1658
+ log.error(`SSH host "${options.host}" not found`);
1659
+ return;
1660
+ }
1661
+ const service = options.service || 'nuxt';
1662
+ const containerMap = {
1663
+ nuxt: 'cicore_nuxt',
1664
+ php: 'cicore_php',
1665
+ nginx: 'cicore_nginx',
1666
+ postgres: 'cicore_postgres',
1667
+ redis: 'cicore_redis',
1668
+ };
1669
+ const container = containerMap[service] || service;
1670
+ const follow = options.follow ? '-f' : '';
1671
+ const lines = options.lines || '100';
1672
+ log.info(`Logs for ${container} on ${host.name}:`);
1673
+ log.blank();
1674
+ const result = await runSSH(host, `docker logs ${follow} --tail ${lines} ${container}`);
1675
+ console.log(result.stdout);
1676
+ if (result.stderr)
1677
+ console.error(result.stderr);
1678
+ }
1679
+ // ============================================
1680
+ // HELPER FUNCTIONS
1681
+ // ============================================
1682
+ async function runSSH(host, command, options = {}) {
1683
+ const { timeout = 120000, verbose = false } = options; // 2 min default timeout
1684
+ if (verbose) {
1685
+ log.debug(`[SSH] ${command.substring(0, 100)}...`);
1686
+ }
1687
+ const args = ['-o', 'ConnectTimeout=30', '-o', 'ServerAliveInterval=10', ...buildSSHCommand(host, command)];
1688
+ try {
1689
+ const result = await Promise.race([
1690
+ exec('ssh', args, { silent: !verbose }),
1691
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`SSH command timed out after ${timeout / 1000}s`)), timeout))
1692
+ ]);
1693
+ if (!result.success && verbose) {
1694
+ log.error(`[SSH ERROR] ${result.stderr}`);
1695
+ }
1696
+ return result;
1697
+ }
1698
+ catch (err) {
1699
+ log.error(`[SSH TIMEOUT] ${err.message}`);
1700
+ return { success: false, stdout: '', stderr: err.message };
1701
+ }
1702
+ }
1703
+ async function runSSHLive(host, command, label) {
1704
+ if (label)
1705
+ log.info(label);
1706
+ const args = ['-o', 'ConnectTimeout=30', '-o', 'ServerAliveInterval=10', ...buildSSHCommand(host, command)];
1707
+ const result = await exec('ssh', args, { silent: true });
1708
+ // Print output
1709
+ if (result.stdout) {
1710
+ console.log(result.stdout);
1711
+ }
1712
+ if (result.stderr && !result.success) {
1713
+ log.error(`❌ Command failed: ${command.substring(0, 50)}...`);
1714
+ console.error(result.stderr);
1715
+ }
1716
+ return result;
1717
+ }
1718
+ async function runSCP(host, source, dest, recursive = false) {
1719
+ const args = ['-o', 'ConnectTimeout=30', ...buildSCPCommand(host, source, dest, recursive)];
1720
+ const result = await exec('scp', args, { silent: false, stream: true });
1721
+ if (!result.success) {
1722
+ log.error(`❌ SCP failed: ${source} -> ${dest}`);
1723
+ if (result.stderr)
1724
+ log.error(result.stderr);
1725
+ }
1726
+ return result;
1727
+ }
1728
+ // ============================================
1729
+ // CONFIG GENERATORS
1730
+ // ============================================
1731
+ function generateCoreEnv(coreName, domain) {
1732
+ return `# ciCore Environment - ${coreName}
1733
+ # Generated by CiCore CLI
1734
+
1735
+ # Application
1736
+ APP_NAME=VuCore
1737
+ APP_ENV=production
1738
+ APP_DEBUG=false
1739
+ APP_URL=https://${domain}
1740
+
1741
+ # Core
1742
+ CORE_NAME=${coreName}
1743
+ CORE_DOMAIN=${domain}
1744
+
1745
+ # Frontend URLs (Nuxt)
1746
+ NUXT_PUBLIC_API_BASE=https://api.${domain}
1747
+ NUXT_PUBLIC_WS_URL=wss://${domain}:2346
1748
+ NUXT_PUBLIC_HOST=${domain}
1749
+ NUXT_PUBLIC_CORE_CDN_URL=https://cdn.cicore.com.tr/CoreUI/latest
1750
+
1751
+ # Database
1752
+ DB_CONNECTION=pgsql
1753
+ DB_HOST=cicore_postgres
1754
+ DB_PORT=5432
1755
+ DB_DATABASE=vucore_${coreName}
1756
+ DB_USERNAME=vucore
1757
+ DB_PASSWORD=vucore_secure_password_${Date.now().toString(36)}
1758
+ DB_GATEWAY=local
1759
+
1760
+ # Redis
1761
+ REDIS_HOST=cicore_redis
1762
+ REDIS_PORT=6379
1763
+ REDIS_PASSWORD=
1764
+
1765
+ # CDN
1766
+ CDN_URL=https://cdn.cicore.com.tr
1767
+ CDN_ENABLED=true
1768
+
1769
+ # Security
1770
+ JWT_SECRET=${generateRandomString(64)}
1771
+ API_KEY=${generateRandomString(32)}
1772
+ `;
1773
+ }
1774
+ function generateNuxtCompose(coreName, domain) {
1775
+ const registry = getDockerRegistry();
1776
+ return `version: '3.8'
1777
+
1778
+ services:
1779
+ nuxt:
1780
+ image: ${registry}/vucore-nuxt:latest
1781
+ container_name: cicore_nuxt
1782
+ restart: unless-stopped
1783
+ ports:
1784
+ - "3000:3000"
1785
+ environment:
1786
+ - NODE_ENV=production
1787
+ - NUXT_PUBLIC_API_BASE=https://${domain}/api
1788
+ volumes:
1789
+ - /home/cores:/app/cores:ro
1790
+ networks:
1791
+ - cicore_network
1792
+ healthcheck:
1793
+ test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
1794
+ interval: 30s
1795
+ timeout: 10s
1796
+ retries: 3
1797
+
1798
+ networks:
1799
+ cicore_network:
1800
+ external: true
1801
+ `;
1802
+ }
1803
+ function generatePHPCompose(coreName) {
1804
+ const registry = getDockerRegistry();
1805
+ return `version: '3.8'
1806
+
1807
+ services:
1808
+ php:
1809
+ image: ${registry}/vucore-php:latest
1810
+ container_name: cicore_php
1811
+ restart: unless-stopped
1812
+ volumes:
1813
+ - /home/cores:/var/www/cores
1814
+ networks:
1815
+ - cicore_network
1816
+ healthcheck:
1817
+ test: ["CMD", "php-fpm", "-t"]
1818
+ interval: 30s
1819
+ timeout: 10s
1820
+ retries: 3
1821
+
1822
+ postgres:
1823
+ image: postgres:15-alpine
1824
+ container_name: cicore_postgres
1825
+ restart: unless-stopped
1826
+ environment:
1827
+ POSTGRES_USER: vucore
1828
+ POSTGRES_PASSWORD: vucore_secure_password
1829
+ POSTGRES_DB: vucore_${coreName}
1830
+ volumes:
1831
+ - postgres_data:/var/lib/postgresql/data
1832
+ networks:
1833
+ - cicore_network
1834
+ healthcheck:
1835
+ test: ["CMD-SHELL", "pg_isready -U vucore"]
1836
+ interval: 10s
1837
+ timeout: 5s
1838
+ retries: 5
1839
+
1840
+ redis:
1841
+ image: redis:7-alpine
1842
+ container_name: cicore_redis
1843
+ restart: unless-stopped
1844
+ networks:
1845
+ - cicore_network
1846
+ healthcheck:
1847
+ test: ["CMD", "redis-cli", "ping"]
1848
+ interval: 10s
1849
+ timeout: 5s
1850
+ retries: 5
1851
+
1852
+ volumes:
1853
+ postgres_data:
1854
+
1855
+ networks:
1856
+ cicore_network:
1857
+ external: true
1858
+ `;
1859
+ }
1860
+ function generateNginxConfig(coreName, domain) {
1861
+ return `# ciCore Nginx Config - ${coreName}
1862
+ # Generated by CiCore CLI
1863
+
1864
+ upstream nuxt_backend {
1865
+ server 127.0.0.1:3000;
1866
+ keepalive 64;
1867
+ }
1868
+
1869
+ upstream php_backend {
1870
+ server 127.0.0.1:9000;
1871
+ keepalive 64;
1872
+ }
1873
+
1874
+ server {
1875
+ listen 80;
1876
+ server_name ${domain} *.${domain};
1877
+
1878
+ location / {
1879
+ return 301 https://\$host\$request_uri;
1880
+ }
1881
+ }
1882
+ `;
1883
+ }
1884
+ function generateNginxConfigWithSSL(coreName, domain) {
1885
+ return `# ciCore Nginx Config - ${coreName}
1886
+ # Generated by CiCore CLI
1887
+
1888
+ upstream nuxt_backend {
1889
+ server 127.0.0.1:3000;
1890
+ keepalive 64;
1891
+ }
1892
+
1893
+ upstream php_backend {
1894
+ server 127.0.0.1:9000;
1895
+ keepalive 64;
1896
+ }
1897
+
1898
+ server {
1899
+ listen 80;
1900
+ server_name ${domain} *.${domain};
1901
+ return 301 https://\$host\$request_uri;
1902
+ }
1903
+
1904
+ server {
1905
+ listen 443 ssl http2;
1906
+ server_name ${domain} *.${domain};
1907
+
1908
+ ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
1909
+ ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
1910
+ ssl_protocols TLSv1.2 TLSv1.3;
1911
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
1912
+ ssl_prefer_server_ciphers off;
1913
+
1914
+ # API routes -> PHP
1915
+ location /api {
1916
+ proxy_pass http://php_backend;
1917
+ proxy_http_version 1.1;
1918
+ proxy_set_header Host \$host;
1919
+ proxy_set_header X-Real-IP \$remote_addr;
1920
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
1921
+ proxy_set_header X-Forwarded-Proto \$scheme;
1922
+ }
1923
+
1924
+ # Everything else -> Nuxt
1925
+ location / {
1926
+ proxy_pass http://nuxt_backend;
1927
+ proxy_http_version 1.1;
1928
+ proxy_set_header Upgrade \$http_upgrade;
1929
+ proxy_set_header Connection 'upgrade';
1930
+ proxy_set_header Host \$host;
1931
+ proxy_cache_bypass \$http_upgrade;
1932
+ }
1933
+ }
1934
+ `;
1935
+ }
1936
+ function generateRebuildNuxtScript() {
1937
+ const registry = getDockerRegistry();
1938
+ return `#!/bin/bash
1939
+ # VuCore - Rebuild Nuxt Container
1940
+ # Generated by CiCore CLI
1941
+
1942
+ set -e
1943
+
1944
+ COMPOSE_FILE="/home/services/docker-core-nuxt.yml"
1945
+ IMAGE="${registry}/vucore-nuxt:latest"
1946
+
1947
+ echo "Stopping Nuxt container..."
1948
+ docker compose -f "$COMPOSE_FILE" stop nuxt 2>/dev/null || true
1949
+ docker compose -f "$COMPOSE_FILE" rm -f nuxt 2>/dev/null || true
1950
+
1951
+ echo "Removing old images..."
1952
+ docker images | grep vucore-nuxt | awk '{print $3}' | xargs -r docker rmi -f 2>/dev/null || true
1953
+
1954
+ echo "Pulling latest image..."
1955
+ docker pull "$IMAGE"
1956
+
1957
+ echo "Starting Nuxt container..."
1958
+ docker compose -f "$COMPOSE_FILE" up -d nuxt
1959
+
1960
+ echo "Waiting for health check..."
1961
+ sleep 10
1962
+
1963
+ echo "Done!"
1964
+ docker ps --filter "name=cicore_nuxt" --format "table {{.Names}}\t{{.Status}}"
1965
+ `;
1966
+ }
1967
+ function generateRebuildPHPScript() {
1968
+ const registry = getDockerRegistry();
1969
+ return `#!/bin/bash
1970
+ # VuCore - Rebuild PHP Container
1971
+ # Generated by CiCore CLI
1972
+
1973
+ set -e
1974
+
1975
+ COMPOSE_FILE="/home/services/docker-core-php.yml"
1976
+ IMAGE="${registry}/vucore-php:latest"
1977
+
1978
+ echo "Stopping PHP container..."
1979
+ docker compose -f "$COMPOSE_FILE" stop php 2>/dev/null || true
1980
+ docker compose -f "$COMPOSE_FILE" rm -f php 2>/dev/null || true
1981
+
1982
+ echo "Removing old images..."
1983
+ docker images | grep vucore-php | awk '{print $3}' | xargs -r docker rmi -f 2>/dev/null || true
1984
+
1985
+ echo "Pulling latest image..."
1986
+ docker pull "$IMAGE"
1987
+
1988
+ echo "Starting PHP container..."
1989
+ docker compose -f "$COMPOSE_FILE" up -d php
1990
+
1991
+ echo "Done!"
1992
+ docker ps --filter "name=cicore_php" --format "table {{.Names}}\t{{.Status}}"
1993
+ `;
1994
+ }
1995
+ function generateRandomString(length) {
1996
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1997
+ let result = '';
1998
+ for (let i = 0; i < length; i++) {
1999
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
2000
+ }
2001
+ return result;
2002
+ }
2003
+ // ============================================
2004
+ // FACTORY RESET
2005
+ // ============================================
2006
+ async function factoryReset(options) {
2007
+ log.title('🔄 ciCore Factory Reset');
2008
+ log.blank();
2009
+ // Get SSH host
2010
+ let host = null;
2011
+ if (options.host) {
2012
+ host = await getSSHHost(options.host);
2013
+ if (!host) {
2014
+ log.error(`SSH host "${options.host}" not found in ~/.ssh/config`);
2015
+ return;
2016
+ }
2017
+ }
2018
+ else {
2019
+ const hosts = await listSSHHosts();
2020
+ if (hosts.length === 0) {
2021
+ log.error('No SSH hosts found in ~/.ssh/config');
2022
+ return;
2023
+ }
2024
+ const selected = await select('Select SSH host:', hosts.map(h => ({ name: h.name, message: `${h.name} (${h.hostname})` })));
2025
+ host = hosts.find(h => h.name === selected) || null;
2026
+ }
2027
+ if (!host) {
2028
+ log.error('No host selected');
2029
+ return;
2030
+ }
2031
+ log.kv('Server', `${host.name} (${host.hostname})`);
2032
+ log.kv('Keep Images', options.keepImages ? 'Yes' : 'No');
2033
+ log.blank();
2034
+ // Warning message
2035
+ log.box('⚠️ WARNING - DESTRUCTIVE OPERATION', [
2036
+ 'This will permanently delete:',
2037
+ ' • All Docker containers (vucore_*)',
2038
+ ' • All data in /home/cores/',
2039
+ ' • All data in /home/services/',
2040
+ options.keepImages ? '' : ' • All Docker images',
2041
+ ' • Docker networks and volumes',
2042
+ '',
2043
+ 'This operation CANNOT be undone!',
2044
+ ].filter(Boolean));
2045
+ log.blank();
2046
+ if (!options.force) {
2047
+ const confirmed = await confirm(`Are you ABSOLUTELY sure you want to factory reset ${host.name}?`, false);
2048
+ if (!confirmed) {
2049
+ log.info('Cancelled');
2050
+ return;
2051
+ }
2052
+ // Double confirmation
2053
+ const doubleConfirm = await input(`Type "${host.name}" to confirm factory reset:`);
2054
+ if (doubleConfirm !== host.name) {
2055
+ log.error('Confirmation failed. Aborting.');
2056
+ return;
2057
+ }
2058
+ }
2059
+ log.blank();
2060
+ log.warn('Starting factory reset...');
2061
+ log.blank();
2062
+ const startTime = Date.now();
2063
+ // Step 1: Stop all containers
2064
+ const stopSpinner = spinner('Stopping all ciCore containers...').start();
2065
+ const stopCmd = `
2066
+ docker ps -a --filter "name=vucore" --format "{{.Names}}" | xargs -r docker stop 2>/dev/null || true
2067
+ docker ps -a --filter "name=vucore" --format "{{.Names}}" | xargs -r docker rm -f 2>/dev/null || true
2068
+ `;
2069
+ const stopArgs = buildSSHCommand(host, stopCmd);
2070
+ await exec('ssh', stopArgs, { silent: true });
2071
+ stopSpinner.succeed('Containers stopped and removed');
2072
+ // Step 2: Remove Docker networks
2073
+ const networkSpinner = spinner('Removing Docker networks...').start();
2074
+ const networkCmd = `
2075
+ docker network rm vucore_global 2>/dev/null || true
2076
+ docker network rm vucore_proxy 2>/dev/null || true
2077
+ `;
2078
+ const networkArgs = buildSSHCommand(host, networkCmd);
2079
+ await exec('ssh', networkArgs, { silent: true });
2080
+ networkSpinner.succeed('Docker networks removed');
2081
+ // Step 3: Remove Docker volumes
2082
+ const volumeSpinner = spinner('Removing Docker volumes...').start();
2083
+ const volumeCmd = `
2084
+ docker volume ls --filter "name=vucore" -q | xargs -r docker volume rm 2>/dev/null || true
2085
+ docker volume ls --filter "name=postgres" -q | xargs -r docker volume rm 2>/dev/null || true
2086
+ docker volume ls --filter "name=redis" -q | xargs -r docker volume rm 2>/dev/null || true
2087
+ `;
2088
+ const volumeArgs = buildSSHCommand(host, volumeCmd);
2089
+ await exec('ssh', volumeArgs, { silent: true });
2090
+ volumeSpinner.succeed('Docker volumes removed');
2091
+ // Step 4: Remove Docker images (if not keeping)
2092
+ if (!options.keepImages) {
2093
+ const imageSpinner = spinner('Removing Docker images...').start();
2094
+ const imageCmd = `
2095
+ docker images --filter "reference=*vucore*" -q | xargs -r docker rmi -f 2>/dev/null || true
2096
+ docker images --filter "reference=baynis/*" -q | xargs -r docker rmi -f 2>/dev/null || true
2097
+ docker image prune -af 2>/dev/null || true
2098
+ `;
2099
+ const imageArgs = buildSSHCommand(host, imageCmd);
2100
+ await exec('ssh', imageArgs, { silent: true });
2101
+ imageSpinner.succeed('Docker images removed');
2102
+ }
2103
+ // Step 5: Remove /home directories
2104
+ const dirSpinner = spinner('Removing /home directories...').start();
2105
+ const dirCmd = `
2106
+ rm -rf /home/cores 2>/dev/null || true
2107
+ rm -rf /home/services 2>/dev/null || true
2108
+ rm -rf /home/nginx 2>/dev/null || true
2109
+ `;
2110
+ const dirArgs = buildSSHCommand(host, dirCmd);
2111
+ await exec('ssh', dirArgs, { silent: true });
2112
+ dirSpinner.succeed('/home directories removed');
2113
+ // Step 6: Docker system prune
2114
+ const pruneSpinner = spinner('Running Docker system prune...').start();
2115
+ const pruneCmd = 'docker system prune -af --volumes 2>/dev/null || true';
2116
+ const pruneArgs = buildSSHCommand(host, pruneCmd);
2117
+ await exec('ssh', pruneArgs, { silent: true });
2118
+ pruneSpinner.succeed('Docker system pruned');
2119
+ // Summary
2120
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
2121
+ log.blank();
2122
+ log.box('✅ Factory Reset Complete', [
2123
+ `Server: ${host.name} (${host.hostname})`,
2124
+ `Duration: ${elapsed}s`,
2125
+ '',
2126
+ 'Removed:',
2127
+ ' ✓ All ciCore containers',
2128
+ ' ✓ Docker networks (vucore_global, vucore_proxy)',
2129
+ ' ✓ Docker volumes',
2130
+ options.keepImages ? ' ⊘ Docker images (kept)' : ' ✓ Docker images',
2131
+ ' ✓ /home/cores/',
2132
+ ' ✓ /home/services/',
2133
+ '',
2134
+ 'Server is now clean and ready for fresh installation.',
2135
+ `Run: vu server install --host ${host.name} --core core1 --domain yourdomain.com`,
2136
+ ]);
2137
+ }
2138
+ // ============================================
2139
+ // PHASE 0: SERVER PREPARE - System Requirements
2140
+ // ============================================
2141
+ async function serverPrepare(options) {
2142
+ log.title('🔧 Phase 0: System Requirements');
2143
+ log.info('Installing Node.js 20, Docker, and essential tools');
2144
+ log.blank();
2145
+ // Check if running locally
2146
+ const isLocal = options.local || (!options.host && (await listSSHHosts()).length === 0);
2147
+ let runCmd;
2148
+ let host = null;
2149
+ if (isLocal) {
2150
+ log.info('Running in LOCAL mode');
2151
+ runCmd = runLocal;
2152
+ }
2153
+ else {
2154
+ host = await getHost(options.host);
2155
+ if (!host)
2156
+ return;
2157
+ log.info(`Target: ${host.name} (${host.user}@${host.hostname})`);
2158
+ runCmd = (cmd, label) => runSSHLive(host, cmd, label);
2159
+ }
2160
+ log.blank();
2161
+ const startTime = Date.now();
2162
+ let errors = [];
2163
+ // ========================================
2164
+ // STEP 1: System Update
2165
+ // ========================================
2166
+ log.step(1, 5, 'Updating system packages...');
2167
+ const updateResult = await runCmd('export DEBIAN_FRONTEND=noninteractive && apt-get update -qq', 'Updating apt...');
2168
+ if (!updateResult.success) {
2169
+ errors.push('apt-get update failed');
2170
+ log.error('❌ apt-get update failed');
2171
+ }
2172
+ else {
2173
+ log.success('✅ Package list updated');
2174
+ }
2175
+ // ========================================
2176
+ // STEP 2: Install Essential Tools
2177
+ // ========================================
2178
+ log.blank();
2179
+ log.step(2, 5, 'Installing essential tools (curl, git, tree, openssl)...');
2180
+ const toolsResult = await runCmd('export DEBIAN_FRONTEND=noninteractive && apt-get install -y -qq curl git tree openssl ca-certificates gnupg lsb-release', 'Installing tools...');
2181
+ if (!toolsResult.success) {
2182
+ errors.push('Essential tools installation failed');
2183
+ log.error('❌ Tools installation failed');
2184
+ }
2185
+ else {
2186
+ // Verify
2187
+ const treeCheck = await runCmd('tree --version 2>/dev/null | head -1');
2188
+ log.success(`✅ Tools installed (tree: ${treeCheck.stdout.trim() || 'installed'})`);
2189
+ }
2190
+ // ========================================
2191
+ // STEP 3: Install Node.js 20
2192
+ // ========================================
2193
+ log.blank();
2194
+ log.step(3, 5, 'Installing Node.js 20...');
2195
+ // Check current Node.js version
2196
+ const nodeCheck = await runCmd('node --version 2>/dev/null || echo "NOT_INSTALLED"');
2197
+ const currentNode = nodeCheck.stdout.trim();
2198
+ if (currentNode.startsWith('v20') || currentNode.startsWith('v22')) {
2199
+ log.info(`Node.js already installed: ${currentNode}`);
2200
+ log.success('✅ Node.js OK');
2201
+ }
2202
+ else {
2203
+ log.info(`Current Node.js: ${currentNode === 'NOT_INSTALLED' ? 'Not installed' : currentNode}`);
2204
+ // Remove old Node.js packages if exists
2205
+ log.info('Removing old Node.js packages...');
2206
+ await runCmd('apt-get remove -y nodejs npm libnode-dev 2>/dev/null || true', 'Removing old packages...');
2207
+ await runCmd('apt-get autoremove -y 2>/dev/null || true');
2208
+ // Install Node.js 20 via NodeSource
2209
+ log.info('Adding NodeSource repository...');
2210
+ const nodeSourceResult = await runCmd('curl -fsSL https://deb.nodesource.com/setup_20.x | bash -', 'Adding NodeSource repo...');
2211
+ if (!nodeSourceResult.success) {
2212
+ errors.push('NodeSource setup failed');
2213
+ log.error('❌ NodeSource setup failed');
2214
+ }
2215
+ else {
2216
+ log.info('Installing Node.js 20...');
2217
+ const nodeInstallResult = await runCmd('export DEBIAN_FRONTEND=noninteractive && apt-get install -y nodejs', 'Installing Node.js...');
2218
+ if (!nodeInstallResult.success) {
2219
+ errors.push('Node.js installation failed');
2220
+ log.error('❌ Node.js installation failed');
2221
+ }
2222
+ else {
2223
+ // Verify installation
2224
+ const newNodeCheck = await runCmd('node --version');
2225
+ const npmCheck = await runCmd('npm --version');
2226
+ log.success(`✅ Node.js installed: ${newNodeCheck.stdout.trim()}, npm: ${npmCheck.stdout.trim()}`);
2227
+ }
2228
+ }
2229
+ }
2230
+ // ========================================
2231
+ // STEP 4: Install Docker
2232
+ // ========================================
2233
+ log.blank();
2234
+ log.step(4, 5, 'Installing Docker...');
2235
+ // Check current Docker
2236
+ const dockerCheck = await runCmd('docker --version 2>/dev/null || echo "NOT_INSTALLED"');
2237
+ const currentDocker = dockerCheck.stdout.trim();
2238
+ if (currentDocker.includes('Docker version')) {
2239
+ log.info(`Docker already installed: ${currentDocker}`);
2240
+ log.success('✅ Docker OK');
2241
+ }
2242
+ else {
2243
+ log.info('Installing Docker...');
2244
+ // Install Docker using official script
2245
+ const dockerInstallResult = await runCmd('curl -fsSL https://get.docker.com | sh', 'Installing Docker...');
2246
+ if (!dockerInstallResult.success) {
2247
+ errors.push('Docker installation failed');
2248
+ log.error('❌ Docker installation failed');
2249
+ }
2250
+ else {
2251
+ // Enable and start Docker
2252
+ await runCmd('systemctl enable docker && systemctl start docker', 'Starting Docker...');
2253
+ // Verify
2254
+ const newDockerCheck = await runCmd('docker --version');
2255
+ log.success(`✅ Docker installed: ${newDockerCheck.stdout.trim()}`);
2256
+ }
2257
+ }
2258
+ // ========================================
2259
+ // STEP 5: Verify All Installations
2260
+ // ========================================
2261
+ log.blank();
2262
+ log.step(5, 5, 'Verifying installations...');
2263
+ const verifications = [
2264
+ { name: 'Node.js', cmd: 'node --version', expected: 'v20' },
2265
+ { name: 'npm', cmd: 'npm --version', expected: '' },
2266
+ { name: 'Docker', cmd: 'docker --version', expected: 'Docker' },
2267
+ { name: 'tree', cmd: 'which tree', expected: '/usr/bin/tree' },
2268
+ { name: 'curl', cmd: 'which curl', expected: '/usr/bin/curl' },
2269
+ { name: 'git', cmd: 'which git', expected: '/usr/bin/git' },
2270
+ { name: 'openssl', cmd: 'which openssl', expected: '/usr/bin/openssl' },
2271
+ ];
2272
+ let allOk = true;
2273
+ for (const v of verifications) {
2274
+ const result = await runCmd(v.cmd);
2275
+ const output = result.stdout.trim();
2276
+ const ok = v.expected ? output.includes(v.expected) || result.success : result.success;
2277
+ if (ok) {
2278
+ log.info(` ✅ ${v.name}: ${output || 'OK'}`);
2279
+ }
2280
+ else {
2281
+ log.error(` ❌ ${v.name}: FAILED`);
2282
+ allOk = false;
2283
+ errors.push(`${v.name} verification failed`);
2284
+ }
2285
+ }
2286
+ // ========================================
2287
+ // Summary
2288
+ // ========================================
2289
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
2290
+ log.blank();
2291
+ if (errors.length === 0) {
2292
+ log.box('✅ Phase 0 Complete', [
2293
+ `Mode: ${isLocal ? 'LOCAL' : 'REMOTE'}`,
2294
+ `Time: ${elapsed}s`,
2295
+ '',
2296
+ 'Installed:',
2297
+ ' ✅ Node.js 20',
2298
+ ' ✅ Docker',
2299
+ ' ✅ Essential tools (curl, git, tree, openssl)',
2300
+ '',
2301
+ 'Next: vu server setup',
2302
+ ]);
2303
+ }
2304
+ else {
2305
+ log.box('⚠️ Phase 0 Completed with Errors', [
2306
+ `Mode: ${isLocal ? 'LOCAL' : 'REMOTE'}`,
2307
+ `Time: ${elapsed}s`,
2308
+ '',
2309
+ 'Errors:',
2310
+ ...errors.map(e => ` ❌ ${e}`),
2311
+ '',
2312
+ 'Please fix errors before continuing.',
2313
+ ]);
2314
+ }
2315
+ process.exit(errors.length > 0 ? 1 : 0);
2316
+ }
2317
+ // ============================================
2318
+ // SERVER CHECK - Health Check
2319
+ // ============================================
2320
+ async function serverCheck(options) {
2321
+ log.title('🔍 Server Health Check');
2322
+ log.blank();
2323
+ // Check if running locally
2324
+ const isLocal = options.local || (!options.host && (await listSSHHosts()).length === 0);
2325
+ let runCmd;
2326
+ let host = null;
2327
+ if (isLocal) {
2328
+ log.info('Running in LOCAL mode');
2329
+ runCmd = runLocal;
2330
+ }
2331
+ else {
2332
+ host = await getHost(options.host);
2333
+ if (!host)
2334
+ return;
2335
+ log.info(`Target: ${host.name} (${host.user}@${host.hostname})`);
2336
+ runCmd = (cmd, label) => runSSHLive(host, cmd, label);
2337
+ }
2338
+ log.blank();
2339
+ let issues = [];
2340
+ let warnings = [];
2341
+ // ========================================
2342
+ // 1. System Requirements
2343
+ // ========================================
2344
+ log.info('📦 System Requirements:');
2345
+ const nodeCheck = await runCmd('node --version 2>/dev/null || echo "NOT_INSTALLED"');
2346
+ const nodeVersion = nodeCheck.stdout.trim();
2347
+ if (nodeVersion.startsWith('v20') || nodeVersion.startsWith('v22')) {
2348
+ log.info(` ✅ Node.js: ${nodeVersion}`);
2349
+ }
2350
+ else if (nodeVersion === 'NOT_INSTALLED') {
2351
+ log.error(' ❌ Node.js: Not installed');
2352
+ issues.push('Node.js not installed');
2353
+ }
2354
+ else {
2355
+ log.warn(` ⚠️ Node.js: ${nodeVersion} (v20+ recommended)`);
2356
+ warnings.push(`Node.js version ${nodeVersion}`);
2357
+ }
2358
+ const dockerCheck = await runCmd('docker --version 2>/dev/null || echo "NOT_INSTALLED"');
2359
+ if (dockerCheck.stdout.includes('Docker version')) {
2360
+ log.info(` ✅ Docker: ${dockerCheck.stdout.trim().split(',')[0]}`);
2361
+ }
2362
+ else {
2363
+ log.error(' ❌ Docker: Not installed');
2364
+ issues.push('Docker not installed');
2365
+ }
2366
+ // ========================================
2367
+ // 2. Docker Containers
2368
+ // ========================================
2369
+ log.blank();
2370
+ log.info('🐳 Docker Containers:');
2371
+ const containers = [
2372
+ { name: 'cicore_global_nginx', required: true },
2373
+ { name: 'cicore_global_postgres', required: true },
2374
+ { name: 'cicore_global_redis', required: true },
2375
+ { name: 'cicore_php', required: true },
2376
+ { name: 'cicore_nuxt', required: true },
2377
+ ];
2378
+ for (const c of containers) {
2379
+ const check = await runCmd(`docker ps --filter "name=${c.name}" --format "{{.Status}}" 2>/dev/null`);
2380
+ const status = check.stdout.trim();
2381
+ if (status.includes('Up') && status.includes('healthy')) {
2382
+ log.info(` ✅ ${c.name}: ${status}`);
2383
+ }
2384
+ else if (status.includes('Up')) {
2385
+ log.warn(` ⚠️ ${c.name}: ${status} (not healthy yet)`);
2386
+ warnings.push(`${c.name} not healthy`);
2387
+ }
2388
+ else if (status) {
2389
+ log.error(` ❌ ${c.name}: ${status}`);
2390
+ if (c.required)
2391
+ issues.push(`${c.name} not running`);
2392
+ }
2393
+ else {
2394
+ log.error(` ❌ ${c.name}: Not found`);
2395
+ if (c.required)
2396
+ issues.push(`${c.name} not found`);
2397
+ }
2398
+ }
2399
+ // ========================================
2400
+ // 3. Docker Networks
2401
+ // ========================================
2402
+ log.blank();
2403
+ log.info('🌐 Docker Networks:');
2404
+ const networks = ['vucore_global', 'vucore_proxy'];
2405
+ for (const net of networks) {
2406
+ const check = await runCmd(`docker network ls --filter "name=${net}" --format "{{.Name}}" 2>/dev/null`);
2407
+ if (check.stdout.includes(net)) {
2408
+ log.info(` ✅ ${net}`);
2409
+ }
2410
+ else {
2411
+ log.error(` ❌ ${net}: Not found`);
2412
+ issues.push(`Network ${net} not found`);
2413
+ }
2414
+ }
2415
+ // ========================================
2416
+ // 4. SSL Certificates
2417
+ // ========================================
2418
+ log.blank();
2419
+ log.info('🔒 SSL Certificates:');
2420
+ const sslCheck = await runCmd('ls -la /home/services/nginx/ssl/*.crt 2>/dev/null || echo "NO_CERTS"');
2421
+ if (sslCheck.stdout.includes('NO_CERTS')) {
2422
+ log.warn(' ⚠️ No SSL certificates found');
2423
+ warnings.push('No SSL certificates');
2424
+ }
2425
+ else {
2426
+ const certs = sslCheck.stdout.split('\n').filter(l => l.includes('.crt'));
2427
+ for (const cert of certs) {
2428
+ const certName = cert.split('/').pop()?.replace('.crt', '') || 'unknown';
2429
+ // Check expiry
2430
+ const expiryCheck = await runCmd(`openssl x509 -enddate -noout -in /home/services/nginx/ssl/${certName}.crt 2>/dev/null`);
2431
+ const expiry = expiryCheck.stdout.replace('notAfter=', '').trim();
2432
+ log.info(` ✅ ${certName}: expires ${expiry}`);
2433
+ }
2434
+ }
2435
+ // ========================================
2436
+ // 5. Nginx Config
2437
+ // ========================================
2438
+ log.blank();
2439
+ log.info('⚙️ Nginx Configuration:');
2440
+ const nginxTest = await runCmd('docker exec cicore_global_nginx nginx -t 2>&1');
2441
+ if (nginxTest.stdout.includes('successful') || nginxTest.stderr.includes('successful')) {
2442
+ log.info(' ✅ Nginx config syntax OK');
2443
+ }
2444
+ else {
2445
+ log.error(' ❌ Nginx config has errors');
2446
+ issues.push('Nginx config invalid');
2447
+ }
2448
+ // Check for escape character issues (literal backslash followed by $)
2449
+ const escapeCheck = await runCmd("grep -rE '\\\\\\\\\\$' /home/services/nginx/conf.d/*.conf 2>/dev/null | head -1");
2450
+ if (escapeCheck.stdout.trim()) {
2451
+ log.warn(' ⚠️ Found escaped $ characters in nginx config');
2452
+ warnings.push('Nginx config has escaped $ characters');
2453
+ if (options.fix) {
2454
+ log.info(' 🔧 Fixing escape characters...');
2455
+ await runCmd('for f in /home/services/nginx/conf.d/*.conf; do sed -i "s/\\\\\\\\\\$/\\$/g" "$f"; done');
2456
+ await runCmd('docker exec cicore_global_nginx nginx -s reload');
2457
+ log.success(' ✅ Fixed');
2458
+ }
2459
+ }
2460
+ // ========================================
2461
+ // 6. API Health
2462
+ // ========================================
2463
+ log.blank();
2464
+ log.info('🔌 API Endpoints:');
2465
+ const apiCheck = await runCmd('curl -s -o /dev/null -w "%{http_code}" http://localhost/health 2>/dev/null || echo "FAILED"');
2466
+ if (apiCheck.stdout.trim() === '200') {
2467
+ log.info(' ✅ Health endpoint: OK');
2468
+ }
2469
+ else {
2470
+ log.warn(` ⚠️ Health endpoint: ${apiCheck.stdout.trim()}`);
2471
+ warnings.push('Health endpoint not responding');
2472
+ }
2473
+ // ========================================
2474
+ // 7. Directory Structure
2475
+ // ========================================
2476
+ log.blank();
2477
+ log.info('📁 Directory Structure:');
2478
+ const dirs = [
2479
+ '/home/cores',
2480
+ '/home/services',
2481
+ '/home/services/nginx/conf.d',
2482
+ '/home/services/nginx/ssl',
2483
+ '/home/services/postgres/data',
2484
+ '/home/services/redis/data',
2485
+ ];
2486
+ for (const dir of dirs) {
2487
+ const check = await runCmd(`test -d ${dir} && echo "EXISTS" || echo "MISSING"`);
2488
+ if (check.stdout.includes('EXISTS')) {
2489
+ log.info(` ✅ ${dir}`);
2490
+ }
2491
+ else {
2492
+ log.error(` ❌ ${dir}: Missing`);
2493
+ issues.push(`Directory ${dir} missing`);
2494
+ }
2495
+ }
2496
+ // ========================================
2497
+ // Summary
2498
+ // ========================================
2499
+ log.blank();
2500
+ if (issues.length === 0 && warnings.length === 0) {
2501
+ log.box('✅ All Checks Passed', [
2502
+ 'Server is healthy and ready!',
2503
+ ]);
2504
+ }
2505
+ else if (issues.length === 0) {
2506
+ log.box('⚠️ Warnings Found', [
2507
+ `${warnings.length} warning(s):`,
2508
+ ...warnings.map(w => ` ⚠️ ${w}`),
2509
+ '',
2510
+ 'Server is functional but may need attention.',
2511
+ ]);
2512
+ }
2513
+ else {
2514
+ log.box('❌ Issues Found', [
2515
+ `${issues.length} issue(s), ${warnings.length} warning(s):`,
2516
+ ...issues.map(i => ` ❌ ${i}`),
2517
+ ...warnings.map(w => ` ⚠️ ${w}`),
2518
+ '',
2519
+ 'Run with --fix to attempt automatic fixes.',
2520
+ ]);
2521
+ }
2522
+ process.exit(issues.length > 0 ? 1 : 0);
2523
+ }
2524
+ //# sourceMappingURL=index.js.map