ante-erp-cli 1.11.62 → 1.11.64

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.62",
3
+ "version": "1.11.64",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,7 +4,7 @@ import { join } from 'path';
4
4
  import { readFileSync, writeFileSync, renameSync } from 'fs';
5
5
  import semver from 'semver';
6
6
  import { getInstallDir } from '../utils/config.js';
7
- import { pullImagesSilent, stopServicesSilent, startServicesSilent, waitForServiceHealthy, runMigrations, pruneDockerSilent, downServicesSilent, forceRecreateServicesSilent, removeVolumesSilent, listVolumes } from '../utils/docker.js';
7
+ import { pullImagesSilent, stopServicesSilent, startServicesSilent, waitForServiceHealthy, runMigrations, pruneDockerSilent, downServicesSilent, forceRecreateServicesSilent, removeVolumesSilent, listVolumes, freeUpPorts } from '../utils/docker.js';
8
8
  import { backup } from './backup.js';
9
9
  import { getCurrentVersion, getLatestVersion } from './update-cli.js';
10
10
  import { generateDockerCompose } from '../templates/docker-compose.yml.js';
@@ -381,6 +381,45 @@ function formatStepTitle(step, total, description) {
381
381
  return `[${step}/${total}] ${description}`;
382
382
  }
383
383
 
384
+ /**
385
+ * Extract all ports used by services from docker-compose.yml
386
+ * @param {string} composeFile - Path to docker-compose.yml
387
+ * @param {string} envFile - Path to .env file
388
+ * @returns {number[]} Array of port numbers
389
+ */
390
+ function getRequiredPorts(composeFile, envFile) {
391
+ const envConfig = parseEnvFile(envFile);
392
+ const installed = detectInstalledApps(composeFile);
393
+
394
+ const ports = [];
395
+
396
+ // Core services - always needed
397
+ ports.push(parseInt(envConfig.BACKEND_PORT) || 3001); // Backend
398
+ ports.push(parseInt(envConfig.FRONTEND_PORT) || 8080); // Frontend
399
+
400
+ // Optional services - only add if installed
401
+ if (installed.hasGateApp) {
402
+ ports.push(parseInt(envConfig.GATE_APP_PORT) || 8081);
403
+ }
404
+ if (installed.hasGuardianApp) {
405
+ ports.push(parseInt(envConfig.GUARDIAN_APP_PORT) || 8082);
406
+ }
407
+ if (installed.hasFacialWeb) {
408
+ ports.push(parseInt(envConfig.FACIAL_WEB_PORT) || 8083);
409
+ }
410
+ if (installed.hasPosApp) {
411
+ ports.push(parseInt(envConfig.POS_APP_PORT) || 8084);
412
+ }
413
+ if (installed.hasMlmApp) {
414
+ ports.push(parseInt(envConfig.MLM_APP_PORT) || 9005);
415
+ }
416
+ if (installed.hasBackendMlm) {
417
+ ports.push(parseInt(envConfig.BACKEND_MLM_PORT) || 4001);
418
+ }
419
+
420
+ return ports;
421
+ }
422
+
384
423
  /**
385
424
  * Database volumes that should be preserved during rebuild
386
425
  * These contain critical data and should never be deleted
@@ -459,7 +498,7 @@ export async function update(options) {
459
498
  let servicesToInstall = { gateApp: false, guardianApp: false, facialWeb: false, posApp: false, mlmApp: false };
460
499
 
461
500
  // Calculate total steps dynamically
462
- let totalSteps = 6; // Base steps: check services, pull, stop, start, backend health, migrations
501
+ let totalSteps = 7; // Base steps: check services, pull, stop, free ports, start, backend health, migrations
463
502
  if (!options.skipBackup) totalSteps += 2; // Pre-start + backup steps
464
503
  if (hasNewServices) totalSteps++; // Install new services step
465
504
  if (options.rebuild) totalSteps += 2; // Rebuild steps: down containers + remove volumes
@@ -499,6 +538,7 @@ export async function update(options) {
499
538
  const stepRebuildDown = options.rebuild ? ++currentStep : null;
500
539
  const stepRebuildVolumes = options.rebuild ? ++currentStep : null;
501
540
  const stepStop = !options.rebuild ? ++currentStep : null; // Skip if rebuild (already downed)
541
+ const stepFreePorts = ++currentStep; // Always free up ports before starting
502
542
  const stepStart = ++currentStep;
503
543
  const stepBackendHealth = ++currentStep;
504
544
  const stepGateHealth = (hasGateApp || missingServices.gateApp) ? ++currentStep : null;
@@ -620,6 +660,22 @@ export async function update(options) {
620
660
  await stopServicesSilent(composeFile);
621
661
  }
622
662
  },
663
+ {
664
+ title: formatStepTitle(stepFreePorts, totalSteps, 'Freeing up required ports'),
665
+ task: async (ctx, task) => {
666
+ // Get all ports that will be needed by our services
667
+ const requiredPorts = getRequiredPorts(composeFile, envFile);
668
+
669
+ // Kill any processes (like orphaned docker-proxy) using these ports
670
+ const result = await freeUpPorts(requiredPorts);
671
+
672
+ if (result.portsFreed.length > 0) {
673
+ task.title = formatStepTitle(stepFreePorts, totalSteps, `Freed ports: ${result.portsFreed.join(', ')} (killed ${result.totalKilled} processes)`);
674
+ } else {
675
+ task.title = formatStepTitle(stepFreePorts, totalSteps, 'All required ports are available');
676
+ }
677
+ }
678
+ },
623
679
  {
624
680
  title: formatStepTitle(stepStart, totalSteps, options.rebuild ? 'Starting with fresh containers' : 'Starting with new images'),
625
681
  task: async () => {
@@ -1,5 +1,66 @@
1
1
  import { execa } from 'execa';
2
2
 
3
+ /**
4
+ * Kill processes using a specific port
5
+ * This is useful when docker-proxy processes are left behind after a failed container stop
6
+ * @param {number} port - Port number to free up
7
+ * @returns {Promise<{killed: boolean, pids: number[]}>}
8
+ */
9
+ export async function killProcessesOnPort(port) {
10
+ const pids = [];
11
+
12
+ try {
13
+ // Use lsof to find processes using the port
14
+ const { stdout } = await execa('lsof', ['-i', `:${port}`, '-t'], {
15
+ stdout: 'pipe',
16
+ stderr: 'pipe',
17
+ reject: false
18
+ });
19
+
20
+ if (stdout && stdout.trim()) {
21
+ const foundPids = stdout.trim().split('\n').map(p => parseInt(p.trim())).filter(p => !isNaN(p));
22
+
23
+ for (const pid of foundPids) {
24
+ try {
25
+ await execa('kill', ['-9', pid.toString()], {
26
+ stdout: 'pipe',
27
+ stderr: 'pipe',
28
+ reject: false
29
+ });
30
+ pids.push(pid);
31
+ } catch {
32
+ // Ignore errors - process might have already exited
33
+ }
34
+ }
35
+ }
36
+
37
+ return { killed: pids.length > 0, pids };
38
+ } catch {
39
+ // lsof might not be installed or port not in use
40
+ return { killed: false, pids: [] };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Free up multiple ports by killing any processes using them
46
+ * @param {number[]} ports - Array of port numbers to free up
47
+ * @returns {Promise<{portsFreed: number[], totalKilled: number}>}
48
+ */
49
+ export async function freeUpPorts(ports) {
50
+ const portsFreed = [];
51
+ let totalKilled = 0;
52
+
53
+ for (const port of ports) {
54
+ const result = await killProcessesOnPort(port);
55
+ if (result.killed) {
56
+ portsFreed.push(port);
57
+ totalKilled += result.pids.length;
58
+ }
59
+ }
60
+
61
+ return { portsFreed, totalKilled };
62
+ }
63
+
3
64
  /**
4
65
  * Pull Docker images
5
66
  * @param {string} composeFile - Path to docker-compose.yml
@@ -32,14 +93,15 @@ export async function startServices(composeFile, services = []) {
32
93
  * Stop Docker services
33
94
  * @param {string} composeFile - Path to docker-compose.yml
34
95
  * @param {string[]} services - Specific services to stop (optional)
96
+ * @param {number} timeout - Timeout in seconds before force kill (default: 30)
35
97
  * @returns {Promise<void>}
36
98
  */
37
- export async function stopServices(composeFile, services = []) {
38
- const args = ['compose', '-f', composeFile, 'stop'];
99
+ export async function stopServices(composeFile, services = [], timeout = 30) {
100
+ const args = ['compose', '-f', composeFile, 'stop', '--timeout', timeout.toString()];
39
101
  if (services.length > 0) {
40
102
  args.push(...services);
41
103
  }
42
-
104
+
43
105
  await execa('docker', args, {
44
106
  stdio: 'inherit'
45
107
  });
@@ -237,15 +299,16 @@ export async function waitForHealthy(composeFile, services, timeout = 60) {
237
299
  * Down all services and optionally remove volumes
238
300
  * @param {string} composeFile - Path to docker-compose.yml
239
301
  * @param {boolean} removeVolumes - Remove volumes
302
+ * @param {number} timeout - Timeout in seconds before force kill (default: 30)
240
303
  * @returns {Promise<void>}
241
304
  */
242
- export async function downServices(composeFile, removeVolumes = false) {
243
- const args = ['compose', '-f', composeFile, 'down'];
244
-
305
+ export async function downServices(composeFile, removeVolumes = false, timeout = 30) {
306
+ const args = ['compose', '-f', composeFile, 'down', '--timeout', timeout.toString()];
307
+
245
308
  if (removeVolumes) {
246
309
  args.push('-v');
247
310
  }
248
-
311
+
249
312
  await execa('docker', args, {
250
313
  stdio: 'inherit'
251
314
  });
@@ -313,10 +376,11 @@ export async function startServicesSilent(composeFile, services = []) {
313
376
  * Stop Docker services (silent version - no output)
314
377
  * @param {string} composeFile - Path to docker-compose.yml
315
378
  * @param {string[]} services - Specific services to stop (optional)
379
+ * @param {number} timeout - Timeout in seconds before force kill (default: 30)
316
380
  * @returns {Promise<void>}
317
381
  */
318
- export async function stopServicesSilent(composeFile, services = []) {
319
- const args = ['compose', '-f', composeFile, 'stop'];
382
+ export async function stopServicesSilent(composeFile, services = [], timeout = 30) {
383
+ const args = ['compose', '-f', composeFile, 'stop', '--timeout', timeout.toString()];
320
384
  if (services.length > 0) {
321
385
  args.push(...services);
322
386
  }
@@ -342,10 +406,11 @@ export async function pruneDockerSilent() {
342
406
  * Down all services (silent version - no output)
343
407
  * @param {string} composeFile - Path to docker-compose.yml
344
408
  * @param {boolean} removeVolumes - Remove volumes
409
+ * @param {number} timeout - Timeout in seconds before force kill (default: 30)
345
410
  * @returns {Promise<void>}
346
411
  */
347
- export async function downServicesSilent(composeFile, removeVolumes = false) {
348
- const args = ['compose', '-f', composeFile, 'down'];
412
+ export async function downServicesSilent(composeFile, removeVolumes = false, timeout = 30) {
413
+ const args = ['compose', '-f', composeFile, 'down', '--timeout', timeout.toString()];
349
414
 
350
415
  if (removeVolumes) {
351
416
  args.push('-v');