ante-erp-cli 1.11.16 → 1.11.17

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.17",
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,93 @@ 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}`);
186
+ console.log(` ${checks.memory.ok ? chalk.green('✓') : chalk.red('✗')} Memory: ${checks.memory.message}`);
219
187
 
220
188
  if (!ok || !checks.docker.ok) {
221
189
  console.log(chalk.red('\n✗ System requirements not met. Please resolve issues above.\n'));
222
190
  process.exit(1);
223
191
  }
224
192
 
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'));
193
+ console.log(chalk.green(`✓ ${formatStepTitle(stepSystemCheck, totalSteps, 'System requirements met')}\n`));
228
194
  }
229
195
 
230
- // Detect public IP for server installations
231
- console.log(chalk.bold('\n🌐 Network Configuration\n'));
232
- const spinner = ora();
196
+ // Step: Network detection
197
+ console.log(chalk.cyan(formatStepTitle(stepNetworkDetect, totalSteps, 'Detecting network configuration')));
198
+ const spinner = ora({ text: 'Detecting public IP...', prefixText: ' ' }).start();
233
199
  const detectedIP = await detectPublicIPWithFeedback(spinner);
234
200
 
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
- }
201
+ // Build configuration (non-interactive, enterprise preset with all frontends)
202
+ const host = detectedIP || 'localhost';
203
+ const frontendPort = parseInt(options.port) || 8080;
204
+ const apiPort = 3001;
205
+ const gateAppPort = 8081;
206
+ const guardianAppPort = 8082;
207
+ const facialWebPort = 8083;
208
+ const posAppPort = 8084;
209
+
210
+ const config = {
211
+ installDir: options.dir || './ante-erp',
212
+ preset: 'enterprise', // Always install all features
213
+ frontendDomain: buildURL(host, frontendPort),
214
+ apiDomain: buildURL(host, apiPort),
215
+ gateAppDomain: buildURL(host, gateAppPort),
216
+ guardianAppDomain: buildURL(host, guardianAppPort),
217
+ facialAppDomain: buildURL(host, facialWebPort),
218
+ posAppDomain: buildURL(host, posAppPort),
219
+ frontendPort,
220
+ apiPort,
221
+ gateAppPort,
222
+ guardianAppPort,
223
+ facialWebPort,
224
+ posAppPort,
225
+ companyId: 1,
226
+ installGate: true, // Always install all frontends
227
+ installGuardian: true,
228
+ installFacial: true,
229
+ installPos: true,
230
+ frontends: ['main', 'gate', 'guardian', 'facial', 'pos']
231
+ };
509
232
 
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
- }
233
+ console.log(chalk.green(`✓ ${formatStepTitle(stepNetworkDetect, totalSteps, `Network detected: ${host}`)}\n`));
547
234
 
548
- // Display configuration summary
549
- console.log(chalk.bold('\n📋 Installation Configuration:\n'));
235
+ // Display configuration summary (compact)
236
+ console.log(chalk.bold('📋 Installation Configuration:\n'));
550
237
  console.log(chalk.cyan(' Directory:'), chalk.white(config.installDir));
551
- console.log(chalk.cyan(' Preset:'), chalk.white(config.preset));
238
+ console.log(chalk.cyan(' Preset:'), chalk.white('Enterprise (All Features)'));
552
239
  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
- }
240
+ console.log(chalk.cyan(' Gate App:'), chalk.white(config.gateAppDomain));
241
+ console.log(chalk.cyan(' Guardian App:'), chalk.white(config.guardianAppDomain));
242
+ console.log(chalk.cyan(' Facial Web:'), chalk.white(config.facialAppDomain));
243
+ console.log(chalk.cyan(' POS App:'), chalk.white(config.posAppDomain));
565
244
  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
245
  console.log();
570
246
 
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');
247
+ // Auto-backup existing config if exists (no prompt)
248
+ const envPath = join(config.installDir, '.env');
249
+ if (existsSync(envPath)) {
580
250
  const backupPath = backupEnvFile(envPath);
581
251
  if (backupPath) {
582
- console.log(chalk.yellow(`⚠ Force mode: Existing config backed up to ${backupPath}\n`));
252
+ console.log(chalk.yellow(`⚠ Existing config backed up: ${backupPath}\n`));
583
253
  }
584
254
  }
585
255
 
586
- // Generate credentials
587
- console.log(chalk.bold('\n🔐 Generating secure credentials...\n'));
256
+ // Step: Generate credentials
257
+ console.log(chalk.cyan(formatStepTitle(stepGenerateCredentials, totalSteps, 'Generating secure credentials')));
588
258
  const credentials = generateCredentials();
589
-
590
- // Installation tasks
259
+ console.log(chalk.green(`✓ ${formatStepTitle(stepGenerateCredentials, totalSteps, 'Secure credentials generated')}\n`));
260
+
261
+ // Installation tasks with Listr2
591
262
  const tasks = new Listr([
592
263
  {
593
- title: 'Creating installation directory',
264
+ title: formatStepTitle(stepCreateDir, totalSteps, 'Creating installation directory'),
594
265
  task: () => {
595
266
  mkdirSync(config.installDir, { recursive: true });
596
267
  mkdirSync(join(config.installDir, 'backups'), { recursive: true });
@@ -598,59 +269,47 @@ export async function install(options) {
598
269
  }
599
270
  },
600
271
  {
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',
272
+ title: formatStepTitle(stepGenerateConfig, totalSteps, 'Generating configuration files'),
614
273
  task: () => {
615
274
  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,
275
+ frontendPort: config.frontendPort,
276
+ backendPort: config.apiPort,
277
+ gateAppPort: config.gateAppPort,
278
+ guardianAppPort: config.guardianAppPort,
279
+ facialWebPort: config.facialWebPort,
280
+ posAppPort: config.posAppPort,
622
281
  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
282
+ installGate: config.installGate,
283
+ installGuardian: config.installGuardian,
284
+ installFacial: config.installFacial,
285
+ installPos: config.installPos,
286
+ companyId: config.companyId
628
287
  });
629
288
 
630
289
  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
290
+ frontendUrl: config.frontendDomain,
291
+ apiUrl: config.apiDomain,
292
+ socketUrl: config.apiDomain,
293
+ frontendPort: config.frontendPort,
294
+ backendPort: config.apiPort,
295
+ gateAppPort: config.gateAppPort,
296
+ guardianAppPort: config.guardianAppPort,
297
+ facialWebPort: config.facialWebPort,
298
+ posAppPort: config.posAppPort,
299
+ gateAppUrl: config.gateAppDomain,
300
+ guardianAppUrl: config.guardianAppDomain,
301
+ facialWebUrl: config.facialAppDomain,
302
+ posAppUrl: config.posAppDomain,
303
+ companyId: config.companyId,
304
+ installGate: config.installGate,
305
+ installGuardian: config.installGuardian,
306
+ installFacial: config.installFacial,
307
+ installPos: config.installPos
649
308
  });
650
-
309
+
651
310
  writeFileSync(join(config.installDir, 'docker-compose.yml'), dockerCompose);
652
311
  writeFileSync(join(config.installDir, '.env'), envContent);
653
-
312
+
654
313
  // Save credentials
655
314
  const credentialsText = `ANTE ERP Installation Credentials
656
315
  Generated: ${new Date().toISOString()}
@@ -671,9 +330,9 @@ Encryption Key: ${credentials.encryptionKey}
671
330
 
672
331
  Access Information:
673
332
  ━━━━━━━━━━━━━━━━━━━
674
- Frontend: ${config.frontendDomain || 'http://localhost:8080'}
675
- Backend: ${config.apiDomain || 'http://localhost:3001'}
676
- WebSocket: ${config.apiDomain || 'http://localhost:3001'}
333
+ Frontend: ${config.frontendDomain}
334
+ Backend: ${config.apiDomain}
335
+ WebSocket: ${config.apiDomain}
677
336
 
678
337
  Next Steps:
679
338
  ━━━━━━━━━━
@@ -686,50 +345,49 @@ Next Steps:
686
345
  Documentation: https://docs.ante.ph
687
346
  Support: support@ante.ph
688
347
  `;
689
-
348
+
690
349
  writeFileSync(join(config.installDir, 'installation-credentials.txt'), credentialsText);
691
350
  }
692
351
  },
693
352
  {
694
- title: 'Pulling Docker images',
353
+ title: formatStepTitle(stepPullImages, totalSteps, 'Pulling Docker images'),
695
354
  task: async () => {
696
355
  const composeFile = join(config.installDir, 'docker-compose.yml');
697
- await pullImages(composeFile);
356
+ await pullImagesSilent(composeFile);
698
357
  }
699
358
  },
700
359
  {
701
- title: 'Starting services',
360
+ title: formatStepTitle(stepStartServices, totalSteps, 'Starting services'),
702
361
  task: async () => {
703
362
  const composeFile = join(config.installDir, 'docker-compose.yml');
704
- await startServices(composeFile);
363
+ await startServicesSilent(composeFile);
705
364
  }
706
365
  },
707
366
  {
708
- title: 'Waiting for services to be ready',
367
+ title: formatStepTitle(stepWaitServices, totalSteps, 'Waiting for services to be ready'),
709
368
  task: async () => {
710
369
  const composeFile = join(config.installDir, 'docker-compose.yml');
711
-
370
+
712
371
  // Wait for backend to be healthy
713
372
  const backendHealthy = await waitForServiceHealthy(composeFile, 'backend', 120);
714
373
  if (!backendHealthy) {
715
374
  throw new Error('Backend service failed to start');
716
375
  }
717
-
718
- // Give it a few more seconds
376
+
377
+ // Give it a few more seconds for full initialization
719
378
  await new Promise(resolve => setTimeout(resolve, 5000));
720
379
  }
721
380
  },
722
381
  {
723
- title: 'Initializing database schema',
382
+ title: formatStepTitle(stepInitDatabase, totalSteps, 'Initializing database'),
724
383
  task: async () => {
725
384
  const composeFile = join(config.installDir, 'docker-compose.yml');
726
385
  const schemaFile = join(__dirname, '../templates/init-schema.sql');
727
386
 
728
387
  try {
729
- const { readFileSync } = await import('fs');
730
388
  const schemaSQL = readFileSync(schemaFile, 'utf8');
731
389
 
732
- // Execute schema SQL in postgres container using stdin
390
+ // Execute schema SQL in postgres container
733
391
  await execa('docker', [
734
392
  'compose',
735
393
  '-f', composeFile,
@@ -740,21 +398,12 @@ Support: support@ante.ph
740
398
  '-U', 'ante',
741
399
  '-d', 'ante_db'
742
400
  ], {
743
- input: schemaSQL
401
+ input: schemaSQL,
402
+ stdout: 'pipe',
403
+ stderr: 'pipe'
744
404
  });
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
405
 
755
- try {
756
- // Run production seed command in the backend container
757
- // Uses seed:prod which compiles TypeScript and runs with node (production-safe)
406
+ // Run production seed command
758
407
  await execa('docker', [
759
408
  'compose',
760
409
  '-f', composeFile,
@@ -764,51 +413,52 @@ Support: support@ante.ph
764
413
  'npm',
765
414
  'run',
766
415
  'seed:prod'
767
- ]);
416
+ ], {
417
+ stdout: 'pipe',
418
+ stderr: 'pipe'
419
+ });
768
420
  } catch (error) {
769
- // Seeding is optional, just log warning
770
- console.log(chalk.yellow(' ⚠ Seeding skipped (optional)'));
421
+ throw new Error(`Database initialization failed: ${error.message}`);
771
422
  }
772
423
  }
773
424
  },
774
425
  {
775
- title: 'Running database migrations',
776
- task: async (ctx, task) => {
426
+ title: formatStepTitle(stepRunMigrations, totalSteps, 'Running database migrations'),
427
+ task: async () => {
777
428
  const composeFile = join(config.installDir, 'docker-compose.yml');
778
429
  const result = await runMigrations(composeFile);
779
430
 
780
431
  if (!result.success) {
781
432
  throw new Error(`Migration failed:\n${result.output}`);
782
433
  }
783
-
784
- // Show migration output if there were migrations run
785
- if (result.output && result.output.trim()) {
786
- task.output = result.output;
787
- }
788
434
  }
789
435
  }
790
436
  ], {
437
+ renderer: 'default',
791
438
  rendererOptions: {
792
- collapseSubtasks: false
793
- }
439
+ collapse: false,
440
+ showSubtasks: false,
441
+ clearOutput: false,
442
+ showTimer: false,
443
+ removeEmptyLines: true
444
+ },
445
+ concurrent: false,
446
+ exitOnError: true
794
447
  });
795
-
796
- await tasks.run();
797
448
 
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'));
449
+ await tasks.run();
801
450
 
451
+ // Configure NGINX if needed (silent)
452
+ if (requiresNginx(config.frontendDomain, config.apiDomain)) {
802
453
  try {
803
454
  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
455
+ frontendDomain: config.frontendDomain,
456
+ apiDomain: config.apiDomain,
457
+ frontendPort: config.frontendPort,
458
+ apiPort: config.apiPort
808
459
  });
809
460
  } 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'));
461
+ console.log(chalk.yellow('⚠ NGINX configuration skipped (manual setup may be required)'));
812
462
  }
813
463
  }
814
464
 
@@ -818,20 +468,14 @@ Support: support@ante.ph
818
468
  version: '1.0.0'
819
469
  };
820
470
 
821
- // Only include domain if it exists
822
- if (config.domain) {
823
- installConfig.domain = config.domain;
824
- }
825
-
826
471
  saveInstallConfig(installConfig);
827
472
 
828
473
  // Show success message
829
474
  showSuccess(config.installDir, credentials, config);
830
-
475
+
831
476
  } catch (error) {
832
477
  console.error(chalk.red('\n✗ Installation failed:'), error.message);
833
478
  console.error(chalk.gray('\nFor help, visit: https://docs.ante.ph/self-hosting/troubleshooting\n'));
834
479
  process.exit(1);
835
480
  }
836
481
  }
837
-