@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.
- package/bin/ci.js +13 -0
- package/dist/commands/addon/api-actions.d.ts +45 -0
- package/dist/commands/addon/api-actions.d.ts.map +1 -0
- package/dist/commands/addon/api-actions.js +281 -0
- package/dist/commands/addon/api-actions.js.map +1 -0
- package/dist/commands/addon/build.d.ts +11 -0
- package/dist/commands/addon/build.d.ts.map +1 -0
- package/dist/commands/addon/build.js +182 -0
- package/dist/commands/addon/build.js.map +1 -0
- package/dist/commands/addon/create.d.ts +11 -0
- package/dist/commands/addon/create.d.ts.map +1 -0
- package/dist/commands/addon/create.js +1186 -0
- package/dist/commands/addon/create.js.map +1 -0
- package/dist/commands/addon/delete.d.ts +13 -0
- package/dist/commands/addon/delete.d.ts.map +1 -0
- package/dist/commands/addon/delete.js +83 -0
- package/dist/commands/addon/delete.js.map +1 -0
- package/dist/commands/addon/deploy.d.ts +27 -0
- package/dist/commands/addon/deploy.d.ts.map +1 -0
- package/dist/commands/addon/deploy.js +459 -0
- package/dist/commands/addon/deploy.js.map +1 -0
- package/dist/commands/addon/dev-deploy.d.ts +31 -0
- package/dist/commands/addon/dev-deploy.d.ts.map +1 -0
- package/dist/commands/addon/dev-deploy.js +128 -0
- package/dist/commands/addon/dev-deploy.js.map +1 -0
- package/dist/commands/addon/dev.d.ts +36 -0
- package/dist/commands/addon/dev.d.ts.map +1 -0
- package/dist/commands/addon/dev.js +323 -0
- package/dist/commands/addon/dev.js.map +1 -0
- package/dist/commands/addon/extract-classes.d.ts +23 -0
- package/dist/commands/addon/extract-classes.d.ts.map +1 -0
- package/dist/commands/addon/extract-classes.js +281 -0
- package/dist/commands/addon/extract-classes.js.map +1 -0
- package/dist/commands/addon/generate-safelist.d.ts +24 -0
- package/dist/commands/addon/generate-safelist.d.ts.map +1 -0
- package/dist/commands/addon/generate-safelist.js +276 -0
- package/dist/commands/addon/generate-safelist.js.map +1 -0
- package/dist/commands/addon/index.d.ts +19 -0
- package/dist/commands/addon/index.d.ts.map +1 -0
- package/dist/commands/addon/index.js +296 -0
- package/dist/commands/addon/index.js.map +1 -0
- package/dist/commands/addon/init-repo.d.ts +25 -0
- package/dist/commands/addon/init-repo.d.ts.map +1 -0
- package/dist/commands/addon/init-repo.js +171 -0
- package/dist/commands/addon/init-repo.js.map +1 -0
- package/dist/commands/addon/install.d.ts +23 -0
- package/dist/commands/addon/install.d.ts.map +1 -0
- package/dist/commands/addon/install.js +84 -0
- package/dist/commands/addon/install.js.map +1 -0
- package/dist/commands/addon/list.d.ts +10 -0
- package/dist/commands/addon/list.d.ts.map +1 -0
- package/dist/commands/addon/list.js +102 -0
- package/dist/commands/addon/list.js.map +1 -0
- package/dist/commands/addon/manifest-refresh.d.ts +17 -0
- package/dist/commands/addon/manifest-refresh.d.ts.map +1 -0
- package/dist/commands/addon/manifest-refresh.js +48 -0
- package/dist/commands/addon/manifest-refresh.js.map +1 -0
- package/dist/commands/addon/migrate.d.ts +40 -0
- package/dist/commands/addon/migrate.d.ts.map +1 -0
- package/dist/commands/addon/migrate.js +236 -0
- package/dist/commands/addon/migrate.js.map +1 -0
- package/dist/commands/addon/publish.d.ts +33 -0
- package/dist/commands/addon/publish.d.ts.map +1 -0
- package/dist/commands/addon/publish.js +236 -0
- package/dist/commands/addon/publish.js.map +1 -0
- package/dist/commands/addon/scaffold-quality.d.ts +21 -0
- package/dist/commands/addon/scaffold-quality.d.ts.map +1 -0
- package/dist/commands/addon/scaffold-quality.js +90 -0
- package/dist/commands/addon/scaffold-quality.js.map +1 -0
- package/dist/commands/addon/sign.d.ts +9 -0
- package/dist/commands/addon/sign.d.ts.map +1 -0
- package/dist/commands/addon/sign.js +83 -0
- package/dist/commands/addon/sign.js.map +1 -0
- package/dist/commands/addon/toggle.d.ts +6 -0
- package/dist/commands/addon/toggle.d.ts.map +1 -0
- package/dist/commands/addon/toggle.js +46 -0
- package/dist/commands/addon/toggle.js.map +1 -0
- package/dist/commands/agent/index.d.ts +34 -0
- package/dist/commands/agent/index.d.ts.map +1 -0
- package/dist/commands/agent/index.js +564 -0
- package/dist/commands/agent/index.js.map +1 -0
- package/dist/commands/brand/index.d.ts +54 -0
- package/dist/commands/brand/index.d.ts.map +1 -0
- package/dist/commands/brand/index.js +367 -0
- package/dist/commands/brand/index.js.map +1 -0
- package/dist/commands/build/index.d.ts +53 -0
- package/dist/commands/build/index.d.ts.map +1 -0
- package/dist/commands/build/index.js +726 -0
- package/dist/commands/build/index.js.map +1 -0
- package/dist/commands/cache/flush-local.d.ts +31 -0
- package/dist/commands/cache/flush-local.d.ts.map +1 -0
- package/dist/commands/cache/flush-local.js +161 -0
- package/dist/commands/cache/flush-local.js.map +1 -0
- package/dist/commands/cache/index.d.ts +14 -0
- package/dist/commands/cache/index.d.ts.map +1 -0
- package/dist/commands/cache/index.js +453 -0
- package/dist/commands/cache/index.js.map +1 -0
- package/dist/commands/check/index.d.ts +8 -0
- package/dist/commands/check/index.d.ts.map +1 -0
- package/dist/commands/check/index.js +1316 -0
- package/dist/commands/check/index.js.map +1 -0
- package/dist/commands/cloudflare/index.d.ts +8 -0
- package/dist/commands/cloudflare/index.d.ts.map +1 -0
- package/dist/commands/cloudflare/index.js +453 -0
- package/dist/commands/cloudflare/index.js.map +1 -0
- package/dist/commands/core/create.d.ts +12 -0
- package/dist/commands/core/create.d.ts.map +1 -0
- package/dist/commands/core/create.js +206 -0
- package/dist/commands/core/create.js.map +1 -0
- package/dist/commands/core/delete.d.ts +11 -0
- package/dist/commands/core/delete.d.ts.map +1 -0
- package/dist/commands/core/delete.js +64 -0
- package/dist/commands/core/delete.js.map +1 -0
- package/dist/commands/core/env.d.ts +12 -0
- package/dist/commands/core/env.d.ts.map +1 -0
- package/dist/commands/core/env.js +95 -0
- package/dist/commands/core/env.js.map +1 -0
- package/dist/commands/core/health.d.ts +6 -0
- package/dist/commands/core/health.d.ts.map +1 -0
- package/dist/commands/core/health.js +215 -0
- package/dist/commands/core/health.js.map +1 -0
- package/dist/commands/core/index.d.ts +15 -0
- package/dist/commands/core/index.d.ts.map +1 -0
- package/dist/commands/core/index.js +86 -0
- package/dist/commands/core/index.js.map +1 -0
- package/dist/commands/core/list.d.ts +11 -0
- package/dist/commands/core/list.d.ts.map +1 -0
- package/dist/commands/core/list.js +58 -0
- package/dist/commands/core/list.js.map +1 -0
- package/dist/commands/core/rebuild.d.ts +13 -0
- package/dist/commands/core/rebuild.d.ts.map +1 -0
- package/dist/commands/core/rebuild.js +119 -0
- package/dist/commands/core/rebuild.js.map +1 -0
- package/dist/commands/db/index.d.ts +23 -0
- package/dist/commands/db/index.d.ts.map +1 -0
- package/dist/commands/db/index.js +355 -0
- package/dist/commands/db/index.js.map +1 -0
- package/dist/commands/db/promote-silo.d.ts +320 -0
- package/dist/commands/db/promote-silo.d.ts.map +1 -0
- package/dist/commands/db/promote-silo.js +930 -0
- package/dist/commands/db/promote-silo.js.map +1 -0
- package/dist/commands/db/relocate.d.ts +41 -0
- package/dist/commands/db/relocate.d.ts.map +1 -0
- package/dist/commands/db/relocate.js +482 -0
- package/dist/commands/db/relocate.js.map +1 -0
- package/dist/commands/db/rollback-silo.d.ts +44 -0
- package/dist/commands/db/rollback-silo.d.ts.map +1 -0
- package/dist/commands/db/rollback-silo.js +402 -0
- package/dist/commands/db/rollback-silo.js.map +1 -0
- package/dist/commands/deploy/index.d.ts +26 -0
- package/dist/commands/deploy/index.d.ts.map +1 -0
- package/dist/commands/deploy/index.js +107 -0
- package/dist/commands/deploy/index.js.map +1 -0
- package/dist/commands/devops/index.d.ts +6 -0
- package/dist/commands/devops/index.d.ts.map +1 -0
- package/dist/commands/devops/index.js +220 -0
- package/dist/commands/devops/index.js.map +1 -0
- package/dist/commands/domain/index.d.ts +8 -0
- package/dist/commands/domain/index.d.ts.map +1 -0
- package/dist/commands/domain/index.js +386 -0
- package/dist/commands/domain/index.js.map +1 -0
- package/dist/commands/image/index.d.ts +8 -0
- package/dist/commands/image/index.d.ts.map +1 -0
- package/dist/commands/image/index.js +308 -0
- package/dist/commands/image/index.js.map +1 -0
- package/dist/commands/install/factory-reset.d.ts +21 -0
- package/dist/commands/install/factory-reset.d.ts.map +1 -0
- package/dist/commands/install/factory-reset.js +83 -0
- package/dist/commands/install/factory-reset.js.map +1 -0
- package/dist/commands/install/index.d.ts +17 -0
- package/dist/commands/install/index.d.ts.map +1 -0
- package/dist/commands/install/index.js +44 -0
- package/dist/commands/install/index.js.map +1 -0
- package/dist/commands/install/install.d.ts +35 -0
- package/dist/commands/install/install.d.ts.map +1 -0
- package/dist/commands/install/install.js +171 -0
- package/dist/commands/install/install.js.map +1 -0
- package/dist/commands/login/index.d.ts +15 -0
- package/dist/commands/login/index.d.ts.map +1 -0
- package/dist/commands/login/index.js +58 -0
- package/dist/commands/login/index.js.map +1 -0
- package/dist/commands/nginx/index.d.ts +11 -0
- package/dist/commands/nginx/index.d.ts.map +1 -0
- package/dist/commands/nginx/index.js +580 -0
- package/dist/commands/nginx/index.js.map +1 -0
- package/dist/commands/server/bootstrap.d.ts +25 -0
- package/dist/commands/server/bootstrap.d.ts.map +1 -0
- package/dist/commands/server/bootstrap.js +260 -0
- package/dist/commands/server/bootstrap.js.map +1 -0
- package/dist/commands/server/index.d.ts +8 -0
- package/dist/commands/server/index.d.ts.map +1 -0
- package/dist/commands/server/index.js +2524 -0
- package/dist/commands/server/index.js.map +1 -0
- package/dist/commands/setup/index.d.ts +34 -0
- package/dist/commands/setup/index.d.ts.map +1 -0
- package/dist/commands/setup/index.js +423 -0
- package/dist/commands/setup/index.js.map +1 -0
- package/dist/commands/ssl/index.d.ts +8 -0
- package/dist/commands/ssl/index.d.ts.map +1 -0
- package/dist/commands/ssl/index.js +275 -0
- package/dist/commands/ssl/index.js.map +1 -0
- package/dist/commands/superadmin/index.d.ts +16 -0
- package/dist/commands/superadmin/index.d.ts.map +1 -0
- package/dist/commands/superadmin/index.js +81 -0
- package/dist/commands/superadmin/index.js.map +1 -0
- package/dist/commands/tenant/index.d.ts +6 -0
- package/dist/commands/tenant/index.d.ts.map +1 -0
- package/dist/commands/tenant/index.js +192 -0
- package/dist/commands/tenant/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +107 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/addon-sign.d.ts +23 -0
- package/dist/lib/addon-sign.d.ts.map +1 -0
- package/dist/lib/addon-sign.js +39 -0
- package/dist/lib/addon-sign.js.map +1 -0
- package/dist/lib/addon-sign.test.d.ts +2 -0
- package/dist/lib/addon-sign.test.d.ts.map +1 -0
- package/dist/lib/addon-sign.test.js +27 -0
- package/dist/lib/addon-sign.test.js.map +1 -0
- package/dist/lib/cdn.d.ts +25 -0
- package/dist/lib/cdn.d.ts.map +1 -0
- package/dist/lib/cdn.js +131 -0
- package/dist/lib/cdn.js.map +1 -0
- package/dist/lib/cloudflare.d.ts +133 -0
- package/dist/lib/cloudflare.d.ts.map +1 -0
- package/dist/lib/cloudflare.js +435 -0
- package/dist/lib/cloudflare.js.map +1 -0
- package/dist/lib/config.d.ts +96 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +132 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/env.d.ts +8 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +64 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/hosts.d.ts +194 -0
- package/dist/lib/hosts.d.ts.map +1 -0
- package/dist/lib/hosts.js +183 -0
- package/dist/lib/hosts.js.map +1 -0
- package/dist/lib/logger.d.ts +68 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +130 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/nginx-config.d.ts +78 -0
- package/dist/lib/nginx-config.d.ts.map +1 -0
- package/dist/lib/nginx-config.js +736 -0
- package/dist/lib/nginx-config.js.map +1 -0
- package/dist/lib/ops/addon-dev.d.ts +93 -0
- package/dist/lib/ops/addon-dev.d.ts.map +1 -0
- package/dist/lib/ops/addon-dev.js +237 -0
- package/dist/lib/ops/addon-dev.js.map +1 -0
- package/dist/lib/ops/addon-quality.d.ts +38 -0
- package/dist/lib/ops/addon-quality.d.ts.map +1 -0
- package/dist/lib/ops/addon-quality.js +338 -0
- package/dist/lib/ops/addon-quality.js.map +1 -0
- package/dist/lib/ops/addon-routes.d.ts +49 -0
- package/dist/lib/ops/addon-routes.d.ts.map +1 -0
- package/dist/lib/ops/addon-routes.js +189 -0
- package/dist/lib/ops/addon-routes.js.map +1 -0
- package/dist/lib/ops/addon.d.ts +120 -0
- package/dist/lib/ops/addon.d.ts.map +1 -0
- package/dist/lib/ops/addon.js +260 -0
- package/dist/lib/ops/addon.js.map +1 -0
- package/dist/lib/ops/cdn.d.ts +87 -0
- package/dist/lib/ops/cdn.d.ts.map +1 -0
- package/dist/lib/ops/cdn.js +170 -0
- package/dist/lib/ops/cdn.js.map +1 -0
- package/dist/lib/ops/cf.d.ts +36 -0
- package/dist/lib/ops/cf.d.ts.map +1 -0
- package/dist/lib/ops/cf.js +114 -0
- package/dist/lib/ops/cf.js.map +1 -0
- package/dist/lib/ops/compose.d.ts +95 -0
- package/dist/lib/ops/compose.d.ts.map +1 -0
- package/dist/lib/ops/compose.js +165 -0
- package/dist/lib/ops/compose.js.map +1 -0
- package/dist/lib/ops/core.d.ts +117 -0
- package/dist/lib/ops/core.d.ts.map +1 -0
- package/dist/lib/ops/core.js +322 -0
- package/dist/lib/ops/core.js.map +1 -0
- package/dist/lib/ops/db.d.ts +116 -0
- package/dist/lib/ops/db.d.ts.map +1 -0
- package/dist/lib/ops/db.js +351 -0
- package/dist/lib/ops/db.js.map +1 -0
- package/dist/lib/ops/dns.d.ts +111 -0
- package/dist/lib/ops/dns.d.ts.map +1 -0
- package/dist/lib/ops/dns.js +306 -0
- package/dist/lib/ops/dns.js.map +1 -0
- package/dist/lib/ops/image.d.ts +94 -0
- package/dist/lib/ops/image.d.ts.map +1 -0
- package/dist/lib/ops/image.js +159 -0
- package/dist/lib/ops/image.js.map +1 -0
- package/dist/lib/ops/nginx.d.ts +114 -0
- package/dist/lib/ops/nginx.d.ts.map +1 -0
- package/dist/lib/ops/nginx.js +388 -0
- package/dist/lib/ops/nginx.js.map +1 -0
- package/dist/lib/ops/redis.d.ts +7 -0
- package/dist/lib/ops/redis.d.ts.map +1 -0
- package/dist/lib/ops/redis.js +35 -0
- package/dist/lib/ops/redis.js.map +1 -0
- package/dist/lib/ops/ssh.d.ts +127 -0
- package/dist/lib/ops/ssh.d.ts.map +1 -0
- package/dist/lib/ops/ssh.js +269 -0
- package/dist/lib/ops/ssh.js.map +1 -0
- package/dist/lib/prompts.d.ts +46 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +113 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/sast.d.ts +43 -0
- package/dist/lib/sast.d.ts.map +1 -0
- package/dist/lib/sast.js +79 -0
- package/dist/lib/sast.js.map +1 -0
- package/dist/lib/sast.test.d.ts +2 -0
- package/dist/lib/sast.test.d.ts.map +1 -0
- package/dist/lib/sast.test.js +33 -0
- package/dist/lib/sast.test.js.map +1 -0
- package/dist/lib/shell.d.ts +61 -0
- package/dist/lib/shell.d.ts.map +1 -0
- package/dist/lib/shell.js +183 -0
- package/dist/lib/shell.js.map +1 -0
- package/dist/lib/ssh-config.d.ts +37 -0
- package/dist/lib/ssh-config.d.ts.map +1 -0
- package/dist/lib/ssh-config.js +122 -0
- package/dist/lib/ssh-config.js.map +1 -0
- package/dist/lib/tenant-scope.d.ts +38 -0
- package/dist/lib/tenant-scope.d.ts.map +1 -0
- package/dist/lib/tenant-scope.js +129 -0
- package/dist/lib/tenant-scope.js.map +1 -0
- package/dist/lib/tenant-scope.test.d.ts +2 -0
- package/dist/lib/tenant-scope.test.d.ts.map +1 -0
- package/dist/lib/tenant-scope.test.js +223 -0
- package/dist/lib/tenant-scope.test.js.map +1 -0
- package/package.json +58 -0
- package/templates/bootstrap/.env.template +54 -0
- package/templates/bootstrap/docker-compose.yml +145 -0
- 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
|