ante-erp-cli 1.11.43 → 1.11.45

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/bin/ante-cli.js CHANGED
@@ -76,6 +76,7 @@ program
76
76
  .option('--skip-backup', 'Skip automatic backup before update')
77
77
  .option('--skip-cleanup', 'Skip Docker cleanup after update')
78
78
  .option('--force', 'Force update without confirmation')
79
+ .option('--rebuild', 'Force rebuild: remove containers and app volumes, recreate from fresh images (preserves databases)')
79
80
  .action(update);
80
81
 
81
82
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.11.43",
3
+ "version": "1.11.45",
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 } from '../utils/docker.js';
7
+ import { pullImagesSilent, stopServicesSilent, startServicesSilent, waitForServiceHealthy, runMigrations, pruneDockerSilent, downServicesSilent, forceRecreateServicesSilent, removeVolumesSilent, listVolumes } 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';
@@ -240,6 +240,7 @@ COMPANY_ID=1
240
240
  /**
241
241
  * Ensure customer app JWT configuration exists in .env file
242
242
  * @param {string} envFile - Path to .env file
243
+ * @returns {boolean} True if any modifications were made
243
244
  */
244
245
  function ensureCustomerAppJwtConfig(envFile) {
245
246
  let envContent = readFileSync(envFile, 'utf8');
@@ -314,6 +315,43 @@ function ensureCustomerAppJwtConfig(envFile) {
314
315
  return modified;
315
316
  }
316
317
 
318
+ /**
319
+ * Refresh docker-compose.yml to ensure it has latest environment variable mappings
320
+ * This regenerates the docker-compose.yml without adding new services
321
+ * @param {string} composeFile - Path to docker-compose.yml
322
+ * @param {string} envFile - Path to .env file
323
+ */
324
+ function refreshDockerCompose(composeFile, envFile) {
325
+ const envConfig = parseEnvFile(envFile);
326
+ const currentInstalled = detectInstalledApps(composeFile);
327
+
328
+ // Generate new docker-compose.yml with existing services only
329
+ const newCompose = generateDockerCompose({
330
+ frontendPort: parseInt(envConfig.FRONTEND_PORT) || 8080,
331
+ backendPort: parseInt(envConfig.BACKEND_PORT) || 3001,
332
+ gateAppPort: parseInt(envConfig.GATE_APP_PORT) || 8081,
333
+ guardianAppPort: parseInt(envConfig.GUARDIAN_APP_PORT) || 8082,
334
+ facialWebPort: parseInt(envConfig.FACIAL_WEB_PORT) || 8083,
335
+ posAppPort: parseInt(envConfig.POS_APP_PORT) || 8084,
336
+ clientAppPort: parseInt(envConfig.CLIENT_APP_PORT) || 9005,
337
+ installMain: true,
338
+ installGate: currentInstalled.hasGateApp,
339
+ installGuardian: currentInstalled.hasGuardianApp,
340
+ installFacial: currentInstalled.hasFacialWeb,
341
+ installPos: currentInstalled.hasPosApp,
342
+ installClient: currentInstalled.hasClientApp,
343
+ companyId: parseInt(envConfig.COMPANY_ID) || 1
344
+ });
345
+
346
+ // Backup existing docker-compose.yml
347
+ const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
348
+ renameSync(composeFile, `${composeFile}.${timestamp}.bak`);
349
+
350
+ // Write new docker-compose.yml
351
+ writeFileSync(composeFile, newCompose);
352
+ console.log('āœ… Docker Compose configuration refreshed');
353
+ }
354
+
317
355
  /**
318
356
  * Format step title with numbering
319
357
  * @param {number} step - Current step number
@@ -325,8 +363,30 @@ function formatStepTitle(step, total, description) {
325
363
  return `[${step}/${total}] ${description}`;
326
364
  }
327
365
 
366
+ /**
367
+ * Database volumes that should be preserved during rebuild
368
+ * These contain critical data and should never be deleted
369
+ */
370
+ const PROTECTED_VOLUMES = [
371
+ 'postgres_data',
372
+ 'mongodb_data',
373
+ 'redis_data'
374
+ ];
375
+
376
+ /**
377
+ * Application volumes that can be safely removed during rebuild
378
+ * These are recreated from fresh images
379
+ */
380
+ const REBUILD_VOLUMES = [
381
+ 'backend_uploads'
382
+ ];
383
+
328
384
  /**
329
385
  * Update ANTE to latest version
386
+ * @param {Object} options - Update options
387
+ * @param {boolean} options.rebuild - Force rebuild: remove app containers and volumes, recreate from fresh images
388
+ * @param {boolean} options.skipBackup - Skip backup before update
389
+ * @param {boolean} options.skipCleanup - Skip Docker cleanup after update
330
390
  */
331
391
  export async function update(options) {
332
392
  try {
@@ -334,8 +394,22 @@ export async function update(options) {
334
394
  const composeFile = join(installDir, 'docker-compose.yml');
335
395
  const envFile = join(installDir, '.env');
336
396
 
397
+ // Get the project name from docker-compose (usually directory name)
398
+ const projectName = 'ante-erp';
399
+
337
400
  console.log(chalk.bold('\nšŸ”„ ANTE ERP Update\n'));
338
401
 
402
+ // Show rebuild warning if flag is set
403
+ if (options.rebuild) {
404
+ console.log(chalk.yellow('āš ļø REBUILD MODE ENABLED'));
405
+ console.log(chalk.gray('This will:'));
406
+ console.log(chalk.gray(' • Stop and remove all containers'));
407
+ console.log(chalk.gray(' • Remove application volumes (uploads, cache)'));
408
+ console.log(chalk.gray(' • Preserve database volumes (PostgreSQL, MongoDB, Redis)'));
409
+ console.log(chalk.gray(' • Pull fresh images and recreate containers'));
410
+ console.log('');
411
+ }
412
+
339
413
  // Check CLI version first
340
414
  try {
341
415
  const currentVersion = getCurrentVersion();
@@ -370,6 +444,7 @@ export async function update(options) {
370
444
  let totalSteps = 6; // Base steps: check services, pull, stop, start, backend health, migrations
371
445
  if (!options.skipBackup) totalSteps += 2; // Pre-start + backup steps
372
446
  if (hasNewServices) totalSteps++; // Install new services step
447
+ if (options.rebuild) totalSteps += 2; // Rebuild steps: down containers + remove volumes
373
448
  if (hasGateApp || missingServices.gateApp) totalSteps++; // Gate App health check
374
449
  if (hasGuardianApp || missingServices.guardianApp) totalSteps++; // Guardian App health check
375
450
  if (hasFacialWeb || missingServices.facialWeb) totalSteps++; // Facial Web health check
@@ -378,7 +453,13 @@ export async function update(options) {
378
453
 
379
454
  // Ensure customer app JWT configuration exists (run before tasks)
380
455
  console.log(chalk.gray('Checking customer app JWT configuration...'));
381
- ensureCustomerAppJwtConfig(envFile);
456
+ const envModified = ensureCustomerAppJwtConfig(envFile);
457
+
458
+ // Refresh docker-compose.yml if .env was modified to ensure containers get new variables
459
+ if (envModified) {
460
+ console.log(chalk.gray('Refreshing Docker Compose configuration...'));
461
+ refreshDockerCompose(composeFile, envFile);
462
+ }
382
463
 
383
464
  // Pre-calculate step numbers for each task (fixes step numbering bug)
384
465
  let currentStep = 0;
@@ -387,7 +468,9 @@ export async function update(options) {
387
468
  const stepCheckServices = ++currentStep;
388
469
  const stepInstallServices = hasNewServices ? ++currentStep : null;
389
470
  const stepPull = ++currentStep;
390
- const stepStop = ++currentStep;
471
+ const stepRebuildDown = options.rebuild ? ++currentStep : null;
472
+ const stepRebuildVolumes = options.rebuild ? ++currentStep : null;
473
+ const stepStop = !options.rebuild ? ++currentStep : null; // Skip if rebuild (already downed)
391
474
  const stepStart = ++currentStep;
392
475
  const stepBackendHealth = ++currentStep;
393
476
  const stepGateHealth = (hasGateApp || missingServices.gateApp) ? ++currentStep : null;
@@ -472,15 +555,52 @@ export async function update(options) {
472
555
  }
473
556
  },
474
557
  {
475
- title: formatStepTitle(stepStop, totalSteps, 'Stopping services'),
558
+ title: stepRebuildDown ? formatStepTitle(stepRebuildDown, totalSteps, 'Removing containers (rebuild mode)') : '',
559
+ skip: () => !stepRebuildDown ? 'Not in rebuild mode' : false,
560
+ task: async () => {
561
+ // Down all containers (but not volumes - we handle those separately)
562
+ await downServicesSilent(composeFile, false);
563
+ }
564
+ },
565
+ {
566
+ title: stepRebuildVolumes ? formatStepTitle(stepRebuildVolumes, totalSteps, 'Removing application volumes (preserving databases)') : '',
567
+ skip: () => !stepRebuildVolumes ? 'Not in rebuild mode' : false,
568
+ task: async (ctx, task) => {
569
+ // Get all volumes for this project
570
+ const allVolumes = await listVolumes(projectName);
571
+
572
+ // Filter to only remove non-protected volumes
573
+ const volumesToRemove = allVolumes.filter(vol => {
574
+ const volumeSuffix = vol.replace(`${projectName}_`, '');
575
+ // Remove if it's in REBUILD_VOLUMES list OR not in PROTECTED_VOLUMES list
576
+ return REBUILD_VOLUMES.includes(volumeSuffix) ||
577
+ (!PROTECTED_VOLUMES.includes(volumeSuffix) && !vol.includes('postgres') && !vol.includes('mongodb') && !vol.includes('redis'));
578
+ });
579
+
580
+ if (volumesToRemove.length > 0) {
581
+ const result = await removeVolumesSilent(volumesToRemove);
582
+ task.title = formatStepTitle(stepRebuildVolumes, totalSteps, `Removed ${result.removed.length} application volumes (databases preserved)`);
583
+ } else {
584
+ task.title = formatStepTitle(stepRebuildVolumes, totalSteps, 'No application volumes to remove (databases preserved)');
585
+ }
586
+ }
587
+ },
588
+ {
589
+ title: stepStop ? formatStepTitle(stepStop, totalSteps, 'Stopping services') : '',
590
+ skip: () => !stepStop ? 'Skipped (rebuild mode)' : false,
476
591
  task: async () => {
477
592
  await stopServicesSilent(composeFile);
478
593
  }
479
594
  },
480
595
  {
481
- title: formatStepTitle(stepStart, totalSteps, 'Starting with new images'),
596
+ title: formatStepTitle(stepStart, totalSteps, options.rebuild ? 'Starting with fresh containers' : 'Starting with new images'),
482
597
  task: async () => {
483
- await startServicesSilent(composeFile);
598
+ if (options.rebuild) {
599
+ // Force recreate all containers
600
+ await forceRecreateServicesSilent(composeFile);
601
+ } else {
602
+ await startServicesSilent(composeFile);
603
+ }
484
604
  }
485
605
  },
486
606
  {
@@ -211,6 +211,13 @@ ${installGate ? `
211
211
  - "${gateAppPort}:3000"
212
212
  networks:
213
213
  - ante-network
214
+ # Security hardening
215
+ security_opt:
216
+ - no-new-privileges:true
217
+ read_only: true
218
+ tmpfs:
219
+ - /tmp:noexec,nosuid,size=100m
220
+ - /app/.next/cache:noexec,nosuid,size=200m
214
221
  healthcheck:
215
222
  test: ["CMD", "curl", "-f", "http://localhost:3000"]
216
223
  interval: 30s
@@ -239,6 +246,13 @@ ${installGate ? `
239
246
  - "${guardianAppPort}:9003"
240
247
  networks:
241
248
  - ante-network
249
+ # Security hardening
250
+ security_opt:
251
+ - no-new-privileges:true
252
+ read_only: true
253
+ tmpfs:
254
+ - /tmp:noexec,nosuid,size=100m
255
+ - /app/.next/cache:noexec,nosuid,size=200m
242
256
  healthcheck:
243
257
  test: ["CMD", "curl", "-f", "http://localhost:9003"]
244
258
  interval: 30s
@@ -338,6 +338,81 @@ export async function pruneDockerSilent() {
338
338
  });
339
339
  }
340
340
 
341
+ /**
342
+ * Down all services (silent version - no output)
343
+ * @param {string} composeFile - Path to docker-compose.yml
344
+ * @param {boolean} removeVolumes - Remove volumes
345
+ * @returns {Promise<void>}
346
+ */
347
+ export async function downServicesSilent(composeFile, removeVolumes = false) {
348
+ const args = ['compose', '-f', composeFile, 'down'];
349
+
350
+ if (removeVolumes) {
351
+ args.push('-v');
352
+ }
353
+
354
+ await execa('docker', args, {
355
+ stdout: 'pipe',
356
+ stderr: 'pipe'
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Start services with force recreate (silent version - no output)
362
+ * @param {string} composeFile - Path to docker-compose.yml
363
+ * @returns {Promise<void>}
364
+ */
365
+ export async function forceRecreateServicesSilent(composeFile) {
366
+ await execa('docker', ['compose', '-f', composeFile, 'up', '-d', '--force-recreate'], {
367
+ stdout: 'pipe',
368
+ stderr: 'pipe'
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Remove specific Docker volumes (silent version - no output)
374
+ * @param {string[]} volumeNames - Array of volume names to remove
375
+ * @returns {Promise<{success: boolean, removed: string[], failed: string[]}>}
376
+ */
377
+ export async function removeVolumesSilent(volumeNames) {
378
+ const removed = [];
379
+ const failed = [];
380
+
381
+ for (const volumeName of volumeNames) {
382
+ try {
383
+ await execa('docker', ['volume', 'rm', '-f', volumeName], {
384
+ stdout: 'pipe',
385
+ stderr: 'pipe'
386
+ });
387
+ removed.push(volumeName);
388
+ } catch (error) {
389
+ // Volume might not exist, which is fine
390
+ failed.push(volumeName);
391
+ }
392
+ }
393
+
394
+ return { success: true, removed, failed };
395
+ }
396
+
397
+ /**
398
+ * List Docker volumes matching a prefix
399
+ * @param {string} prefix - Volume name prefix to match
400
+ * @returns {Promise<string[]>}
401
+ */
402
+ export async function listVolumes(prefix) {
403
+ try {
404
+ const { stdout } = await execa('docker', ['volume', 'ls', '--format', '{{.Name}}'], {
405
+ stdout: 'pipe',
406
+ stderr: 'pipe'
407
+ });
408
+
409
+ const volumes = stdout.trim().split('\n').filter(v => v.startsWith(prefix));
410
+ return volumes;
411
+ } catch (error) {
412
+ return [];
413
+ }
414
+ }
415
+
341
416
  /**
342
417
  * Run Prisma migrations in backend container
343
418
  * @param {string} composeFile - Path to docker-compose.yml