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 +1 -0
- package/package.json +1 -1
- package/src/commands/update.js +126 -6
- 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';
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
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
|