ante-erp-cli 1.11.44 → 1.11.46

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.44",
3
+ "version": "1.11.46",
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';
@@ -363,8 +363,30 @@ function formatStepTitle(step, total, description) {
363
363
  return `[${step}/${total}] ${description}`;
364
364
  }
365
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
+
366
384
  /**
367
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
368
390
  */
369
391
  export async function update(options) {
370
392
  try {
@@ -372,8 +394,22 @@ export async function update(options) {
372
394
  const composeFile = join(installDir, 'docker-compose.yml');
373
395
  const envFile = join(installDir, '.env');
374
396
 
397
+ // Get the project name from docker-compose (usually directory name)
398
+ const projectName = 'ante-erp';
399
+
375
400
  console.log(chalk.bold('\nšŸ”„ ANTE ERP Update\n'));
376
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
+
377
413
  // Check CLI version first
378
414
  try {
379
415
  const currentVersion = getCurrentVersion();
@@ -408,6 +444,7 @@ export async function update(options) {
408
444
  let totalSteps = 6; // Base steps: check services, pull, stop, start, backend health, migrations
409
445
  if (!options.skipBackup) totalSteps += 2; // Pre-start + backup steps
410
446
  if (hasNewServices) totalSteps++; // Install new services step
447
+ if (options.rebuild) totalSteps += 2; // Rebuild steps: down containers + remove volumes
411
448
  if (hasGateApp || missingServices.gateApp) totalSteps++; // Gate App health check
412
449
  if (hasGuardianApp || missingServices.guardianApp) totalSteps++; // Guardian App health check
413
450
  if (hasFacialWeb || missingServices.facialWeb) totalSteps++; // Facial Web health check
@@ -431,7 +468,9 @@ export async function update(options) {
431
468
  const stepCheckServices = ++currentStep;
432
469
  const stepInstallServices = hasNewServices ? ++currentStep : null;
433
470
  const stepPull = ++currentStep;
434
- 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)
435
474
  const stepStart = ++currentStep;
436
475
  const stepBackendHealth = ++currentStep;
437
476
  const stepGateHealth = (hasGateApp || missingServices.gateApp) ? ++currentStep : null;
@@ -516,15 +555,52 @@ export async function update(options) {
516
555
  }
517
556
  },
518
557
  {
519
- 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,
520
591
  task: async () => {
521
592
  await stopServicesSilent(composeFile);
522
593
  }
523
594
  },
524
595
  {
525
- title: formatStepTitle(stepStart, totalSteps, 'Starting with new images'),
596
+ title: formatStepTitle(stepStart, totalSteps, options.rebuild ? 'Starting with fresh containers' : 'Starting with new images'),
526
597
  task: async () => {
527
- await startServicesSilent(composeFile);
598
+ if (options.rebuild) {
599
+ // Force recreate all containers
600
+ await forceRecreateServicesSilent(composeFile);
601
+ } else {
602
+ await startServicesSilent(composeFile);
603
+ }
528
604
  }
529
605
  },
530
606
  {
@@ -211,6 +211,11 @@ ${installGate ? `
211
211
  - "${gateAppPort}:3000"
212
212
  networks:
213
213
  - ante-network
214
+ # Security hardening
215
+ security_opt:
216
+ - no-new-privileges:true
217
+ tmpfs:
218
+ - /tmp:noexec,nosuid,size=100m
214
219
  healthcheck:
215
220
  test: ["CMD", "curl", "-f", "http://localhost:3000"]
216
221
  interval: 30s
@@ -239,6 +244,11 @@ ${installGate ? `
239
244
  - "${guardianAppPort}:9003"
240
245
  networks:
241
246
  - ante-network
247
+ # Security hardening
248
+ security_opt:
249
+ - no-new-privileges:true
250
+ tmpfs:
251
+ - /tmp:noexec,nosuid,size=100m
242
252
  healthcheck:
243
253
  test: ["CMD", "curl", "-f", "http://localhost:9003"]
244
254
  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