ante-erp-cli 1.11.44 ā 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 +1 -0
- package/package.json +1 -1
- package/src/commands/update.js +81 -5
- package/src/templates/docker-compose.yml.js +14 -0
- package/src/utils/docker.js +75 -0
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
package/src/commands/update.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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,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
|
package/src/utils/docker.js
CHANGED
|
@@ -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
|