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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.11.12",
3
+ "version": "1.11.14",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.succeed(chalk.green('Database backup created successfully!'));
237
+ if (spinner) {
238
+ spinner.succeed(chalk.green('Database backup created successfully!'));
239
+ }
238
240
 
239
- // Show backup details
240
- const { stdout: size } = await execa('du', ['-h', backupFile]);
241
- console.log(chalk.white('\n✓ PostgreSQL database backed up'));
242
- console.log(chalk.white(`✓ MongoDB database backed up (${mongoResult.collections} collections, ${mongoResult.size})`));
243
- console.log(chalk.cyan(`\nBackup file: ${backupFile}`));
244
- console.log(chalk.gray(`Total size: ${size.split('\t')[0]}\n`));
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.fail('Backup failed');
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
  }
@@ -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 { pullImages, stopServices, startServices, waitForServiceHealthy, runMigrations, pruneDocker } from '../utils/docker.js';
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
- // Confirmation
225
- if (!options.force) {
226
- // Check if running in non-interactive mode (no TTY)
227
- if (!process.stdin.isTTY) {
228
- console.error(chalk.yellow('\n⚠️ Warning: Running in non-interactive mode (no TTY detected)'));
229
- console.error(chalk.gray('Use --force flag to skip confirmation prompts\n'));
230
- console.error(chalk.cyan('Example: ante update --force\n'));
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
- const { confirm } = await inquirer.prompt([
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
- if (!confirm) {
244
- console.log(chalk.gray('\nUpdate cancelled.\n'));
245
- return;
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({ output: join(installDir, 'backups', `pre-update-${timestamp}.tar.gz`) });
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 missingServices = detectMissingServices(composeFile);
290
+ const stepNumber = currentStep; // Capture current step number
265
291
 
266
- if (!missingServices.gateApp && !missingServices.guardianApp && !missingServices.facialWeb && !missingServices.posApp) {
267
- task.skip('All available services are already installed');
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 (School attendance)');
273
- if (missingServices.guardianApp) availableServices.push('Guardian App (Parent portal)');
274
- if (missingServices.facialWeb) availableServices.push('Facial Web (Face recognition)');
275
- if (missingServices.posApp) availableServices.push('POS App (Point of Sale)');
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.output = `Found ${availableServices.length} new service(s): ${availableServices.join(', ')}`;
307
+ task.title = formatStepTitle(stepNumber, totalSteps, `Found new services: ${availableServices.join(', ')}`);
278
308
  ctx.missingServices = missingServices;
279
309
  }
280
310
  },
281
311
  {
282
- title: 'Confirming installation of new services',
283
- skip: (ctx) => !ctx.missingServices || (!ctx.missingServices.gateApp && !ctx.missingServices.guardianApp && !ctx.missingServices.facialWeb && !ctx.missingServices.posApp),
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
- 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
- }
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.output = `Successfully added ${servicesAdded.join(', ')}`;
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 pullImages(composeFile);
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 stopServices(composeFile);
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 startServices(composeFile);
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
- // Show migration output if there were migrations run
417
- if (result.output && result.output.trim()) {
418
- task.output = result.output;
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
- // Run Docker cleanup to remove dangling images
428
- await pruneDocker();
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 skipped: ${error.message}`);
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
 
@@ -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