ante-erp-cli 1.11.12 → 1.11.14
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 +1 -1
- package/src/commands/backup.js +22 -16
- package/src/commands/update-cli.js +6 -5
- package/src/commands/update.js +113 -106
- package/src/utils/docker.js +59 -0
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -92,9 +92,9 @@ export async function backup(options) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// Prompt for keyword if not provided via flag
|
|
95
|
+
// Prompt for keyword if not provided via flag (skip if silent mode)
|
|
96
96
|
let keyword = options.keyword;
|
|
97
|
-
if (!keyword) {
|
|
97
|
+
if (!keyword && !options.silent) {
|
|
98
98
|
const { backupKeyword } = await inquirer.prompt([
|
|
99
99
|
{
|
|
100
100
|
type: 'input',
|
|
@@ -113,7 +113,7 @@ export async function backup(options) {
|
|
|
113
113
|
: '';
|
|
114
114
|
const backupFile = options.output || join(backupDir, `ante-backup-${timestamp}${keywordPart}.tar.gz`);
|
|
115
115
|
|
|
116
|
-
const spinner = ora('Creating database backup...').start();
|
|
116
|
+
const spinner = options.silent ? null : ora('Creating database backup...').start();
|
|
117
117
|
|
|
118
118
|
try {
|
|
119
119
|
// Create temporary backup directory
|
|
@@ -121,7 +121,7 @@ export async function backup(options) {
|
|
|
121
121
|
await execa('mkdir', ['-p', tempDir]);
|
|
122
122
|
|
|
123
123
|
// Backup PostgreSQL - dump directly to file inside container
|
|
124
|
-
spinner.text = 'Backing up PostgreSQL database...';
|
|
124
|
+
if (spinner) spinner.text = 'Backing up PostgreSQL database...';
|
|
125
125
|
await execa('docker', [
|
|
126
126
|
'compose',
|
|
127
127
|
'-f',
|
|
@@ -150,7 +150,7 @@ export async function backup(options) {
|
|
|
150
150
|
const pgSize = pgSizeOutput.trim();
|
|
151
151
|
|
|
152
152
|
// Copy dump file from container to temp directory
|
|
153
|
-
spinner.text = `Copying PostgreSQL dump from container (${pgSize})...`;
|
|
153
|
+
if (spinner) spinner.text = `Copying PostgreSQL dump from container (${pgSize})...`;
|
|
154
154
|
await execa('docker', [
|
|
155
155
|
'cp',
|
|
156
156
|
'ante-postgres:/tmp/postgres.dump',
|
|
@@ -158,7 +158,7 @@ export async function backup(options) {
|
|
|
158
158
|
]);
|
|
159
159
|
|
|
160
160
|
// Backup MongoDB
|
|
161
|
-
spinner.text = 'Backing up MongoDB database...';
|
|
161
|
+
if (spinner) spinner.text = 'Backing up MongoDB database...';
|
|
162
162
|
|
|
163
163
|
// Read MongoDB password from .env file
|
|
164
164
|
const envPath = join(installDir, '.env');
|
|
@@ -207,7 +207,7 @@ export async function backup(options) {
|
|
|
207
207
|
const mongoSize = mongoSizeOutput.split('\t')[0];
|
|
208
208
|
|
|
209
209
|
// Move MongoDB dump to final structure (mongodb/ folder)
|
|
210
|
-
spinner.text = `Organizing MongoDB dump (${mongoSize})...`;
|
|
210
|
+
if (spinner) spinner.text = `Organizing MongoDB dump (${mongoSize})...`;
|
|
211
211
|
await execa('mv', [
|
|
212
212
|
join(mongoDumpTempDir, mongoInfo.database),
|
|
213
213
|
join(tempDir, 'mongodb')
|
|
@@ -221,7 +221,7 @@ export async function backup(options) {
|
|
|
221
221
|
const totalSize = totalSizeOutput.split('\t')[0];
|
|
222
222
|
|
|
223
223
|
// Create tar.gz archive
|
|
224
|
-
spinner.text = `Creating compressed archive (${totalSize})...`;
|
|
224
|
+
if (spinner) spinner.text = `Creating compressed archive (${totalSize})...`;
|
|
225
225
|
await execa('mkdir', ['-p', backupDir]);
|
|
226
226
|
await execa('tar', [
|
|
227
227
|
'-czf',
|
|
@@ -234,17 +234,23 @@ export async function backup(options) {
|
|
|
234
234
|
// Cleanup
|
|
235
235
|
await execa('rm', ['-rf', tempDir]);
|
|
236
236
|
|
|
237
|
-
spinner
|
|
237
|
+
if (spinner) {
|
|
238
|
+
spinner.succeed(chalk.green('Database backup created successfully!'));
|
|
239
|
+
}
|
|
238
240
|
|
|
239
|
-
// Show backup details
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
// Show backup details (only if not in silent mode)
|
|
242
|
+
if (!options.silent) {
|
|
243
|
+
const { stdout: size } = await execa('du', ['-h', backupFile]);
|
|
244
|
+
console.log(chalk.white('\n✓ PostgreSQL database backed up'));
|
|
245
|
+
console.log(chalk.white(`✓ MongoDB database backed up (${mongoResult.collections} collections, ${mongoResult.size})`));
|
|
246
|
+
console.log(chalk.cyan(`\nBackup file: ${backupFile}`));
|
|
247
|
+
console.log(chalk.gray(`Total size: ${size.split('\t')[0]}\n`));
|
|
248
|
+
}
|
|
245
249
|
|
|
246
250
|
} catch (error) {
|
|
247
|
-
spinner
|
|
251
|
+
if (spinner) {
|
|
252
|
+
spinner.fail('Backup failed');
|
|
253
|
+
}
|
|
248
254
|
throw error;
|
|
249
255
|
}
|
|
250
256
|
|
|
@@ -14,7 +14,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
14
14
|
* Get current CLI version
|
|
15
15
|
* @returns {string} Current version
|
|
16
16
|
*/
|
|
17
|
-
function getCurrentVersion() {
|
|
17
|
+
export function getCurrentVersion() {
|
|
18
18
|
const pkgPath = join(__dirname, '../../package.json');
|
|
19
19
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
20
20
|
return pkg.version;
|
|
@@ -22,10 +22,11 @@ function getCurrentVersion() {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Fetch latest version from npm registry
|
|
25
|
+
* @param {boolean} silent - If true, don't show spinner
|
|
25
26
|
* @returns {Promise<string>} Latest version
|
|
26
27
|
*/
|
|
27
|
-
async function getLatestVersion() {
|
|
28
|
-
const spinner = ora('Checking npm registry for latest version...').start();
|
|
28
|
+
export async function getLatestVersion(silent = false) {
|
|
29
|
+
const spinner = silent ? null : ora('Checking npm registry for latest version...').start();
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
32
|
const response = await fetch('https://registry.npmjs.org/ante-erp-cli/latest', {
|
|
@@ -37,11 +38,11 @@ async function getLatestVersion() {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const data = await response.json();
|
|
40
|
-
spinner.succeed('Retrieved latest version from npm');
|
|
41
|
+
if (spinner) spinner.succeed('Retrieved latest version from npm');
|
|
41
42
|
return data.version;
|
|
42
43
|
|
|
43
44
|
} catch (error) {
|
|
44
|
-
spinner.fail('Failed to check npm registry');
|
|
45
|
+
if (spinner) spinner.fail('Failed to check npm registry');
|
|
45
46
|
throw new Error(`Unable to fetch latest version: ${error.message}`);
|
|
46
47
|
}
|
|
47
48
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
2
|
import { Listr } from 'listr2';
|
|
4
3
|
import { join } from 'path';
|
|
5
4
|
import { readFileSync, writeFileSync, renameSync } from 'fs';
|
|
5
|
+
import semver from 'semver';
|
|
6
6
|
import { getInstallDir } from '../utils/config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { pullImagesSilent, stopServicesSilent, startServicesSilent, waitForServiceHealthy, runMigrations, pruneDockerSilent } from '../utils/docker.js';
|
|
8
8
|
import { backup } from './backup.js';
|
|
9
|
+
import { getCurrentVersion, getLatestVersion } from './update-cli.js';
|
|
9
10
|
import { generateDockerCompose } from '../templates/docker-compose.yml.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -207,6 +208,17 @@ COMPANY_ID=1
|
|
|
207
208
|
writeFileSync(envFile, envContent);
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Format step title with numbering
|
|
213
|
+
* @param {number} step - Current step number
|
|
214
|
+
* @param {number} total - Total number of steps
|
|
215
|
+
* @param {string} description - Step description
|
|
216
|
+
* @returns {string} Formatted title
|
|
217
|
+
*/
|
|
218
|
+
function formatStepTitle(step, total, description) {
|
|
219
|
+
return `[${step}/${total}] ${description}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
210
222
|
/**
|
|
211
223
|
* Update ANTE to latest version
|
|
212
224
|
*/
|
|
@@ -216,147 +228,127 @@ export async function update(options) {
|
|
|
216
228
|
const composeFile = join(installDir, 'docker-compose.yml');
|
|
217
229
|
const envFile = join(installDir, '.env');
|
|
218
230
|
|
|
219
|
-
// Detect which apps are installed
|
|
220
|
-
const { hasGateApp, hasGuardianApp, hasFacialWeb, hasPosApp } = detectInstalledApps(composeFile);
|
|
221
|
-
|
|
222
231
|
console.log(chalk.bold('\n🔄 ANTE ERP Update\n'));
|
|
223
232
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
console.error(chalk.
|
|
233
|
+
// Check CLI version first
|
|
234
|
+
try {
|
|
235
|
+
const currentVersion = getCurrentVersion();
|
|
236
|
+
const latestVersion = await getLatestVersion(true); // silent mode
|
|
237
|
+
|
|
238
|
+
if (!semver.gte(currentVersion, latestVersion)) {
|
|
239
|
+
console.error(chalk.red('❌ CLI version check failed!\n'));
|
|
240
|
+
console.error(chalk.gray(`You are using CLI version ${chalk.yellow(currentVersion)}`));
|
|
241
|
+
console.error(chalk.gray(`Latest version available: ${chalk.green(latestVersion)}\n`));
|
|
242
|
+
console.error(chalk.cyan('Please update the CLI first:'));
|
|
243
|
+
console.error(chalk.white(' npm install -g ante-erp-cli\n'));
|
|
244
|
+
console.error(chalk.gray('Then run \'ante update\' again.\n'));
|
|
231
245
|
process.exit(1);
|
|
232
246
|
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error(chalk.yellow('⚠️ Warning: Unable to check CLI version'));
|
|
249
|
+
console.error(chalk.gray(`Reason: ${error.message}`));
|
|
250
|
+
console.error(chalk.gray('Proceeding with update anyway...\n'));
|
|
251
|
+
}
|
|
233
252
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
type: 'confirm',
|
|
237
|
-
name: 'confirm',
|
|
238
|
-
message: 'Update ANTE to the latest version?',
|
|
239
|
-
default: true
|
|
240
|
-
}
|
|
241
|
-
]);
|
|
253
|
+
// Detect which apps are installed
|
|
254
|
+
const { hasGateApp, hasGuardianApp, hasFacialWeb, hasPosApp } = detectInstalledApps(composeFile);
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}
|
|
256
|
+
// Detect missing services that could be installed
|
|
257
|
+
const missingServices = detectMissingServices(composeFile);
|
|
258
|
+
const hasNewServices = missingServices.gateApp || missingServices.guardianApp || missingServices.facialWeb || missingServices.posApp;
|
|
248
259
|
|
|
249
|
-
// Track which services to install
|
|
260
|
+
// Track which services to install (will be set to missingServices if any found)
|
|
250
261
|
let servicesToInstall = { gateApp: false, guardianApp: false, facialWeb: false, posApp: false };
|
|
251
262
|
|
|
263
|
+
// Calculate total steps dynamically
|
|
264
|
+
let totalSteps = 6; // Base steps: check services, pull, stop, start, backend health, migrations
|
|
265
|
+
if (!options.skipBackup) totalSteps++; // Backup step
|
|
266
|
+
if (hasNewServices) totalSteps++; // Install new services step
|
|
267
|
+
if (hasGateApp || missingServices.gateApp) totalSteps++; // Gate App health check
|
|
268
|
+
if (hasGuardianApp || missingServices.guardianApp) totalSteps++; // Guardian App health check
|
|
269
|
+
if (hasFacialWeb || missingServices.facialWeb) totalSteps++; // Facial Web health check
|
|
270
|
+
if (hasPosApp || missingServices.posApp) totalSteps++; // POS App health check
|
|
271
|
+
if (!options.skipCleanup) totalSteps++; // Cleanup step
|
|
272
|
+
|
|
273
|
+
let currentStep = 0;
|
|
274
|
+
|
|
252
275
|
const tasks = new Listr([
|
|
253
276
|
{
|
|
254
|
-
title: 'Creating backup',
|
|
255
|
-
skip: () => options.skipBackup,
|
|
277
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Creating backup'),
|
|
278
|
+
skip: () => options.skipBackup ? (currentStep--, 'Backup skipped') : false,
|
|
256
279
|
task: async () => {
|
|
257
280
|
const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
|
|
258
|
-
await backup({
|
|
281
|
+
await backup({
|
|
282
|
+
output: join(installDir, 'backups', `pre-update-${timestamp}.tar.gz`),
|
|
283
|
+
silent: true
|
|
284
|
+
});
|
|
259
285
|
}
|
|
260
286
|
},
|
|
261
287
|
{
|
|
262
|
-
title: 'Checking for available new services',
|
|
288
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Checking for available new services'),
|
|
263
289
|
task: async (ctx, task) => {
|
|
264
|
-
const
|
|
290
|
+
const stepNumber = currentStep; // Capture current step number
|
|
265
291
|
|
|
266
|
-
if (!
|
|
267
|
-
task.
|
|
292
|
+
if (!hasNewServices) {
|
|
293
|
+
task.title = formatStepTitle(stepNumber, totalSteps, 'Checking for available new services (none found)');
|
|
294
|
+
ctx.missingServices = null;
|
|
268
295
|
return;
|
|
269
296
|
}
|
|
270
297
|
|
|
298
|
+
// Auto-install new services without prompting
|
|
299
|
+
servicesToInstall = missingServices;
|
|
300
|
+
|
|
271
301
|
const availableServices = [];
|
|
272
|
-
if (missingServices.gateApp) availableServices.push('Gate App
|
|
273
|
-
if (missingServices.guardianApp) availableServices.push('Guardian App
|
|
274
|
-
if (missingServices.facialWeb) availableServices.push('Facial Web
|
|
275
|
-
if (missingServices.posApp) availableServices.push('POS App
|
|
302
|
+
if (missingServices.gateApp) availableServices.push('Gate App');
|
|
303
|
+
if (missingServices.guardianApp) availableServices.push('Guardian App');
|
|
304
|
+
if (missingServices.facialWeb) availableServices.push('Facial Web');
|
|
305
|
+
if (missingServices.posApp) availableServices.push('POS App');
|
|
276
306
|
|
|
277
|
-
task.
|
|
307
|
+
task.title = formatStepTitle(stepNumber, totalSteps, `Found new services: ${availableServices.join(', ')}`);
|
|
278
308
|
ctx.missingServices = missingServices;
|
|
279
309
|
}
|
|
280
310
|
},
|
|
281
311
|
{
|
|
282
|
-
title:
|
|
283
|
-
skip: (
|
|
312
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Installing new services'),
|
|
313
|
+
skip: () => !hasNewServices ? (currentStep--, 'No new services to install') : false,
|
|
284
314
|
task: async (ctx, task) => {
|
|
285
|
-
|
|
286
|
-
servicesToInstall = ctx.missingServices;
|
|
287
|
-
task.output = 'Auto-installing new services (--force flag)';
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (!process.stdin.isTTY) {
|
|
292
|
-
task.skip('Skipping new services (non-interactive mode, use --force to auto-install)');
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
315
|
+
const stepNumber = currentStep; // Capture current step number
|
|
295
316
|
|
|
296
|
-
const availableServices = [];
|
|
297
|
-
if (ctx.missingServices.gateApp) availableServices.push('Gate App');
|
|
298
|
-
if (ctx.missingServices.guardianApp) availableServices.push('Guardian App');
|
|
299
|
-
if (ctx.missingServices.facialWeb) availableServices.push('Facial Web');
|
|
300
|
-
if (ctx.missingServices.posApp) availableServices.push('POS App');
|
|
301
|
-
|
|
302
|
-
const { installNew } = await inquirer.prompt([
|
|
303
|
-
{
|
|
304
|
-
type: 'confirm',
|
|
305
|
-
name: 'installNew',
|
|
306
|
-
message: `Install ${availableServices.join(', ')}?`,
|
|
307
|
-
default: true
|
|
308
|
-
}
|
|
309
|
-
]);
|
|
310
|
-
|
|
311
|
-
if (installNew) {
|
|
312
|
-
servicesToInstall = ctx.missingServices;
|
|
313
|
-
task.output = `Installing: ${availableServices.join(', ')}`;
|
|
314
|
-
} else {
|
|
315
|
-
task.skip('User declined installation of new services');
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
title: 'Installing new services',
|
|
321
|
-
skip: () => !servicesToInstall.gateApp && !servicesToInstall.guardianApp && !servicesToInstall.facialWeb && !servicesToInstall.posApp,
|
|
322
|
-
task: async (ctx, task) => {
|
|
323
317
|
const servicesAdded = [];
|
|
324
318
|
if (servicesToInstall.gateApp) servicesAdded.push('Gate App');
|
|
325
319
|
if (servicesToInstall.guardianApp) servicesAdded.push('Guardian App');
|
|
326
320
|
if (servicesToInstall.facialWeb) servicesAdded.push('Facial Web');
|
|
327
321
|
if (servicesToInstall.posApp) servicesAdded.push('POS App');
|
|
328
322
|
|
|
329
|
-
task.output = `Adding ${servicesAdded.join(', ')} to configuration...`;
|
|
330
|
-
|
|
331
323
|
// Update docker-compose.yml with new services
|
|
332
324
|
updateDockerCompose(composeFile, envFile, servicesToInstall);
|
|
333
325
|
|
|
334
326
|
// Update .env file with new app configuration
|
|
335
327
|
updateEnvFile(envFile, servicesToInstall);
|
|
336
328
|
|
|
337
|
-
task.
|
|
329
|
+
task.title = formatStepTitle(stepNumber, totalSteps, `Installed: ${servicesAdded.join(', ')}`);
|
|
338
330
|
}
|
|
339
331
|
},
|
|
340
332
|
{
|
|
341
|
-
title: 'Pulling latest images',
|
|
333
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Pulling latest Docker images'),
|
|
342
334
|
task: async () => {
|
|
343
|
-
await
|
|
335
|
+
await pullImagesSilent(composeFile);
|
|
344
336
|
}
|
|
345
337
|
},
|
|
346
338
|
{
|
|
347
|
-
title: 'Stopping services',
|
|
339
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Stopping services'),
|
|
348
340
|
task: async () => {
|
|
349
|
-
await
|
|
341
|
+
await stopServicesSilent(composeFile);
|
|
350
342
|
}
|
|
351
343
|
},
|
|
352
344
|
{
|
|
353
|
-
title: 'Starting with new images',
|
|
345
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Starting with new images'),
|
|
354
346
|
task: async () => {
|
|
355
|
-
await
|
|
347
|
+
await startServicesSilent(composeFile);
|
|
356
348
|
}
|
|
357
349
|
},
|
|
358
350
|
{
|
|
359
|
-
title: 'Waiting for backend to be healthy',
|
|
351
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for backend to be healthy'),
|
|
360
352
|
task: async () => {
|
|
361
353
|
const healthy = await waitForServiceHealthy(composeFile, 'backend', 120);
|
|
362
354
|
if (!healthy) {
|
|
@@ -365,8 +357,8 @@ export async function update(options) {
|
|
|
365
357
|
}
|
|
366
358
|
},
|
|
367
359
|
{
|
|
368
|
-
title: 'Waiting for Gate App to be healthy',
|
|
369
|
-
skip: () => !hasGateApp && !servicesToInstall.gateApp,
|
|
360
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for Gate App to be healthy'),
|
|
361
|
+
skip: () => !hasGateApp && !servicesToInstall.gateApp ? (currentStep--, 'Gate App not installed') : false,
|
|
370
362
|
task: async () => {
|
|
371
363
|
const healthy = await waitForServiceHealthy(composeFile, 'gate-app', 60);
|
|
372
364
|
if (!healthy) {
|
|
@@ -375,8 +367,8 @@ export async function update(options) {
|
|
|
375
367
|
}
|
|
376
368
|
},
|
|
377
369
|
{
|
|
378
|
-
title: 'Waiting for Guardian App to be healthy',
|
|
379
|
-
skip: () => !hasGuardianApp && !servicesToInstall.guardianApp,
|
|
370
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for Guardian App to be healthy'),
|
|
371
|
+
skip: () => !hasGuardianApp && !servicesToInstall.guardianApp ? (currentStep--, 'Guardian App not installed') : false,
|
|
380
372
|
task: async () => {
|
|
381
373
|
const healthy = await waitForServiceHealthy(composeFile, 'guardian-app', 60);
|
|
382
374
|
if (!healthy) {
|
|
@@ -385,8 +377,8 @@ export async function update(options) {
|
|
|
385
377
|
}
|
|
386
378
|
},
|
|
387
379
|
{
|
|
388
|
-
title: 'Waiting for Facial Web to be healthy',
|
|
389
|
-
skip: () => !hasFacialWeb && !servicesToInstall.facialWeb,
|
|
380
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for Facial Web to be healthy'),
|
|
381
|
+
skip: () => !hasFacialWeb && !servicesToInstall.facialWeb ? (currentStep--, 'Facial Web not installed') : false,
|
|
390
382
|
task: async () => {
|
|
391
383
|
const healthy = await waitForServiceHealthy(composeFile, 'facial-web', 60);
|
|
392
384
|
if (!healthy) {
|
|
@@ -395,8 +387,8 @@ export async function update(options) {
|
|
|
395
387
|
}
|
|
396
388
|
},
|
|
397
389
|
{
|
|
398
|
-
title: 'Waiting for POS App to be healthy',
|
|
399
|
-
skip: () => !hasPosApp && !servicesToInstall.posApp,
|
|
390
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for POS App to be healthy'),
|
|
391
|
+
skip: () => !hasPosApp && !servicesToInstall.posApp ? (currentStep--, 'POS App not installed') : false,
|
|
400
392
|
task: async () => {
|
|
401
393
|
const healthy = await waitForServiceHealthy(composeFile, 'pos-app', 60);
|
|
402
394
|
if (!healthy) {
|
|
@@ -405,35 +397,50 @@ export async function update(options) {
|
|
|
405
397
|
}
|
|
406
398
|
},
|
|
407
399
|
{
|
|
408
|
-
title: 'Running database migrations',
|
|
400
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Running database migrations'),
|
|
409
401
|
task: async (ctx, task) => {
|
|
402
|
+
const stepNumber = currentStep; // Capture current step number
|
|
410
403
|
const result = await runMigrations(composeFile);
|
|
411
404
|
|
|
412
405
|
if (!result.success) {
|
|
413
406
|
throw new Error(`Migration failed:\n${result.output}`);
|
|
414
407
|
}
|
|
415
408
|
|
|
416
|
-
//
|
|
417
|
-
if (result.output && result.output.
|
|
418
|
-
task.
|
|
409
|
+
// Update title if migrations were applied
|
|
410
|
+
if (result.output && result.output.includes('Applied')) {
|
|
411
|
+
task.title = formatStepTitle(stepNumber, totalSteps, 'Database migrations applied');
|
|
412
|
+
} else {
|
|
413
|
+
task.title = formatStepTitle(stepNumber, totalSteps, 'Database migrations (up to date)');
|
|
419
414
|
}
|
|
420
415
|
}
|
|
421
416
|
},
|
|
422
417
|
{
|
|
423
|
-
title: 'Cleaning up unused Docker resources',
|
|
424
|
-
skip: () => options.skipCleanup,
|
|
418
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Cleaning up unused Docker resources'),
|
|
419
|
+
skip: () => options.skipCleanup ? (currentStep--, 'Cleanup skipped') : false,
|
|
425
420
|
task: async (ctx, task) => {
|
|
421
|
+
const stepNumber = currentStep; // Capture current step number
|
|
426
422
|
try {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
task.output = 'Removed unused Docker images and containers';
|
|
423
|
+
await pruneDockerSilent();
|
|
424
|
+
task.title = formatStepTitle(stepNumber, totalSteps, 'Cleaned up unused Docker resources');
|
|
430
425
|
} catch (error) {
|
|
431
426
|
// Non-critical error - don't fail the update
|
|
432
|
-
task.skip(`Cleanup
|
|
427
|
+
task.skip(`Cleanup failed: ${error.message}`);
|
|
428
|
+
currentStep--;
|
|
433
429
|
}
|
|
434
430
|
}
|
|
435
431
|
}
|
|
436
|
-
]
|
|
432
|
+
], {
|
|
433
|
+
renderer: 'default',
|
|
434
|
+
rendererOptions: {
|
|
435
|
+
collapse: false,
|
|
436
|
+
showSubtasks: false,
|
|
437
|
+
clearOutput: false,
|
|
438
|
+
showTimer: false,
|
|
439
|
+
removeEmptyLines: true
|
|
440
|
+
},
|
|
441
|
+
concurrent: false,
|
|
442
|
+
exitOnError: true
|
|
443
|
+
});
|
|
437
444
|
|
|
438
445
|
await tasks.run();
|
|
439
446
|
|
package/src/utils/docker.js
CHANGED
|
@@ -279,6 +279,65 @@ export async function pruneDocker() {
|
|
|
279
279
|
});
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Pull Docker images (silent version - no output)
|
|
284
|
+
* @param {string} composeFile - Path to docker-compose.yml
|
|
285
|
+
* @returns {Promise<void>}
|
|
286
|
+
*/
|
|
287
|
+
export async function pullImagesSilent(composeFile) {
|
|
288
|
+
await execa('docker', ['compose', '-f', composeFile, 'pull'], {
|
|
289
|
+
stdout: 'pipe',
|
|
290
|
+
stderr: 'pipe'
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Start Docker services (silent version - no output)
|
|
296
|
+
* @param {string} composeFile - Path to docker-compose.yml
|
|
297
|
+
* @param {string[]} services - Specific services to start (optional)
|
|
298
|
+
* @returns {Promise<void>}
|
|
299
|
+
*/
|
|
300
|
+
export async function startServicesSilent(composeFile, services = []) {
|
|
301
|
+
const args = ['compose', '-f', composeFile, 'up', '-d'];
|
|
302
|
+
if (services.length > 0) {
|
|
303
|
+
args.push(...services);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await execa('docker', args, {
|
|
307
|
+
stdout: 'pipe',
|
|
308
|
+
stderr: 'pipe'
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Stop Docker services (silent version - no output)
|
|
314
|
+
* @param {string} composeFile - Path to docker-compose.yml
|
|
315
|
+
* @param {string[]} services - Specific services to stop (optional)
|
|
316
|
+
* @returns {Promise<void>}
|
|
317
|
+
*/
|
|
318
|
+
export async function stopServicesSilent(composeFile, services = []) {
|
|
319
|
+
const args = ['compose', '-f', composeFile, 'stop'];
|
|
320
|
+
if (services.length > 0) {
|
|
321
|
+
args.push(...services);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await execa('docker', args, {
|
|
325
|
+
stdout: 'pipe',
|
|
326
|
+
stderr: 'pipe'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Prune unused Docker resources (silent version - no output)
|
|
332
|
+
* @returns {Promise<void>}
|
|
333
|
+
*/
|
|
334
|
+
export async function pruneDockerSilent() {
|
|
335
|
+
await execa('docker', ['system', 'prune', '-af'], {
|
|
336
|
+
stdout: 'pipe',
|
|
337
|
+
stderr: 'pipe'
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
282
341
|
/**
|
|
283
342
|
* Run Prisma migrations in backend container
|
|
284
343
|
* @param {string} composeFile - Path to docker-compose.yml
|