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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.11.12",
3
+ "version": "1.11.13",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,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
- // 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
  }
233
-
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
- ]);
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.skip('All available services are already installed');
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 (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)');
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.output = `Found ${availableServices.length} new service(s): ${availableServices.join(', ')}`;
299
+ task.title = formatStepTitle(currentStep, totalSteps, `Found new services: ${availableServices.join(', ')}`);
278
300
  ctx.missingServices = missingServices;
279
301
  }
280
302
  },
281
303
  {
282
- title: 'Confirming installation of new services',
283
- skip: (ctx) => !ctx.missingServices || (!ctx.missingServices.gateApp && !ctx.missingServices.guardianApp && !ctx.missingServices.facialWeb && !ctx.missingServices.posApp),
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.output = `Successfully added ${servicesAdded.join(', ')}`;
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 pullImages(composeFile);
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 stopServices(composeFile);
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 startServices(composeFile);
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
- // Show migration output if there were migrations run
417
- if (result.output && result.output.trim()) {
418
- task.output = result.output;
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
- // Run Docker cleanup to remove dangling images
428
- await pruneDocker();
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 skipped: ${error.message}`);
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
 
@@ -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