ante-erp-cli 1.10.3 → 1.10.4

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.10.3",
3
+ "version": "1.10.4",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -139,7 +139,18 @@ export async function backup(options) {
139
139
  'ante_db'
140
140
  ]);
141
141
 
142
+ // Get PostgreSQL dump size for progress indication
143
+ const { stdout: pgSizeOutput } = await execa('docker', [
144
+ 'exec',
145
+ 'ante-postgres',
146
+ 'sh',
147
+ '-c',
148
+ 'du -h /tmp/postgres.dump | cut -f1'
149
+ ]);
150
+ const pgSize = pgSizeOutput.trim();
151
+
142
152
  // Copy dump file from container to temp directory
153
+ spinner.text = `Copying PostgreSQL dump from container (${pgSize})...`;
143
154
  await execa('docker', [
144
155
  'cp',
145
156
  'ante-postgres:/tmp/postgres.dump',
@@ -191,7 +202,12 @@ export async function backup(options) {
191
202
  throw new Error(`MongoDB backup failed: ${mongoResult.error}`);
192
203
  }
193
204
 
205
+ // Get MongoDB dump size for progress indication
206
+ const { stdout: mongoSizeOutput } = await execa('du', ['-sh', join(mongoDumpTempDir, mongoInfo.database)]);
207
+ const mongoSize = mongoSizeOutput.split('\t')[0];
208
+
194
209
  // Move MongoDB dump to final structure (mongodb/ folder)
210
+ spinner.text = `Organizing MongoDB dump (${mongoSize})...`;
195
211
  await execa('mv', [
196
212
  join(mongoDumpTempDir, mongoInfo.database),
197
213
  join(tempDir, 'mongodb')
@@ -200,8 +216,12 @@ export async function backup(options) {
200
216
  // Remove temporary MongoDB dump directory
201
217
  await execa('rm', ['-rf', mongoDumpTempDir]);
202
218
 
219
+ // Get total size before compression
220
+ const { stdout: totalSizeOutput } = await execa('du', ['-sh', tempDir]);
221
+ const totalSize = totalSizeOutput.split('\t')[0];
222
+
203
223
  // Create tar.gz archive
204
- spinner.text = 'Creating compressed archive...';
224
+ spinner.text = `Creating compressed archive (${totalSize})...`;
205
225
  await execa('mkdir', ['-p', backupDir]);
206
226
  await execa('tar', [
207
227
  '-czf',
@@ -261,32 +261,38 @@ export async function cloneDb(sourceUrl, options = {}) {
261
261
 
262
262
  // Generate Prisma client
263
263
  const generateSpinner = ora('Generating Prisma client...').start();
264
+ const generateStartTime = Date.now();
264
265
  try {
265
266
  await execInContainer(
266
267
  composeFile,
267
268
  'backend',
268
269
  ['npx', 'prisma', 'generate']
269
270
  );
270
- generateSpinner.succeed(chalk.green('Prisma client generated'));
271
+ const generateDuration = ((Date.now() - generateStartTime) / 1000).toFixed(1);
272
+ generateSpinner.succeed(chalk.green(`Prisma client generated (${generateDuration}s)`));
271
273
  generateSuccess = true;
272
274
  } catch (error) {
273
275
  generateSpinner.fail(chalk.red('Prisma generate failed'));
274
276
  console.log(chalk.yellow(' Warning: You may need to run "npx prisma generate" manually in the backend container'));
277
+ console.log(chalk.gray(` Error: ${error.message}`));
275
278
  }
276
279
 
277
280
  // Run Prisma migrations
278
281
  const migrateSpinner = ora('Running Prisma migrations...').start();
282
+ const migrateStartTime = Date.now();
279
283
  try {
280
284
  await execInContainer(
281
285
  composeFile,
282
286
  'backend',
283
287
  ['npx', 'prisma', 'migrate', 'deploy']
284
288
  );
285
- migrateSpinner.succeed(chalk.green('Prisma migrations completed'));
289
+ const migrateDuration = ((Date.now() - migrateStartTime) / 1000).toFixed(1);
290
+ migrateSpinner.succeed(chalk.green(`Prisma migrations completed (${migrateDuration}s)`));
286
291
  migrateSuccess = true;
287
292
  } catch (error) {
288
293
  migrateSpinner.fail(chalk.red('Prisma migrate failed'));
289
294
  console.log(chalk.yellow(' Note: Database schema already exists from restore. Migrations may not be needed.'));
295
+ console.log(chalk.gray(` Error: ${error.message}`));
290
296
  }
291
297
  console.log('');
292
298
  }
@@ -2,10 +2,10 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import inquirer from 'inquirer';
4
4
  import { join, basename } from 'path';
5
- import { existsSync, mkdirSync, readdirSync } from 'fs';
5
+ import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
6
6
  import { execa } from 'execa';
7
7
  import { getInstallDir } from '../utils/config.js';
8
- import { execInContainer, stopServices, startServices } from '../utils/docker.js';
8
+ import { execInContainer, stopServices, startServices, waitForHealthy } from '../utils/docker.js';
9
9
 
10
10
  /**
11
11
  * Restore ANTE ERP databases from backup (PostgreSQL and MongoDB only)
@@ -96,21 +96,31 @@ export async function restore(backupFile, options = {}) {
96
96
  spinner.text = 'Stopping services...';
97
97
  await stopServices(composeFile);
98
98
 
99
- // Wait for services to stop
100
- await new Promise(resolve => setTimeout(resolve, 3000));
101
-
102
99
  // Start only database services for restore
103
100
  spinner.text = 'Starting database services...';
104
101
  await startServices(composeFile, ['postgres', 'mongodb', 'redis']);
105
102
 
106
- // Wait for databases to be ready
107
- await new Promise(resolve => setTimeout(resolve, 5000));
103
+ // Wait for databases to be healthy (replaces fixed waits)
104
+ spinner.text = 'Waiting for databases to be ready...';
105
+ await waitForHealthy(composeFile, ['postgres', 'mongodb', 'redis'], 60);
108
106
 
109
107
  // Restore PostgreSQL
110
- spinner.text = 'Restoring PostgreSQL database...';
111
108
  const pgDumpFile = join(tempDir, 'postgres.dump');
112
109
  if (existsSync(pgDumpFile)) {
110
+ // Get dump size for progress indication
111
+ const stats = statSync(pgDumpFile);
112
+ const sizeInMB = Math.round(stats.size / (1024 * 1024));
113
+
114
+ // Copy dump file to container (file-based restore, zero memory usage)
115
+ spinner.text = `Copying PostgreSQL dump to container (${sizeInMB}MB)...`;
116
+ await execa('docker', [
117
+ 'cp',
118
+ pgDumpFile,
119
+ 'ante-postgres:/tmp/postgres.dump'
120
+ ]);
121
+
113
122
  // Drop database (connect to postgres database to avoid connection errors)
123
+ spinner.text = 'Preparing PostgreSQL database...';
114
124
  await execInContainer(
115
125
  composeFile,
116
126
  'postgres',
@@ -124,20 +134,24 @@ export async function restore(backupFile, options = {}) {
124
134
  ['psql', '-U', 'ante', '-d', 'postgres', '-c', 'CREATE DATABASE ante_db;']
125
135
  );
126
136
 
127
- // Restore from dump (connect to ante_db)
128
- const dumpContent = await execa('cat', [pgDumpFile]);
137
+ // Restore from file inside container (streaming, zero memory)
138
+ spinner.text = `Restoring PostgreSQL database (${sizeInMB}MB)...`;
129
139
  await execInContainer(
130
140
  composeFile,
131
141
  'postgres',
132
- ['pg_restore', '-U', 'ante', '-d', 'ante_db'],
133
- { input: dumpContent.stdout }
142
+ ['sh', '-c', 'pg_restore -U ante -d ante_db /tmp/postgres.dump && rm /tmp/postgres.dump']
134
143
  );
135
144
  }
136
145
 
137
146
  // Restore MongoDB
138
- spinner.text = 'Restoring MongoDB database...';
139
147
  const mongoDumpDir = join(tempDir, 'mongodb');
140
148
  if (existsSync(mongoDumpDir)) {
149
+ // Get folder size for progress indication
150
+ const { stdout: mongoSize } = await execa('du', ['-sh', mongoDumpDir]);
151
+ const sizeStr = mongoSize.split('\t')[0];
152
+
153
+ spinner.text = `Restoring MongoDB database (${sizeStr})...`;
154
+
141
155
  // Copy MongoDB dump directory to container
142
156
  // The backup structure is: mongodb/ contains the database files directly
143
157
  await execa('docker', [
@@ -147,7 +161,6 @@ export async function restore(backupFile, options = {}) {
147
161
  ]);
148
162
 
149
163
  // Restore from the mongodb directory (contains BSON files)
150
- // Use --nsInclude to restore all collections from the dump
151
164
  await execInContainer(
152
165
  composeFile,
153
166
  'mongodb',
@@ -182,18 +182,53 @@ export async function isServiceHealthy(composeFile, service) {
182
182
  */
183
183
  export async function waitForServiceHealthy(composeFile, service, timeout = 120) {
184
184
  const startTime = Date.now();
185
-
185
+
186
186
  while (Date.now() - startTime < timeout * 1000) {
187
187
  if (await isServiceHealthy(composeFile, service)) {
188
188
  return true;
189
189
  }
190
-
190
+
191
191
  await new Promise(resolve => setTimeout(resolve, 2000));
192
192
  }
193
-
193
+
194
194
  return false;
195
195
  }
196
196
 
197
+ /**
198
+ * Wait for multiple services to be healthy
199
+ * @param {string} composeFile - Path to docker-compose.yml
200
+ * @param {string[]} services - Array of service names
201
+ * @param {number} timeout - Timeout in seconds (default: 60)
202
+ * @returns {Promise<boolean>} True if all services are healthy
203
+ */
204
+ export async function waitForHealthy(composeFile, services, timeout = 60) {
205
+ const endTime = Date.now() + (timeout * 1000);
206
+
207
+ while (Date.now() < endTime) {
208
+ try {
209
+ const serviceStatuses = await getServiceStatus(composeFile);
210
+
211
+ const allHealthy = services.every(serviceName => {
212
+ const service = serviceStatuses.find(s => s.Service === serviceName);
213
+ if (!service) return false;
214
+
215
+ // Service is healthy if: running AND (healthy OR no healthcheck)
216
+ return service.State === 'running' &&
217
+ (service.Health === 'healthy' || service.Health === undefined || service.Health === '');
218
+ });
219
+
220
+ if (allHealthy) return true;
221
+
222
+ } catch (error) {
223
+ // Continue waiting
224
+ }
225
+
226
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Check every 2s
227
+ }
228
+
229
+ throw new Error(`Services failed to become healthy within ${timeout}s: ${services.join(', ')}`);
230
+ }
231
+
197
232
  /**
198
233
  * Down all services and optionally remove volumes
199
234
  * @param {string} composeFile - Path to docker-compose.yml