ante-erp-cli 1.6.2 ā 1.6.3
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/package.json +1 -1
- package/src/commands/install.js +48 -16
- package/src/commands/set-domain.js +25 -6
- package/src/utils/nginx.js +258 -0
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -14,6 +14,7 @@ import { pullImages, startServices, waitForServiceHealthy } from '../utils/docke
|
|
|
14
14
|
import { generateDockerCompose } from '../templates/docker-compose.yml.js';
|
|
15
15
|
import { generateEnv } from '../templates/env.js';
|
|
16
16
|
import { detectPublicIPWithFeedback, buildURL, isValidIPv4, isValidDomain } from '../utils/network.js';
|
|
17
|
+
import { configureNginx, requiresNginx } from '../utils/nginx.js';
|
|
17
18
|
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = dirname(__filename);
|
|
@@ -273,6 +274,7 @@ export async function install(options) {
|
|
|
273
274
|
name: 'frontendPort',
|
|
274
275
|
message: 'Frontend port:',
|
|
275
276
|
default: 8080,
|
|
277
|
+
when: (answers) => answers.networkType !== 'domain',
|
|
276
278
|
validate: (input) => {
|
|
277
279
|
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
278
280
|
return true;
|
|
@@ -283,6 +285,7 @@ export async function install(options) {
|
|
|
283
285
|
name: 'apiPort',
|
|
284
286
|
message: 'API port:',
|
|
285
287
|
default: 3001,
|
|
288
|
+
when: (answers) => answers.networkType !== 'domain',
|
|
286
289
|
validate: (input) => {
|
|
287
290
|
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
288
291
|
return true;
|
|
@@ -292,36 +295,48 @@ export async function install(options) {
|
|
|
292
295
|
|
|
293
296
|
// Build URLs based on configuration
|
|
294
297
|
let frontendUrl, apiUrl;
|
|
298
|
+
const frontendPort = networkConfig.frontendPort || 8080;
|
|
299
|
+
const apiPort = networkConfig.apiPort || 3001;
|
|
300
|
+
|
|
295
301
|
if (networkConfig.networkType === 'localhost') {
|
|
296
|
-
frontendUrl = buildURL('localhost',
|
|
297
|
-
apiUrl = buildURL('localhost',
|
|
302
|
+
frontendUrl = buildURL('localhost', frontendPort);
|
|
303
|
+
apiUrl = buildURL('localhost', apiPort);
|
|
298
304
|
} else if (networkConfig.networkType === 'ip') {
|
|
299
|
-
frontendUrl = buildURL(networkConfig.publicIP,
|
|
300
|
-
apiUrl = buildURL(networkConfig.publicIP,
|
|
305
|
+
frontendUrl = buildURL(networkConfig.publicIP, frontendPort);
|
|
306
|
+
apiUrl = buildURL(networkConfig.publicIP, apiPort);
|
|
301
307
|
} else {
|
|
302
|
-
// Domain -
|
|
308
|
+
// Domain - use standard HTTP/HTTPS ports (NGINX will handle reverse proxy)
|
|
303
309
|
const isHttps = await inquirer.prompt([{
|
|
304
310
|
type: 'confirm',
|
|
305
311
|
name: 'useHttps',
|
|
306
|
-
message: 'Is SSL/TLS configured for this domain?',
|
|
312
|
+
message: 'Is SSL/TLS configured for this domain (e.g., via Cloudflare)?',
|
|
307
313
|
default: true
|
|
308
314
|
}]);
|
|
309
315
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
316
|
+
// Ask for API subdomain
|
|
317
|
+
const apiSubdomain = await inquirer.prompt([{
|
|
318
|
+
type: 'input',
|
|
319
|
+
name: 'subdomain',
|
|
320
|
+
message: 'API subdomain (e.g., "api" for api.example.com):',
|
|
321
|
+
default: 'api',
|
|
322
|
+
validate: (input) => {
|
|
323
|
+
if (!input) return 'Subdomain is required';
|
|
324
|
+
if (!/^[a-z0-9-]+$/.test(input)) return 'Invalid subdomain format';
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}]);
|
|
328
|
+
|
|
329
|
+
const protocol = isHttps.useHttps ? 'https' : 'http';
|
|
330
|
+
frontendUrl = `${protocol}://${networkConfig.domainName}`;
|
|
331
|
+
apiUrl = `${protocol}://${apiSubdomain.subdomain}.${networkConfig.domainName}`;
|
|
317
332
|
}
|
|
318
333
|
|
|
319
334
|
config = {
|
|
320
335
|
...baseConfig,
|
|
321
336
|
frontendDomain: frontendUrl,
|
|
322
337
|
apiDomain: apiUrl,
|
|
323
|
-
frontendPort
|
|
324
|
-
apiPort
|
|
338
|
+
frontendPort,
|
|
339
|
+
apiPort
|
|
325
340
|
};
|
|
326
341
|
} else {
|
|
327
342
|
// Non-interactive mode - use options or defaults with IP detection
|
|
@@ -554,7 +569,24 @@ Support: support@ante.ph
|
|
|
554
569
|
});
|
|
555
570
|
|
|
556
571
|
await tasks.run();
|
|
557
|
-
|
|
572
|
+
|
|
573
|
+
// Configure NGINX if needed (for domains or HTTPS)
|
|
574
|
+
if (requiresNginx(config.frontendDomain || 'http://localhost:8080', config.apiDomain || 'http://localhost:3001')) {
|
|
575
|
+
console.log(chalk.gray('\nš§ Setting up reverse proxy...\n'));
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
await configureNginx({
|
|
579
|
+
frontendDomain: config.frontendDomain || 'http://localhost:8080',
|
|
580
|
+
apiDomain: config.apiDomain || 'http://localhost:3001',
|
|
581
|
+
frontendPort: config.frontendPort || 8080,
|
|
582
|
+
apiPort: config.apiPort || 3001
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.log(chalk.yellow('\nā NGINX configuration failed:', error.message));
|
|
586
|
+
console.log(chalk.gray('You may need to configure reverse proxy manually\n'));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
558
590
|
// Save installation configuration
|
|
559
591
|
const installConfig = {
|
|
560
592
|
installPath: config.installDir,
|
|
@@ -6,6 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { execa } from 'execa';
|
|
7
7
|
import { getInstallDir } from '../utils/config.js';
|
|
8
8
|
import { detectPublicIPWithFeedback, buildURL, isValidIPv4 } from '../utils/network.js';
|
|
9
|
+
import { configureNginx, requiresNginx } from '../utils/nginx.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Validate URL format
|
|
@@ -239,15 +240,33 @@ export async function setDomain(options) {
|
|
|
239
240
|
|
|
240
241
|
console.log(chalk.green('ā Configuration updated'));
|
|
241
242
|
|
|
242
|
-
//
|
|
243
|
-
|
|
243
|
+
// Configure NGINX if needed (for domains or HTTPS)
|
|
244
|
+
if (requiresNginx(frontendUrl, apiUrl)) {
|
|
245
|
+
console.log(chalk.gray('\nš§ Setting up reverse proxy...\n'));
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await configureNginx({
|
|
249
|
+
frontendDomain: frontendUrl,
|
|
250
|
+
apiDomain: apiUrl,
|
|
251
|
+
frontendPort: 8080,
|
|
252
|
+
apiPort: 3001
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.log(chalk.yellow('\nā NGINX configuration failed:', error.message));
|
|
256
|
+
console.log(chalk.gray('You may need to configure reverse proxy manually\n'));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Restart containers to apply new environment variables
|
|
261
|
+
console.log(chalk.gray('\nš Restarting containers...\n'));
|
|
244
262
|
|
|
245
263
|
try {
|
|
246
|
-
await execa('docker', ['compose', '-f', composeFile, '
|
|
247
|
-
|
|
264
|
+
await execa('docker', ['compose', '-f', composeFile, 'down'], { cwd: installDir });
|
|
265
|
+
await execa('docker', ['compose', '-f', composeFile, 'up', '-d'], { cwd: installDir });
|
|
266
|
+
console.log(chalk.green('ā Containers restarted successfully'));
|
|
248
267
|
} catch (error) {
|
|
249
|
-
console.log(chalk.yellow('ā Could not restart
|
|
250
|
-
console.log(chalk.gray('Run:
|
|
268
|
+
console.log(chalk.yellow('ā Could not restart containers automatically'));
|
|
269
|
+
console.log(chalk.gray('Run: docker compose down && docker compose up -d'));
|
|
251
270
|
}
|
|
252
271
|
|
|
253
272
|
// Show summary
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if NGINX is installed
|
|
9
|
+
* @returns {Promise<boolean>} True if NGINX is installed
|
|
10
|
+
*/
|
|
11
|
+
export async function isNginxInstalled() {
|
|
12
|
+
try {
|
|
13
|
+
await execa('which', ['nginx']);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Install NGINX on Ubuntu/Debian
|
|
22
|
+
* @param {Object} spinner - Ora spinner instance
|
|
23
|
+
* @returns {Promise<void>}
|
|
24
|
+
*/
|
|
25
|
+
export async function installNginx(spinner) {
|
|
26
|
+
try {
|
|
27
|
+
spinner.text = 'Installing NGINX...';
|
|
28
|
+
|
|
29
|
+
// Update package list
|
|
30
|
+
await execa('apt-get', ['update', '-qq'], { stdio: 'pipe' });
|
|
31
|
+
|
|
32
|
+
// Install NGINX
|
|
33
|
+
await execa('apt-get', ['install', '-y', '-qq', 'nginx'], { stdio: 'pipe' });
|
|
34
|
+
|
|
35
|
+
// Enable NGINX to start on boot
|
|
36
|
+
await execa('systemctl', ['enable', 'nginx'], { stdio: 'pipe' });
|
|
37
|
+
|
|
38
|
+
spinner.succeed('NGINX installed successfully');
|
|
39
|
+
return true;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
spinner.fail('Failed to install NGINX');
|
|
42
|
+
throw new Error(`NGINX installation failed: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate NGINX configuration for ANTE
|
|
48
|
+
* @param {Object} config - Configuration object
|
|
49
|
+
* @param {string} config.frontendDomain - Frontend domain (e.g., ante.example.com)
|
|
50
|
+
* @param {string} config.apiDomain - API domain (e.g., api.ante.example.com)
|
|
51
|
+
* @param {number} config.frontendPort - Frontend Docker port (default: 8080)
|
|
52
|
+
* @param {number} config.apiPort - API Docker port (default: 3001)
|
|
53
|
+
* @returns {string} NGINX configuration content
|
|
54
|
+
*/
|
|
55
|
+
export function generateNginxConfig(config) {
|
|
56
|
+
const { frontendDomain, apiDomain, frontendPort = 8080, apiPort = 3001 } = config;
|
|
57
|
+
|
|
58
|
+
// Extract domain without protocol
|
|
59
|
+
const frontendHost = frontendDomain.replace(/^https?:\/\//, '').replace(/:\d+$/, '');
|
|
60
|
+
const apiHost = apiDomain.replace(/^https?:\/\//, '').replace(/:\d+$/, '');
|
|
61
|
+
|
|
62
|
+
return `# ANTE Frontend Configuration
|
|
63
|
+
server {
|
|
64
|
+
listen 80;
|
|
65
|
+
listen [::]:80;
|
|
66
|
+
server_name ${frontendHost};
|
|
67
|
+
|
|
68
|
+
# Increase buffer sizes for large headers
|
|
69
|
+
client_header_buffer_size 16k;
|
|
70
|
+
large_client_header_buffers 4 16k;
|
|
71
|
+
|
|
72
|
+
# Increase body size for file uploads
|
|
73
|
+
client_max_body_size 100M;
|
|
74
|
+
|
|
75
|
+
location / {
|
|
76
|
+
proxy_pass http://localhost:${frontendPort};
|
|
77
|
+
proxy_http_version 1.1;
|
|
78
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
79
|
+
proxy_set_header Connection 'upgrade';
|
|
80
|
+
proxy_set_header Host $host;
|
|
81
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
82
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
83
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
84
|
+
proxy_cache_bypass $http_upgrade;
|
|
85
|
+
|
|
86
|
+
# Timeout settings
|
|
87
|
+
proxy_connect_timeout 60s;
|
|
88
|
+
proxy_send_timeout 60s;
|
|
89
|
+
proxy_read_timeout 60s;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# ANTE API Configuration
|
|
94
|
+
server {
|
|
95
|
+
listen 80;
|
|
96
|
+
listen [::]:80;
|
|
97
|
+
server_name ${apiHost};
|
|
98
|
+
|
|
99
|
+
# Increase buffer sizes for large headers
|
|
100
|
+
client_header_buffer_size 16k;
|
|
101
|
+
large_client_header_buffers 4 16k;
|
|
102
|
+
|
|
103
|
+
# Increase body size for file uploads
|
|
104
|
+
client_max_body_size 100M;
|
|
105
|
+
|
|
106
|
+
location / {
|
|
107
|
+
proxy_pass http://localhost:${apiPort};
|
|
108
|
+
proxy_http_version 1.1;
|
|
109
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
110
|
+
proxy_set_header Connection 'upgrade';
|
|
111
|
+
proxy_set_header Host $host;
|
|
112
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
113
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
114
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
115
|
+
proxy_cache_bypass $http_upgrade;
|
|
116
|
+
|
|
117
|
+
# Timeout settings
|
|
118
|
+
proxy_connect_timeout 60s;
|
|
119
|
+
proxy_send_timeout 60s;
|
|
120
|
+
proxy_read_timeout 60s;
|
|
121
|
+
|
|
122
|
+
# WebSocket support
|
|
123
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
124
|
+
proxy_set_header X-Forwarded-Server $host;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Configure NGINX for ANTE domains
|
|
132
|
+
* @param {Object} config - Configuration object
|
|
133
|
+
* @param {string} config.frontendDomain - Frontend domain
|
|
134
|
+
* @param {string} config.apiDomain - API domain
|
|
135
|
+
* @param {number} [config.frontendPort=8080] - Frontend Docker port
|
|
136
|
+
* @param {number} [config.apiPort=3001] - API Docker port
|
|
137
|
+
* @returns {Promise<void>}
|
|
138
|
+
*/
|
|
139
|
+
export async function configureNginx(config) {
|
|
140
|
+
const spinner = ora('Configuring NGINX reverse proxy...').start();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Check if NGINX is installed
|
|
144
|
+
const nginxInstalled = await isNginxInstalled();
|
|
145
|
+
|
|
146
|
+
if (!nginxInstalled) {
|
|
147
|
+
spinner.text = 'NGINX not found, installing...';
|
|
148
|
+
await installNginx(spinner);
|
|
149
|
+
spinner.start('Configuring NGINX reverse proxy...');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Ensure sites-enabled directory exists
|
|
153
|
+
const sitesEnabledDir = '/etc/nginx/sites-enabled';
|
|
154
|
+
if (!existsSync(sitesEnabledDir)) {
|
|
155
|
+
mkdirSync(sitesEnabledDir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Generate NGINX configuration
|
|
159
|
+
const nginxConfig = generateNginxConfig(config);
|
|
160
|
+
|
|
161
|
+
// Write configuration to sites-enabled (direct, not using sites-available)
|
|
162
|
+
const configPath = join(sitesEnabledDir, 'ante');
|
|
163
|
+
writeFileSync(configPath, nginxConfig);
|
|
164
|
+
|
|
165
|
+
spinner.text = 'Testing NGINX configuration...';
|
|
166
|
+
|
|
167
|
+
// Test NGINX configuration
|
|
168
|
+
try {
|
|
169
|
+
await execa('nginx', ['-t'], { stdio: 'pipe' });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
spinner.fail('NGINX configuration test failed');
|
|
172
|
+
throw new Error(`Invalid NGINX configuration: ${error.message}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
spinner.text = 'Reloading NGINX...';
|
|
176
|
+
|
|
177
|
+
// Reload NGINX to apply changes
|
|
178
|
+
try {
|
|
179
|
+
await execa('systemctl', ['reload', 'nginx'], { stdio: 'pipe' });
|
|
180
|
+
} catch {
|
|
181
|
+
// If reload fails, try restart
|
|
182
|
+
await execa('systemctl', ['restart', 'nginx'], { stdio: 'pipe' });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Verify NGINX is running
|
|
186
|
+
const { stdout } = await execa('systemctl', ['is-active', 'nginx'], { stdio: 'pipe' });
|
|
187
|
+
if (stdout.trim() !== 'active') {
|
|
188
|
+
throw new Error('NGINX failed to start');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
spinner.succeed('NGINX configured successfully');
|
|
192
|
+
|
|
193
|
+
// Show helpful information
|
|
194
|
+
console.log(chalk.gray('\nš NGINX Configuration:'));
|
|
195
|
+
console.log(chalk.gray(` Config file: ${configPath}`));
|
|
196
|
+
console.log(chalk.gray(` Frontend: ${config.frontendDomain} ā localhost:${config.frontendPort || 8080}`));
|
|
197
|
+
console.log(chalk.gray(` API: ${config.apiDomain} ā localhost:${config.apiPort || 3001}`));
|
|
198
|
+
|
|
199
|
+
} catch (error) {
|
|
200
|
+
spinner.fail('Failed to configure NGINX');
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove ANTE NGINX configuration
|
|
207
|
+
* @returns {Promise<void>}
|
|
208
|
+
*/
|
|
209
|
+
export async function removeNginxConfig() {
|
|
210
|
+
const spinner = ora('Removing NGINX configuration...').start();
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const configPath = '/etc/nginx/sites-enabled/ante';
|
|
214
|
+
|
|
215
|
+
if (existsSync(configPath)) {
|
|
216
|
+
await execa('rm', [configPath]);
|
|
217
|
+
|
|
218
|
+
// Reload NGINX
|
|
219
|
+
try {
|
|
220
|
+
await execa('systemctl', ['reload', 'nginx'], { stdio: 'pipe' });
|
|
221
|
+
} catch {
|
|
222
|
+
// Ignore if NGINX is not running
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
spinner.succeed('NGINX configuration removed');
|
|
226
|
+
} else {
|
|
227
|
+
spinner.info('No NGINX configuration found');
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
spinner.fail('Failed to remove NGINX configuration');
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if domains require NGINX (i.e., using HTTPS or standard HTTP without port)
|
|
237
|
+
* @param {string} frontendUrl - Frontend URL
|
|
238
|
+
* @param {string} apiUrl - API URL
|
|
239
|
+
* @returns {boolean} True if NGINX is required
|
|
240
|
+
*/
|
|
241
|
+
export function requiresNginx(frontendUrl, apiUrl) {
|
|
242
|
+
// NGINX required if:
|
|
243
|
+
// 1. Using HTTPS
|
|
244
|
+
// 2. Using HTTP without explicit port (implies port 80)
|
|
245
|
+
// 3. Using a domain name (not IP address)
|
|
246
|
+
|
|
247
|
+
const frontendUsesHttps = frontendUrl.startsWith('https://');
|
|
248
|
+
const apiUsesHttps = apiUrl.startsWith('https://');
|
|
249
|
+
|
|
250
|
+
const frontendNoPort = !frontendUrl.match(/:\d+$/);
|
|
251
|
+
const apiNoPort = !apiUrl.match(/:\d+$/);
|
|
252
|
+
|
|
253
|
+
const frontendIsDomain = !frontendUrl.match(/https?:\/\/\d+\.\d+\.\d+\.\d+/);
|
|
254
|
+
const apiIsDomain = !apiUrl.match(/https?:\/\/\d+\.\d+\.\d+\.\d+/);
|
|
255
|
+
|
|
256
|
+
return (frontendUsesHttps || apiUsesHttps) ||
|
|
257
|
+
((frontendNoPort || apiNoPort) && (frontendIsDomain || apiIsDomain));
|
|
258
|
+
}
|