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 +1 -1
- package/src/commands/backup.js +21 -1
- package/src/commands/clone-db.js +8 -2
- package/src/commands/restore.js +27 -14
- package/src/utils/docker.js +38 -3
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -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 =
|
|
224
|
+
spinner.text = `Creating compressed archive (${totalSize})...`;
|
|
205
225
|
await execa('mkdir', ['-p', backupDir]);
|
|
206
226
|
await execa('tar', [
|
|
207
227
|
'-czf',
|
package/src/commands/clone-db.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/restore.js
CHANGED
|
@@ -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
|
|
107
|
-
|
|
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
|
|
128
|
-
|
|
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
|
-
['
|
|
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',
|
package/src/utils/docker.js
CHANGED
|
@@ -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
|