ante-erp-cli 1.11.16 → 1.11.18

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.11.16",
3
+ "version": "1.11.18",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,19 +1,18 @@
1
1
  import chalk from 'chalk';
2
2
  import boxen from 'boxen';
3
- import inquirer from 'inquirer';
4
3
  import ora from 'ora';
5
4
  import { Listr } from 'listr2';
6
5
  import { execa } from 'execa';
7
- import { mkdirSync, writeFileSync, existsSync, renameSync } from 'fs';
6
+ import { mkdirSync, writeFileSync, existsSync, renameSync, readFileSync } from 'fs';
8
7
  import { join, dirname } from 'path';
9
8
  import { fileURLToPath } from 'url';
10
9
  import { runSystemChecks, checkAndInstallDocker } from '../utils/validation.js';
11
10
  import { generateCredentials } from '../utils/password.js';
12
11
  import { saveInstallConfig, detectInstallation } from '../utils/config.js';
13
- import { pullImages, startServices, waitForServiceHealthy, runMigrations } from '../utils/docker.js';
12
+ import { pullImagesSilent, startServicesSilent, waitForServiceHealthy, runMigrations } from '../utils/docker.js';
14
13
  import { generateDockerCompose } from '../templates/docker-compose.yml.js';
15
14
  import { generateEnv } from '../templates/env.js';
16
- import { detectPublicIPWithFeedback, buildURL, isValidIPv4, isValidDomain } from '../utils/network.js';
15
+ import { detectPublicIPWithFeedback, buildURL } from '../utils/network.js';
17
16
  import { configureNginx, requiresNginx } from '../utils/nginx.js';
18
17
 
19
18
  const __filename = fileURLToPath(import.meta.url);
@@ -42,50 +41,14 @@ function backupEnvFile(envPath) {
42
41
  }
43
42
 
44
43
  /**
45
- * Check for existing .env files and warn user
46
- * @param {string} installDir - Installation directory
47
- * @returns {boolean} - True if user confirms to continue
44
+ * Format step title with numbering
45
+ * @param {number} step - Current step number
46
+ * @param {number} total - Total number of steps
47
+ * @param {string} description - Step description
48
+ * @returns {string} Formatted title
48
49
  */
49
- async function checkExistingConfig(installDir) {
50
- const envPath = join(installDir, '.env');
51
-
52
- if (!existsSync(envPath)) {
53
- return true; // No existing config, safe to continue
54
- }
55
-
56
- console.log(chalk.yellow('\n⚠ Existing configuration detected!'));
57
- console.log(chalk.white(` Found: ${envPath}`));
58
- console.log(chalk.gray(' This file contains API URLs, credentials, and other settings.\n'));
59
-
60
- const { action } = await inquirer.prompt([
61
- {
62
- type: 'list',
63
- name: 'action',
64
- message: 'How would you like to proceed?',
65
- choices: [
66
- { name: 'Backup old config and create new (Recommended)', value: 'backup' },
67
- { name: 'Overwrite with new configuration', value: 'overwrite' },
68
- { name: 'Cancel installation', value: 'cancel' }
69
- ],
70
- default: 'backup'
71
- }
72
- ]);
73
-
74
- if (action === 'cancel') {
75
- console.log(chalk.gray('\nInstallation cancelled.\n'));
76
- return false;
77
- }
78
-
79
- if (action === 'backup') {
80
- const backupPath = backupEnvFile(envPath);
81
- if (backupPath) {
82
- console.log(chalk.green(` ✓ Configuration backed up to: ${backupPath}\n`));
83
- }
84
- } else {
85
- console.log(chalk.yellow(' ⚠ Existing configuration will be overwritten\n'));
86
- }
87
-
88
- return true;
50
+ function formatStepTitle(step, total, description) {
51
+ return `[${step}/${total}] ${description}`;
89
52
  }
90
53
 
91
54
  /**
@@ -165,34 +128,43 @@ function showSuccess(installDir, credentials, config) {
165
128
  }
166
129
 
167
130
  /**
168
- * Install ANTE ERP
131
+ * Install ANTE ERP (Non-Interactive Mode)
169
132
  */
170
133
  export async function install(options) {
171
134
  try {
172
135
  showWelcome();
173
-
136
+
137
+ console.log(chalk.bold('\n🚀 ANTE ERP Installation\n'));
138
+
174
139
  // Check if already installed
175
140
  const existing = detectInstallation();
176
141
  if (existing && !options.force) {
177
- console.log(chalk.yellow('\n ANTE is already installed at:'), chalk.white(existing));
178
- const { continueAnyway } = await inquirer.prompt([
179
- {
180
- type: 'confirm',
181
- name: 'continueAnyway',
182
- message: 'Install anyway?',
183
- default: false
184
- }
185
- ]);
186
-
187
- if (!continueAnyway) {
188
- console.log(chalk.gray('\nInstallation cancelled.'));
189
- return;
190
- }
142
+ console.log(chalk.yellow('⚠ ANTE is already installed at:'), chalk.white(existing));
143
+ console.log(chalk.gray('Proceeding with re-installation...\n'));
191
144
  }
192
-
193
- // System checks
145
+
146
+ // Calculate total steps
147
+ let totalSteps = 10; // Base steps
148
+
149
+ // Pre-calculate step numbers
150
+ let currentStep = 0;
151
+ const stepSystemCheck = !options.skipChecks ? ++currentStep : null;
152
+ const stepNetworkDetect = ++currentStep;
153
+ const stepGenerateCredentials = ++currentStep;
154
+ const stepCreateDir = ++currentStep;
155
+ const stepGenerateConfig = ++currentStep;
156
+ const stepPullImages = ++currentStep;
157
+ const stepStartServices = ++currentStep;
158
+ const stepWaitServices = ++currentStep;
159
+ const stepInitDatabase = ++currentStep;
160
+ const stepRunMigrations = ++currentStep;
161
+
162
+ // Start installation
163
+ let stepNum = 0;
164
+
165
+ // Step: System checks
194
166
  if (!options.skipChecks) {
195
- console.log(chalk.bold('\n🔍 Checking system requirements...\n'));
167
+ console.log(chalk.cyan(formatStepTitle(stepSystemCheck, totalSteps, 'Checking system requirements')));
196
168
 
197
169
  // Check Docker first and offer installation if needed
198
170
  const dockerResult = await checkAndInstallDocker(!options.skipDockerInstall);
@@ -203,394 +175,92 @@ export async function install(options) {
203
175
  // Update checks with Docker installation result
204
176
  checks.docker = dockerResult;
205
177
 
206
- // Display check results
207
- console.log(chalk.bold('System Requirements:'));
208
- console.log(`${checks.docker.ok ? chalk.green('✓') : chalk.red('✗')} Docker: ${checks.docker.message}`);
178
+ // Display check results (compact)
179
+ console.log(` ${checks.docker.ok ? chalk.green('✓') : chalk.red('✗')} Docker: ${checks.docker.message}`);
209
180
  if (checks.docker.installed) {
210
- console.log(chalk.green(' ↳ Docker was installed during this setup'));
181
+ console.log(chalk.green(' ↳ Docker was installed during this setup'));
211
182
  }
212
- console.log(`${checks.dockerCompose.ok ? chalk.green('✓') : chalk.red('✗')} Docker Compose: ${checks.dockerCompose.message}`);
213
- console.log(`${checks.node.ok ? chalk.green('✓') : chalk.red('✗')} Node.js: ${checks.node.message}`);
214
-
215
- console.log(chalk.bold('\nResources:'));
216
- console.log(`${checks.diskSpace.ok ? chalk.green('✓') : chalk.red('✗')} Disk Space: ${checks.diskSpace.message}`);
217
- console.log(`${checks.memory.ok ? chalk.green('✓') : chalk.red('✗')} Memory: ${checks.memory.message}`);
218
- console.log(`${checks.cpu.ok ? chalk.green('✓') : chalk.yellow('⚠')} CPU: ${checks.cpu.message}`);
183
+ console.log(` ${checks.dockerCompose.ok ? chalk.green('✓') : chalk.red('✗')} Docker Compose: ${checks.dockerCompose.message}`);
184
+ console.log(` ${checks.node.ok ? chalk.green('✓') : chalk.red('✗')} Node.js: ${checks.node.message}`);
185
+ console.log(` ${checks.diskSpace.ok ? chalk.green('✓') : chalk.red('✗')} Disk Space: ${checks.diskSpace.message}`);
219
186
 
220
187
  if (!ok || !checks.docker.ok) {
221
188
  console.log(chalk.red('\n✗ System requirements not met. Please resolve issues above.\n'));
222
189
  process.exit(1);
223
190
  }
224
191
 
225
- console.log(chalk.green('\n✓ All requirements met!\n'));
226
- } else {
227
- console.log(chalk.yellow('\n⚠ Skipping system requirements check (--skip-checks)\n'));
192
+ console.log(chalk.green(`✓ ${formatStepTitle(stepSystemCheck, totalSteps, 'System requirements met')}\n`));
228
193
  }
229
194
 
230
- // Detect public IP for server installations
231
- console.log(chalk.bold('\n🌐 Network Configuration\n'));
232
- const spinner = ora();
195
+ // Step: Network detection
196
+ console.log(chalk.cyan(formatStepTitle(stepNetworkDetect, totalSteps, 'Detecting network configuration')));
197
+ const spinner = ora({ text: 'Detecting public IP...', prefixText: ' ' }).start();
233
198
  const detectedIP = await detectPublicIPWithFeedback(spinner);
234
199
 
235
- // Interactive prompts or use options
236
- let config;
237
- if (options.interactive) {
238
- // Interactive mode with IP/domain configuration
239
- const baseConfig = await inquirer.prompt([
240
- {
241
- type: 'input',
242
- name: 'installDir',
243
- message: 'Installation directory:',
244
- default: options.dir || './ante-erp'
245
- },
246
- {
247
- type: 'list',
248
- name: 'preset',
249
- message: 'Choose installation type:',
250
- choices: [
251
- { name: 'Minimal (Evaluation)', value: 'minimal' },
252
- { name: 'Standard (Recommended)', value: 'standard' },
253
- { name: 'Enterprise (Full Features)', value: 'enterprise' }
254
- ],
255
- default: 'standard'
256
- }
257
- ]);
258
-
259
- // Frontend selection prompts
260
- const frontendConfig = await inquirer.prompt([
261
- {
262
- type: 'checkbox',
263
- name: 'frontends',
264
- message: 'Which frontends do you want to install?',
265
- choices: [
266
- { name: 'Main Frontend (Required)', value: 'main', checked: true, disabled: true },
267
- { name: 'Gate App (School/Gate attendance)', value: 'gate', checked: false },
268
- { name: 'Guardian App (Parent portal)', value: 'guardian', checked: false },
269
- { name: 'Facial Web (Employee face recognition)', value: 'facial', checked: false },
270
- { name: 'POS App (Point of Sale)', value: 'pos', checked: false }
271
- ],
272
- validate: (answer) => {
273
- if (answer.length < 1) {
274
- return 'You must select at least one frontend (Main Frontend is required).';
275
- }
276
- return true;
277
- }
278
- },
279
- {
280
- type: 'number',
281
- name: 'companyId',
282
- message: 'Company ID (for multi-tenant apps):',
283
- default: 1,
284
- when: (answers) => answers.frontends.includes('gate') || answers.frontends.includes('guardian') || answers.frontends.includes('facial') || answers.frontends.includes('pos'),
285
- validate: (input) => {
286
- if (input < 1) return 'Company ID must be at least 1';
287
- return true;
288
- }
289
- }
290
- ]);
291
-
292
- // Network configuration prompts
293
- const networkConfig = await inquirer.prompt([
294
- {
295
- type: 'list',
296
- name: 'networkType',
297
- message: 'How will users access this installation?',
298
- choices: [
299
- { name: 'Local development (localhost)', value: 'localhost' },
300
- { name: 'Public IP address', value: 'ip' },
301
- { name: 'Domain name', value: 'domain' }
302
- ],
303
- default: detectedIP ? 'ip' : 'localhost'
304
- },
305
- {
306
- type: 'input',
307
- name: 'publicIP',
308
- message: 'Enter the public IP address:',
309
- default: detectedIP || '',
310
- when: (answers) => answers.networkType === 'ip',
311
- validate: (input) => {
312
- if (!input) return 'IP address is required';
313
- if (!isValidIPv4(input)) return 'Please enter a valid IPv4 address';
314
- return true;
315
- }
316
- },
317
- {
318
- type: 'input',
319
- name: 'domainName',
320
- message: 'Enter your domain name (e.g., erp.example.com):',
321
- when: (answers) => answers.networkType === 'domain',
322
- validate: (input) => {
323
- if (!input) return 'Domain name is required';
324
- if (!isValidDomain(input)) return 'Please enter a valid domain name';
325
- return true;
326
- }
327
- },
328
- {
329
- type: 'number',
330
- name: 'frontendPort',
331
- message: 'Frontend port:',
332
- default: 8080,
333
- when: (answers) => answers.networkType !== 'domain',
334
- validate: (input) => {
335
- if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
336
- return true;
337
- }
338
- },
339
- {
340
- type: 'number',
341
- name: 'apiPort',
342
- message: 'API port:',
343
- default: 3001,
344
- when: (answers) => answers.networkType !== 'domain',
345
- validate: (input) => {
346
- if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
347
- return true;
348
- }
349
- },
350
- {
351
- type: 'number',
352
- name: 'gateAppPort',
353
- message: 'Gate App port:',
354
- default: 8081,
355
- when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('gate'),
356
- validate: (input) => {
357
- if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
358
- return true;
359
- }
360
- },
361
- {
362
- type: 'number',
363
- name: 'guardianAppPort',
364
- message: 'Guardian App port:',
365
- default: 8082,
366
- when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('guardian'),
367
- validate: (input) => {
368
- if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
369
- return true;
370
- }
371
- },
372
- {
373
- type: 'number',
374
- name: 'facialWebPort',
375
- message: 'Facial Web port:',
376
- default: 8083,
377
- when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('facial'),
378
- validate: (input) => {
379
- if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
380
- return true;
381
- }
382
- },
383
- {
384
- type: 'number',
385
- name: 'posAppPort',
386
- message: 'POS App port:',
387
- default: 8084,
388
- when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('pos'),
389
- validate: (input) => {
390
- if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
391
- return true;
392
- }
393
- }
394
- ]);
395
-
396
- // Build URLs based on configuration
397
- let frontendUrl, apiUrl, gateAppUrl, guardianAppUrl, facialWebUrl, posAppUrl;
398
- const frontendPort = networkConfig.frontendPort || 8080;
399
- const apiPort = networkConfig.apiPort || 3001;
400
- const gateAppPort = networkConfig.gateAppPort || 8081;
401
- const guardianAppPort = networkConfig.guardianAppPort || 8082;
402
- const facialWebPort = networkConfig.facialWebPort || 8083;
403
- const posAppPort = networkConfig.posAppPort || 8084;
404
-
405
- if (networkConfig.networkType === 'localhost') {
406
- frontendUrl = buildURL('localhost', frontendPort);
407
- apiUrl = buildURL('localhost', apiPort);
408
- gateAppUrl = buildURL('localhost', gateAppPort);
409
- guardianAppUrl = buildURL('localhost', guardianAppPort);
410
- facialWebUrl = buildURL('localhost', facialWebPort);
411
- posAppUrl = buildURL('localhost', posAppPort);
412
- } else if (networkConfig.networkType === 'ip') {
413
- frontendUrl = buildURL(networkConfig.publicIP, frontendPort);
414
- apiUrl = buildURL(networkConfig.publicIP, apiPort);
415
- gateAppUrl = buildURL(networkConfig.publicIP, gateAppPort);
416
- guardianAppUrl = buildURL(networkConfig.publicIP, guardianAppPort);
417
- facialWebUrl = buildURL(networkConfig.publicIP, facialWebPort);
418
- posAppUrl = buildURL(networkConfig.publicIP, posAppPort);
419
- } else {
420
- // Domain - use standard HTTP/HTTPS ports (NGINX will handle reverse proxy)
421
- const isHttps = await inquirer.prompt([{
422
- type: 'confirm',
423
- name: 'useHttps',
424
- message: 'Is SSL/TLS configured for this domain (e.g., via Cloudflare)?',
425
- default: true
426
- }]);
427
-
428
- // Ask for API subdomain
429
- const apiSubdomain = await inquirer.prompt([{
430
- type: 'input',
431
- name: 'subdomain',
432
- message: 'API subdomain (e.g., "api" for api.example.com):',
433
- default: 'api',
434
- validate: (input) => {
435
- if (!input) return 'Subdomain is required';
436
- if (!/^[a-z0-9-]+$/.test(input)) return 'Invalid subdomain format';
437
- return true;
438
- }
439
- }]);
440
-
441
- const protocol = isHttps.useHttps ? 'https' : 'http';
442
- frontendUrl = `${protocol}://${networkConfig.domainName}`;
443
- apiUrl = `${protocol}://${apiSubdomain.subdomain}.${networkConfig.domainName}`;
444
- gateAppUrl = `${protocol}://gate.${networkConfig.domainName}`;
445
- guardianAppUrl = `${protocol}://guardian.${networkConfig.domainName}`;
446
- facialWebUrl = `${protocol}://facial.${networkConfig.domainName}`;
447
- posAppUrl = `${protocol}://pos.${networkConfig.domainName}`;
448
- }
449
-
450
- config = {
451
- ...baseConfig,
452
- ...frontendConfig,
453
- frontendDomain: frontendUrl,
454
- apiDomain: apiUrl,
455
- gateAppDomain: gateAppUrl,
456
- guardianAppDomain: guardianAppUrl,
457
- facialAppDomain: facialWebUrl,
458
- posAppDomain: posAppUrl,
459
- frontendPort,
460
- apiPort,
461
- gateAppPort,
462
- guardianAppPort,
463
- facialWebPort,
464
- posAppPort,
465
- installGate: frontendConfig.frontends.includes('gate'),
466
- installGuardian: frontendConfig.frontends.includes('guardian'),
467
- installFacial: frontendConfig.frontends.includes('facial'),
468
- installPos: frontendConfig.frontends.includes('pos')
469
- };
470
- } else {
471
- // Non-interactive mode - use options or defaults with IP detection
472
- let frontendDomain = options.frontendDomain;
473
- let apiDomain = options.apiDomain;
474
-
475
- // If no domains provided and IP detected, use detected IP
476
- if (!frontendDomain && detectedIP) {
477
- frontendDomain = buildURL(detectedIP, parseInt(options.port) || 8080);
478
- console.log(chalk.cyan(' → Using detected IP for frontend:'), chalk.white(frontendDomain));
479
- }
480
-
481
- if (!apiDomain && detectedIP) {
482
- apiDomain = buildURL(detectedIP, 3001);
483
- console.log(chalk.cyan(' → Using detected IP for API:'), chalk.white(apiDomain));
484
- }
485
-
486
- // If still no domain and no IP detected, prompt user
487
- if (!frontendDomain || !apiDomain) {
488
- console.log(chalk.yellow('\n⚠ Could not auto-detect public IP address.'));
489
- console.log(chalk.white('Please provide domain/IP configuration manually:\n'));
490
-
491
- const manualConfig = await inquirer.prompt([
492
- {
493
- type: 'input',
494
- name: 'hostIp',
495
- message: 'Enter public IP address or domain:',
496
- validate: (input) => {
497
- if (!input) return 'IP address or domain is required';
498
- if (!isValidIPv4(input) && !isValidDomain(input)) {
499
- return 'Please enter a valid IP address or domain name';
500
- }
501
- return true;
502
- }
503
- }
504
- ]);
505
-
506
- frontendDomain = buildURL(manualConfig.hostIp, parseInt(options.port) || 8080);
507
- apiDomain = buildURL(manualConfig.hostIp, 3001);
508
- }
200
+ // Build configuration (non-interactive, enterprise preset with all frontends)
201
+ const host = detectedIP || 'localhost';
202
+ const frontendPort = parseInt(options.port) || 8080;
203
+ const apiPort = 3001;
204
+ const gateAppPort = 8081;
205
+ const guardianAppPort = 8082;
206
+ const facialWebPort = 8083;
207
+ const posAppPort = 8084;
208
+
209
+ const config = {
210
+ installDir: options.dir || './ante-erp',
211
+ preset: 'enterprise', // Always install all features
212
+ frontendDomain: buildURL(host, frontendPort),
213
+ apiDomain: buildURL(host, apiPort),
214
+ gateAppDomain: buildURL(host, gateAppPort),
215
+ guardianAppDomain: buildURL(host, guardianAppPort),
216
+ facialAppDomain: buildURL(host, facialWebPort),
217
+ posAppDomain: buildURL(host, posAppPort),
218
+ frontendPort,
219
+ apiPort,
220
+ gateAppPort,
221
+ guardianAppPort,
222
+ facialWebPort,
223
+ posAppPort,
224
+ companyId: 1,
225
+ installGate: true, // Always install all frontends
226
+ installGuardian: true,
227
+ installFacial: true,
228
+ installPos: true,
229
+ frontends: ['main', 'gate', 'guardian', 'facial', 'pos']
230
+ };
509
231
 
510
- // Determine which frontends to install based on flags
511
- const installAllFrontends = options.withAllFrontends;
512
- const installGate = installAllFrontends || options.withGate;
513
- const installGuardian = installAllFrontends || options.withGuardian;
514
- const installFacial = installAllFrontends || options.withFacial;
515
- const installPos = installAllFrontends || options.withPos;
516
-
517
- // Build frontends array
518
- const frontends = ['main'];
519
- if (installGate) frontends.push('gate');
520
- if (installGuardian) frontends.push('guardian');
521
- if (installFacial) frontends.push('facial');
522
- if (installPos) frontends.push('pos');
523
-
524
- config = {
525
- installDir: options.dir,
526
- preset: options.preset || 'standard',
527
- frontendDomain,
528
- apiDomain,
529
- frontendPort: parseInt(options.port) || 8080,
530
- apiPort: 3001,
531
- gateAppPort: 8081,
532
- guardianAppPort: 8082,
533
- facialWebPort: 8083,
534
- posAppPort: 8084,
535
- gateAppDomain: buildURL(detectedIP || 'localhost', 8081),
536
- guardianAppDomain: buildURL(detectedIP || 'localhost', 8082),
537
- facialAppDomain: buildURL(detectedIP || 'localhost', 8083),
538
- posAppDomain: buildURL(detectedIP || 'localhost', 8084),
539
- companyId: 1,
540
- installGate,
541
- installGuardian,
542
- installFacial,
543
- installPos,
544
- frontends
545
- };
546
- }
232
+ console.log(chalk.green(`✓ ${formatStepTitle(stepNetworkDetect, totalSteps, `Network detected: ${host}`)}\n`));
547
233
 
548
- // Display configuration summary
549
- console.log(chalk.bold('\n📋 Installation Configuration:\n'));
234
+ // Display configuration summary (compact)
235
+ console.log(chalk.bold('📋 Installation Configuration:\n'));
550
236
  console.log(chalk.cyan(' Directory:'), chalk.white(config.installDir));
551
- console.log(chalk.cyan(' Preset:'), chalk.white(config.preset));
237
+ console.log(chalk.cyan(' Preset:'), chalk.white('Enterprise (All Features)'));
552
238
  console.log(chalk.cyan(' Frontend Main:'), chalk.white(config.frontendDomain));
553
- if (config.installGate) {
554
- console.log(chalk.cyan(' Gate App:'), chalk.white(config.gateAppDomain));
555
- }
556
- if (config.installGuardian) {
557
- console.log(chalk.cyan(' Guardian App:'), chalk.white(config.guardianAppDomain));
558
- }
559
- if (config.installFacial) {
560
- console.log(chalk.cyan(' Facial Web:'), chalk.white(config.facialAppDomain));
561
- }
562
- if (config.installPos) {
563
- console.log(chalk.cyan(' POS App:'), chalk.white(config.posAppDomain));
564
- }
239
+ console.log(chalk.cyan(' Gate App:'), chalk.white(config.gateAppDomain));
240
+ console.log(chalk.cyan(' Guardian App:'), chalk.white(config.guardianAppDomain));
241
+ console.log(chalk.cyan(' Facial Web:'), chalk.white(config.facialAppDomain));
242
+ console.log(chalk.cyan(' POS App:'), chalk.white(config.posAppDomain));
565
243
  console.log(chalk.cyan(' API:'), chalk.white(config.apiDomain));
566
- if (config.companyId) {
567
- console.log(chalk.cyan(' Company ID:'), chalk.white(config.companyId));
568
- }
569
244
  console.log();
570
245
 
571
- // Check for existing configuration files
572
- if (!options.force) {
573
- const canContinue = await checkExistingConfig(config.installDir);
574
- if (!canContinue) {
575
- return;
576
- }
577
- } else {
578
- // Force mode: backup existing .env without prompting
579
- const envPath = join(config.installDir, '.env');
246
+ // Auto-backup existing config if exists (no prompt)
247
+ const envPath = join(config.installDir, '.env');
248
+ if (existsSync(envPath)) {
580
249
  const backupPath = backupEnvFile(envPath);
581
250
  if (backupPath) {
582
- console.log(chalk.yellow(`⚠ Force mode: Existing config backed up to ${backupPath}\n`));
251
+ console.log(chalk.yellow(`⚠ Existing config backed up: ${backupPath}\n`));
583
252
  }
584
253
  }
585
254
 
586
- // Generate credentials
587
- console.log(chalk.bold('\n🔐 Generating secure credentials...\n'));
255
+ // Step: Generate credentials
256
+ console.log(chalk.cyan(formatStepTitle(stepGenerateCredentials, totalSteps, 'Generating secure credentials')));
588
257
  const credentials = generateCredentials();
589
-
590
- // Installation tasks
258
+ console.log(chalk.green(`✓ ${formatStepTitle(stepGenerateCredentials, totalSteps, 'Secure credentials generated')}\n`));
259
+
260
+ // Installation tasks with Listr2
591
261
  const tasks = new Listr([
592
262
  {
593
- title: 'Creating installation directory',
263
+ title: formatStepTitle(stepCreateDir, totalSteps, 'Creating installation directory'),
594
264
  task: () => {
595
265
  mkdirSync(config.installDir, { recursive: true });
596
266
  mkdirSync(join(config.installDir, 'backups'), { recursive: true });
@@ -598,59 +268,47 @@ export async function install(options) {
598
268
  }
599
269
  },
600
270
  {
601
- title: 'Configuring server timezone',
602
- task: async () => {
603
- try {
604
- // Set system timezone to Asia/Manila
605
- await execa('timedatectl', ['set-timezone', 'Asia/Manila'], { stdio: 'pipe' });
606
- } catch (error) {
607
- // Non-critical: log warning but continue installation
608
- console.warn(' ⚠ Could not set system timezone (may require sudo)');
609
- }
610
- }
611
- },
612
- {
613
- title: 'Generating configuration files',
271
+ title: formatStepTitle(stepGenerateConfig, totalSteps, 'Generating configuration files'),
614
272
  task: () => {
615
273
  const dockerCompose = generateDockerCompose({
616
- frontendPort: config.frontendPort || 8080,
617
- backendPort: config.apiPort || 3001,
618
- gateAppPort: config.gateAppPort || 8081,
619
- guardianAppPort: config.guardianAppPort || 8082,
620
- facialWebPort: config.facialWebPort || 8083,
621
- posAppPort: config.posAppPort || 8084,
274
+ frontendPort: config.frontendPort,
275
+ backendPort: config.apiPort,
276
+ gateAppPort: config.gateAppPort,
277
+ guardianAppPort: config.guardianAppPort,
278
+ facialWebPort: config.facialWebPort,
279
+ posAppPort: config.posAppPort,
622
280
  installMain: true,
623
- installGate: config.installGate || false,
624
- installGuardian: config.installGuardian || false,
625
- installFacial: config.installFacial || false,
626
- installPos: config.installPos || false,
627
- companyId: config.companyId || 1
281
+ installGate: config.installGate,
282
+ installGuardian: config.installGuardian,
283
+ installFacial: config.installFacial,
284
+ installPos: config.installPos,
285
+ companyId: config.companyId
628
286
  });
629
287
 
630
288
  const envContent = generateEnv(credentials, {
631
- frontendUrl: config.frontendDomain || 'http://localhost:8080',
632
- apiUrl: config.apiDomain || 'http://localhost:3001',
633
- socketUrl: config.apiDomain || 'http://localhost:3001',
634
- frontendPort: config.frontendPort || 8080,
635
- backendPort: config.apiPort || 3001,
636
- gateAppPort: config.gateAppPort || 8081,
637
- guardianAppPort: config.guardianAppPort || 8082,
638
- facialWebPort: config.facialWebPort || 8083,
639
- posAppPort: config.posAppPort || 8084,
640
- gateAppUrl: config.gateAppDomain || 'http://localhost:8081',
641
- guardianAppUrl: config.guardianAppDomain || 'http://localhost:8082',
642
- facialWebUrl: config.facialAppDomain || 'http://localhost:8083',
643
- posAppUrl: config.posAppDomain || 'http://localhost:8084',
644
- companyId: config.companyId || 1,
645
- installGate: config.installGate || false,
646
- installGuardian: config.installGuardian || false,
647
- installFacial: config.installFacial || false,
648
- installPos: config.installPos || false
289
+ frontendUrl: config.frontendDomain,
290
+ apiUrl: config.apiDomain,
291
+ socketUrl: config.apiDomain,
292
+ frontendPort: config.frontendPort,
293
+ backendPort: config.apiPort,
294
+ gateAppPort: config.gateAppPort,
295
+ guardianAppPort: config.guardianAppPort,
296
+ facialWebPort: config.facialWebPort,
297
+ posAppPort: config.posAppPort,
298
+ gateAppUrl: config.gateAppDomain,
299
+ guardianAppUrl: config.guardianAppDomain,
300
+ facialWebUrl: config.facialAppDomain,
301
+ posAppUrl: config.posAppDomain,
302
+ companyId: config.companyId,
303
+ installGate: config.installGate,
304
+ installGuardian: config.installGuardian,
305
+ installFacial: config.installFacial,
306
+ installPos: config.installPos
649
307
  });
650
-
308
+
651
309
  writeFileSync(join(config.installDir, 'docker-compose.yml'), dockerCompose);
652
310
  writeFileSync(join(config.installDir, '.env'), envContent);
653
-
311
+
654
312
  // Save credentials
655
313
  const credentialsText = `ANTE ERP Installation Credentials
656
314
  Generated: ${new Date().toISOString()}
@@ -671,9 +329,9 @@ Encryption Key: ${credentials.encryptionKey}
671
329
 
672
330
  Access Information:
673
331
  ━━━━━━━━━━━━━━━━━━━
674
- Frontend: ${config.frontendDomain || 'http://localhost:8080'}
675
- Backend: ${config.apiDomain || 'http://localhost:3001'}
676
- WebSocket: ${config.apiDomain || 'http://localhost:3001'}
332
+ Frontend: ${config.frontendDomain}
333
+ Backend: ${config.apiDomain}
334
+ WebSocket: ${config.apiDomain}
677
335
 
678
336
  Next Steps:
679
337
  ━━━━━━━━━━
@@ -686,50 +344,49 @@ Next Steps:
686
344
  Documentation: https://docs.ante.ph
687
345
  Support: support@ante.ph
688
346
  `;
689
-
347
+
690
348
  writeFileSync(join(config.installDir, 'installation-credentials.txt'), credentialsText);
691
349
  }
692
350
  },
693
351
  {
694
- title: 'Pulling Docker images',
352
+ title: formatStepTitle(stepPullImages, totalSteps, 'Pulling Docker images'),
695
353
  task: async () => {
696
354
  const composeFile = join(config.installDir, 'docker-compose.yml');
697
- await pullImages(composeFile);
355
+ await pullImagesSilent(composeFile);
698
356
  }
699
357
  },
700
358
  {
701
- title: 'Starting services',
359
+ title: formatStepTitle(stepStartServices, totalSteps, 'Starting services'),
702
360
  task: async () => {
703
361
  const composeFile = join(config.installDir, 'docker-compose.yml');
704
- await startServices(composeFile);
362
+ await startServicesSilent(composeFile);
705
363
  }
706
364
  },
707
365
  {
708
- title: 'Waiting for services to be ready',
366
+ title: formatStepTitle(stepWaitServices, totalSteps, 'Waiting for services to be ready'),
709
367
  task: async () => {
710
368
  const composeFile = join(config.installDir, 'docker-compose.yml');
711
-
369
+
712
370
  // Wait for backend to be healthy
713
371
  const backendHealthy = await waitForServiceHealthy(composeFile, 'backend', 120);
714
372
  if (!backendHealthy) {
715
373
  throw new Error('Backend service failed to start');
716
374
  }
717
-
718
- // Give it a few more seconds
375
+
376
+ // Give it a few more seconds for full initialization
719
377
  await new Promise(resolve => setTimeout(resolve, 5000));
720
378
  }
721
379
  },
722
380
  {
723
- title: 'Initializing database schema',
381
+ title: formatStepTitle(stepInitDatabase, totalSteps, 'Initializing database'),
724
382
  task: async () => {
725
383
  const composeFile = join(config.installDir, 'docker-compose.yml');
726
384
  const schemaFile = join(__dirname, '../templates/init-schema.sql');
727
385
 
728
386
  try {
729
- const { readFileSync } = await import('fs');
730
387
  const schemaSQL = readFileSync(schemaFile, 'utf8');
731
388
 
732
- // Execute schema SQL in postgres container using stdin
389
+ // Execute schema SQL in postgres container
733
390
  await execa('docker', [
734
391
  'compose',
735
392
  '-f', composeFile,
@@ -740,21 +397,12 @@ Support: support@ante.ph
740
397
  '-U', 'ante',
741
398
  '-d', 'ante_db'
742
399
  ], {
743
- input: schemaSQL
400
+ input: schemaSQL,
401
+ stdout: 'pipe',
402
+ stderr: 'pipe'
744
403
  });
745
- } catch (error) {
746
- throw new Error(`Schema initialization failed: ${error.message}`);
747
- }
748
- }
749
- },
750
- {
751
- title: 'Seeding initial data',
752
- task: async () => {
753
- const composeFile = join(config.installDir, 'docker-compose.yml');
754
404
 
755
- try {
756
- // Run production seed command in the backend container
757
- // Uses seed:prod which compiles TypeScript and runs with node (production-safe)
405
+ // Run production seed command
758
406
  await execa('docker', [
759
407
  'compose',
760
408
  '-f', composeFile,
@@ -764,51 +412,52 @@ Support: support@ante.ph
764
412
  'npm',
765
413
  'run',
766
414
  'seed:prod'
767
- ]);
415
+ ], {
416
+ stdout: 'pipe',
417
+ stderr: 'pipe'
418
+ });
768
419
  } catch (error) {
769
- // Seeding is optional, just log warning
770
- console.log(chalk.yellow(' ⚠ Seeding skipped (optional)'));
420
+ throw new Error(`Database initialization failed: ${error.message}`);
771
421
  }
772
422
  }
773
423
  },
774
424
  {
775
- title: 'Running database migrations',
776
- task: async (ctx, task) => {
425
+ title: formatStepTitle(stepRunMigrations, totalSteps, 'Running database migrations'),
426
+ task: async () => {
777
427
  const composeFile = join(config.installDir, 'docker-compose.yml');
778
428
  const result = await runMigrations(composeFile);
779
429
 
780
430
  if (!result.success) {
781
431
  throw new Error(`Migration failed:\n${result.output}`);
782
432
  }
783
-
784
- // Show migration output if there were migrations run
785
- if (result.output && result.output.trim()) {
786
- task.output = result.output;
787
- }
788
433
  }
789
434
  }
790
435
  ], {
436
+ renderer: 'default',
791
437
  rendererOptions: {
792
- collapseSubtasks: false
793
- }
438
+ collapse: false,
439
+ showSubtasks: false,
440
+ clearOutput: false,
441
+ showTimer: false,
442
+ removeEmptyLines: true
443
+ },
444
+ concurrent: false,
445
+ exitOnError: true
794
446
  });
795
-
796
- await tasks.run();
797
447
 
798
- // Configure NGINX if needed (for domains or HTTPS)
799
- if (requiresNginx(config.frontendDomain || 'http://localhost:8080', config.apiDomain || 'http://localhost:3001')) {
800
- console.log(chalk.gray('\n🔧 Setting up reverse proxy...\n'));
448
+ await tasks.run();
801
449
 
450
+ // Configure NGINX if needed (silent)
451
+ if (requiresNginx(config.frontendDomain, config.apiDomain)) {
802
452
  try {
803
453
  await configureNginx({
804
- frontendDomain: config.frontendDomain || 'http://localhost:8080',
805
- apiDomain: config.apiDomain || 'http://localhost:3001',
806
- frontendPort: config.frontendPort || 8080,
807
- apiPort: config.apiPort || 3001
454
+ frontendDomain: config.frontendDomain,
455
+ apiDomain: config.apiDomain,
456
+ frontendPort: config.frontendPort,
457
+ apiPort: config.apiPort
808
458
  });
809
459
  } catch (error) {
810
- console.log(chalk.yellow('\n NGINX configuration failed:', error.message));
811
- console.log(chalk.gray('You may need to configure reverse proxy manually\n'));
460
+ console.log(chalk.yellow('⚠ NGINX configuration skipped (manual setup may be required)'));
812
461
  }
813
462
  }
814
463
 
@@ -818,20 +467,14 @@ Support: support@ante.ph
818
467
  version: '1.0.0'
819
468
  };
820
469
 
821
- // Only include domain if it exists
822
- if (config.domain) {
823
- installConfig.domain = config.domain;
824
- }
825
-
826
470
  saveInstallConfig(installConfig);
827
471
 
828
472
  // Show success message
829
473
  showSuccess(config.installDir, credentials, config);
830
-
474
+
831
475
  } catch (error) {
832
476
  console.error(chalk.red('\n✗ Installation failed:'), error.message);
833
477
  console.error(chalk.gray('\nFor help, visit: https://docs.ante.ph/self-hosting/troubleshooting\n'));
834
478
  process.exit(1);
835
479
  }
836
480
  }
837
-
@@ -300,13 +300,11 @@ export async function runSystemChecks() {
300
300
  dockerCompose: await checkDockerCompose(),
301
301
  node: checkNode(),
302
302
  diskSpace: checkDiskSpace(),
303
- memory: checkMemory(),
304
- cpu: checkCPU(),
305
303
  ports: await checkPorts()
306
304
  };
307
-
305
+
308
306
  const ok = Object.values(checks).every(check => check.ok);
309
-
307
+
310
308
  return { ok, checks };
311
309
  }
312
310