ante-erp-cli 1.11.12 ā 1.11.13
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/update-cli.js +6 -5
- package/src/commands/update.js +99 -104
- package/src/utils/docker.js +59 -0
package/package.json
CHANGED
|
@@ -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,109 +228,81 @@ 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
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
name: 'confirm',
|
|
238
|
-
message: 'Update ANTE to the latest version?',
|
|
239
|
-
default: true
|
|
240
|
-
}
|
|
241
|
-
]);
|
|
242
|
-
|
|
243
|
-
if (!confirm) {
|
|
244
|
-
console.log(chalk.gray('\nUpdate cancelled.\n'));
|
|
245
|
-
return;
|
|
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'));
|
|
247
251
|
}
|
|
248
252
|
|
|
253
|
+
// Detect which apps are installed
|
|
254
|
+
const { hasGateApp, hasGuardianApp, hasFacialWeb, hasPosApp } = detectInstalledApps(composeFile);
|
|
255
|
+
|
|
249
256
|
// Track which services to install
|
|
250
257
|
let servicesToInstall = { gateApp: false, guardianApp: false, facialWeb: false, posApp: false };
|
|
251
258
|
|
|
259
|
+
// Calculate total steps dynamically
|
|
260
|
+
let totalSteps = 7; // Base steps: check services, install services, pull, stop, start, backend health, migrations
|
|
261
|
+
if (!options.skipBackup) totalSteps++; // Backup step
|
|
262
|
+
if (hasGateApp || servicesToInstall.gateApp) totalSteps++; // Gate App health check
|
|
263
|
+
if (hasGuardianApp || servicesToInstall.guardianApp) totalSteps++; // Guardian App health check
|
|
264
|
+
if (hasFacialWeb || servicesToInstall.facialWeb) totalSteps++; // Facial Web health check
|
|
265
|
+
if (hasPosApp || servicesToInstall.posApp) totalSteps++; // POS App health check
|
|
266
|
+
if (!options.skipCleanup) totalSteps++; // Cleanup step
|
|
267
|
+
|
|
268
|
+
let currentStep = 0;
|
|
269
|
+
|
|
252
270
|
const tasks = new Listr([
|
|
253
271
|
{
|
|
254
|
-
title: 'Creating backup',
|
|
255
|
-
skip: () => options.skipBackup,
|
|
272
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Creating backup'),
|
|
273
|
+
skip: () => options.skipBackup ? (currentStep--, 'Backup skipped') : false,
|
|
256
274
|
task: async () => {
|
|
257
275
|
const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
|
|
258
276
|
await backup({ output: join(installDir, 'backups', `pre-update-${timestamp}.tar.gz`) });
|
|
259
277
|
}
|
|
260
278
|
},
|
|
261
279
|
{
|
|
262
|
-
title: 'Checking for available new services',
|
|
280
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Checking for available new services'),
|
|
263
281
|
task: async (ctx, task) => {
|
|
264
282
|
const missingServices = detectMissingServices(composeFile);
|
|
265
283
|
|
|
266
284
|
if (!missingServices.gateApp && !missingServices.guardianApp && !missingServices.facialWeb && !missingServices.posApp) {
|
|
267
|
-
task.
|
|
285
|
+
task.title = formatStepTitle(currentStep, totalSteps, 'Checking for available new services (none found)');
|
|
286
|
+
ctx.missingServices = null;
|
|
268
287
|
return;
|
|
269
288
|
}
|
|
270
289
|
|
|
290
|
+
// Auto-install new services without prompting
|
|
291
|
+
servicesToInstall = missingServices;
|
|
292
|
+
|
|
271
293
|
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
|
|
294
|
+
if (missingServices.gateApp) availableServices.push('Gate App');
|
|
295
|
+
if (missingServices.guardianApp) availableServices.push('Guardian App');
|
|
296
|
+
if (missingServices.facialWeb) availableServices.push('Facial Web');
|
|
297
|
+
if (missingServices.posApp) availableServices.push('POS App');
|
|
276
298
|
|
|
277
|
-
task.
|
|
299
|
+
task.title = formatStepTitle(currentStep, totalSteps, `Found new services: ${availableServices.join(', ')}`);
|
|
278
300
|
ctx.missingServices = missingServices;
|
|
279
301
|
}
|
|
280
302
|
},
|
|
281
303
|
{
|
|
282
|
-
title:
|
|
283
|
-
skip: (
|
|
284
|
-
task: async (ctx, task) => {
|
|
285
|
-
if (options.force) {
|
|
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
|
-
}
|
|
295
|
-
|
|
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,
|
|
304
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Installing new services'),
|
|
305
|
+
skip: () => !servicesToInstall.gateApp && !servicesToInstall.guardianApp && !servicesToInstall.facialWeb && !servicesToInstall.posApp ? (currentStep--, 'No new services to install') : false,
|
|
322
306
|
task: async (ctx, task) => {
|
|
323
307
|
const servicesAdded = [];
|
|
324
308
|
if (servicesToInstall.gateApp) servicesAdded.push('Gate App');
|
|
@@ -326,37 +310,35 @@ export async function update(options) {
|
|
|
326
310
|
if (servicesToInstall.facialWeb) servicesAdded.push('Facial Web');
|
|
327
311
|
if (servicesToInstall.posApp) servicesAdded.push('POS App');
|
|
328
312
|
|
|
329
|
-
task.output = `Adding ${servicesAdded.join(', ')} to configuration...`;
|
|
330
|
-
|
|
331
313
|
// Update docker-compose.yml with new services
|
|
332
314
|
updateDockerCompose(composeFile, envFile, servicesToInstall);
|
|
333
315
|
|
|
334
316
|
// Update .env file with new app configuration
|
|
335
317
|
updateEnvFile(envFile, servicesToInstall);
|
|
336
318
|
|
|
337
|
-
task.
|
|
319
|
+
task.title = formatStepTitle(currentStep, totalSteps, `Installed: ${servicesAdded.join(', ')}`);
|
|
338
320
|
}
|
|
339
321
|
},
|
|
340
322
|
{
|
|
341
|
-
title: 'Pulling latest images',
|
|
323
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Pulling latest Docker images'),
|
|
342
324
|
task: async () => {
|
|
343
|
-
await
|
|
325
|
+
await pullImagesSilent(composeFile);
|
|
344
326
|
}
|
|
345
327
|
},
|
|
346
328
|
{
|
|
347
|
-
title: 'Stopping services',
|
|
329
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Stopping services'),
|
|
348
330
|
task: async () => {
|
|
349
|
-
await
|
|
331
|
+
await stopServicesSilent(composeFile);
|
|
350
332
|
}
|
|
351
333
|
},
|
|
352
334
|
{
|
|
353
|
-
title: 'Starting with new images',
|
|
335
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Starting with new images'),
|
|
354
336
|
task: async () => {
|
|
355
|
-
await
|
|
337
|
+
await startServicesSilent(composeFile);
|
|
356
338
|
}
|
|
357
339
|
},
|
|
358
340
|
{
|
|
359
|
-
title: 'Waiting for backend to be healthy',
|
|
341
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for backend to be healthy'),
|
|
360
342
|
task: async () => {
|
|
361
343
|
const healthy = await waitForServiceHealthy(composeFile, 'backend', 120);
|
|
362
344
|
if (!healthy) {
|
|
@@ -365,8 +347,8 @@ export async function update(options) {
|
|
|
365
347
|
}
|
|
366
348
|
},
|
|
367
349
|
{
|
|
368
|
-
title: 'Waiting for Gate App to be healthy',
|
|
369
|
-
skip: () => !hasGateApp && !servicesToInstall.gateApp,
|
|
350
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for Gate App to be healthy'),
|
|
351
|
+
skip: () => !hasGateApp && !servicesToInstall.gateApp ? (currentStep--, 'Gate App not installed') : false,
|
|
370
352
|
task: async () => {
|
|
371
353
|
const healthy = await waitForServiceHealthy(composeFile, 'gate-app', 60);
|
|
372
354
|
if (!healthy) {
|
|
@@ -375,8 +357,8 @@ export async function update(options) {
|
|
|
375
357
|
}
|
|
376
358
|
},
|
|
377
359
|
{
|
|
378
|
-
title: 'Waiting for Guardian App to be healthy',
|
|
379
|
-
skip: () => !hasGuardianApp && !servicesToInstall.guardianApp,
|
|
360
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for Guardian App to be healthy'),
|
|
361
|
+
skip: () => !hasGuardianApp && !servicesToInstall.guardianApp ? (currentStep--, 'Guardian App not installed') : false,
|
|
380
362
|
task: async () => {
|
|
381
363
|
const healthy = await waitForServiceHealthy(composeFile, 'guardian-app', 60);
|
|
382
364
|
if (!healthy) {
|
|
@@ -385,8 +367,8 @@ export async function update(options) {
|
|
|
385
367
|
}
|
|
386
368
|
},
|
|
387
369
|
{
|
|
388
|
-
title: 'Waiting for Facial Web to be healthy',
|
|
389
|
-
skip: () => !hasFacialWeb && !servicesToInstall.facialWeb,
|
|
370
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for Facial Web to be healthy'),
|
|
371
|
+
skip: () => !hasFacialWeb && !servicesToInstall.facialWeb ? (currentStep--, 'Facial Web not installed') : false,
|
|
390
372
|
task: async () => {
|
|
391
373
|
const healthy = await waitForServiceHealthy(composeFile, 'facial-web', 60);
|
|
392
374
|
if (!healthy) {
|
|
@@ -395,8 +377,8 @@ export async function update(options) {
|
|
|
395
377
|
}
|
|
396
378
|
},
|
|
397
379
|
{
|
|
398
|
-
title: 'Waiting for POS App to be healthy',
|
|
399
|
-
skip: () => !hasPosApp && !servicesToInstall.posApp,
|
|
380
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Waiting for POS App to be healthy'),
|
|
381
|
+
skip: () => !hasPosApp && !servicesToInstall.posApp ? (currentStep--, 'POS App not installed') : false,
|
|
400
382
|
task: async () => {
|
|
401
383
|
const healthy = await waitForServiceHealthy(composeFile, 'pos-app', 60);
|
|
402
384
|
if (!healthy) {
|
|
@@ -405,7 +387,7 @@ export async function update(options) {
|
|
|
405
387
|
}
|
|
406
388
|
},
|
|
407
389
|
{
|
|
408
|
-
title: 'Running database migrations',
|
|
390
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Running database migrations'),
|
|
409
391
|
task: async (ctx, task) => {
|
|
410
392
|
const result = await runMigrations(composeFile);
|
|
411
393
|
|
|
@@ -413,27 +395,40 @@ export async function update(options) {
|
|
|
413
395
|
throw new Error(`Migration failed:\n${result.output}`);
|
|
414
396
|
}
|
|
415
397
|
|
|
416
|
-
//
|
|
417
|
-
if (result.output && result.output.
|
|
418
|
-
task.
|
|
398
|
+
// Update title if migrations were applied
|
|
399
|
+
if (result.output && result.output.includes('Applied')) {
|
|
400
|
+
task.title = formatStepTitle(currentStep, totalSteps, 'Database migrations applied');
|
|
401
|
+
} else {
|
|
402
|
+
task.title = formatStepTitle(currentStep, totalSteps, 'Database migrations (up to date)');
|
|
419
403
|
}
|
|
420
404
|
}
|
|
421
405
|
},
|
|
422
406
|
{
|
|
423
|
-
title: 'Cleaning up unused Docker resources',
|
|
424
|
-
skip: () => options.skipCleanup,
|
|
407
|
+
title: formatStepTitle(++currentStep, totalSteps, 'Cleaning up unused Docker resources'),
|
|
408
|
+
skip: () => options.skipCleanup ? (currentStep--, 'Cleanup skipped') : false,
|
|
425
409
|
task: async (ctx, task) => {
|
|
426
410
|
try {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
task.output = 'Removed unused Docker images and containers';
|
|
411
|
+
await pruneDockerSilent();
|
|
412
|
+
task.title = formatStepTitle(currentStep, totalSteps, 'Cleaned up unused Docker resources');
|
|
430
413
|
} catch (error) {
|
|
431
414
|
// Non-critical error - don't fail the update
|
|
432
|
-
task.skip(`Cleanup
|
|
415
|
+
task.skip(`Cleanup failed: ${error.message}`);
|
|
416
|
+
currentStep--;
|
|
433
417
|
}
|
|
434
418
|
}
|
|
435
419
|
}
|
|
436
|
-
]
|
|
420
|
+
], {
|
|
421
|
+
renderer: 'default',
|
|
422
|
+
rendererOptions: {
|
|
423
|
+
collapse: false,
|
|
424
|
+
showSubtasks: false,
|
|
425
|
+
clearOutput: false,
|
|
426
|
+
showTimer: false,
|
|
427
|
+
removeEmptyLines: true
|
|
428
|
+
},
|
|
429
|
+
concurrent: false,
|
|
430
|
+
exitOnError: true
|
|
431
|
+
});
|
|
437
432
|
|
|
438
433
|
await tasks.run();
|
|
439
434
|
|
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
|