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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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', networkConfig.frontendPort);
297
- apiUrl = buildURL('localhost', networkConfig.apiPort);
302
+ frontendUrl = buildURL('localhost', frontendPort);
303
+ apiUrl = buildURL('localhost', apiPort);
298
304
  } else if (networkConfig.networkType === 'ip') {
299
- frontendUrl = buildURL(networkConfig.publicIP, networkConfig.frontendPort);
300
- apiUrl = buildURL(networkConfig.publicIP, networkConfig.apiPort);
305
+ frontendUrl = buildURL(networkConfig.publicIP, frontendPort);
306
+ apiUrl = buildURL(networkConfig.publicIP, apiPort);
301
307
  } else {
302
- // Domain - assume standard ports with potential reverse proxy
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
- if (isHttps.useHttps) {
311
- frontendUrl = `https://${networkConfig.domainName}`;
312
- apiUrl = `https://api.${networkConfig.domainName}`;
313
- } else {
314
- frontendUrl = buildURL(networkConfig.domainName, networkConfig.frontendPort);
315
- apiUrl = buildURL(networkConfig.domainName, networkConfig.apiPort);
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: networkConfig.frontendPort,
324
- apiPort: networkConfig.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
- // Restart frontend container
243
- console.log(chalk.gray('\nšŸ”„ Restarting frontend container...\n'));
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, 'restart', 'frontend']);
247
- console.log(chalk.green('āœ“ Frontend restarted successfully'));
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 frontend automatically'));
250
- console.log(chalk.gray('Run: ante restart frontend'));
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
+ }