ante-erp-cli 1.9.7 → 1.9.9
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/bin/ante-cli.js +37 -0
- package/package.json +14 -14
- package/src/commands/clone-mongodb.js +290 -0
- package/src/commands/database-ports.js +346 -0
- package/src/commands/install.js +18 -3
- package/src/utils/database-ports.js +274 -0
- package/src/utils/mongodb.js +463 -0
package/bin/ante-cli.js
CHANGED
|
@@ -32,6 +32,8 @@ import { doctor } from '../src/commands/doctor.js';
|
|
|
32
32
|
import { uninstall } from '../src/commands/uninstall.js';
|
|
33
33
|
import { migrate, seed, shell, optimize, reset as dbReset, info } from '../src/commands/database.js';
|
|
34
34
|
import { cloneDb } from '../src/commands/clone-db.js';
|
|
35
|
+
import { cloneMongodb } from '../src/commands/clone-mongodb.js';
|
|
36
|
+
import { exposeDbPorts, secureDbPorts, dbPortStatus } from '../src/commands/database-ports.js';
|
|
35
37
|
import { setDomain } from '../src/commands/set-domain.js';
|
|
36
38
|
import { sslEnable, sslStatus } from '../src/commands/ssl-enable.js';
|
|
37
39
|
import { regenerateCompose } from '../src/commands/regenerate-compose.js';
|
|
@@ -47,6 +49,10 @@ program
|
|
|
47
49
|
.option('--preset <type>', 'Installation preset (minimal, standard, enterprise)', 'standard')
|
|
48
50
|
.option('--no-interactive', 'Non-interactive mode with defaults')
|
|
49
51
|
.option('--skip-checks', 'Skip system requirements check')
|
|
52
|
+
.option('--with-facial', 'Install Facial Recognition Web app')
|
|
53
|
+
.option('--with-gate', 'Install Gate App')
|
|
54
|
+
.option('--with-guardian', 'Install Guardian App')
|
|
55
|
+
.option('--with-all-frontends', 'Install all frontend applications')
|
|
50
56
|
.action(install);
|
|
51
57
|
|
|
52
58
|
// Update & Maintenance
|
|
@@ -152,6 +158,37 @@ dbCmd
|
|
|
152
158
|
.option('--no-prisma', 'Skip Prisma operations')
|
|
153
159
|
.action(cloneDb);
|
|
154
160
|
|
|
161
|
+
dbCmd
|
|
162
|
+
.command('clone-mongodb <mongodb-url>')
|
|
163
|
+
.description('Clone remote MongoDB database to local')
|
|
164
|
+
.option('--skip-dump', 'Use existing backup directory')
|
|
165
|
+
.option('--backup-dir <path>', 'Path to existing backup directory')
|
|
166
|
+
.option('--force', 'Skip confirmation prompts')
|
|
167
|
+
.action(cloneMongodb);
|
|
168
|
+
|
|
169
|
+
dbCmd
|
|
170
|
+
.command('expose')
|
|
171
|
+
.description('Expose database ports for external access (DEVELOPMENT ONLY)')
|
|
172
|
+
.option('--postgres', 'Expose only PostgreSQL (5432)')
|
|
173
|
+
.option('--redis', 'Expose only Redis (6379)')
|
|
174
|
+
.option('--mongodb', 'Expose only MongoDB (27017)')
|
|
175
|
+
.option('--all', 'Expose all databases (default)', true)
|
|
176
|
+
.option('--force', 'Skip confirmation prompts')
|
|
177
|
+
.option('--no-restart', 'Don\'t restart services automatically')
|
|
178
|
+
.action(exposeDbPorts);
|
|
179
|
+
|
|
180
|
+
dbCmd
|
|
181
|
+
.command('secure')
|
|
182
|
+
.description('Remove database port bindings (secure databases)')
|
|
183
|
+
.option('--force', 'Skip confirmation prompts')
|
|
184
|
+
.option('--no-restart', 'Don\'t restart services automatically')
|
|
185
|
+
.action(secureDbPorts);
|
|
186
|
+
|
|
187
|
+
dbCmd
|
|
188
|
+
.command('port-status')
|
|
189
|
+
.description('Show database port exposure status')
|
|
190
|
+
.action(dbPortStatus);
|
|
191
|
+
|
|
155
192
|
// Status & Monitoring
|
|
156
193
|
program
|
|
157
194
|
.command('status')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ante-erp-cli",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.9",
|
|
4
4
|
"description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,25 +42,25 @@
|
|
|
42
42
|
"node": ">=24.0.0"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"commander": "^12.0.0",
|
|
46
|
-
"inquirer": "^9.2.0",
|
|
47
|
-
"chalk": "^5.3.0",
|
|
48
|
-
"ora": "^8.0.0",
|
|
49
45
|
"boxen": "^7.1.0",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
46
|
+
"chalk": "^5.3.0",
|
|
47
|
+
"cli-table3": "^0.6.3",
|
|
48
|
+
"commander": "^12.0.0",
|
|
52
49
|
"conf": "^12.0.0",
|
|
53
50
|
"dotenv": "^16.4.0",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
51
|
+
"execa": "^8.0.0",
|
|
52
|
+
"inquirer": "^9.2.0",
|
|
53
|
+
"js-yaml": "^4.1.1",
|
|
54
|
+
"listr2": "^8.0.0",
|
|
57
55
|
"node-fetch": "^3.3.0",
|
|
58
|
-
"
|
|
59
|
-
"prompts": "^2.4.2"
|
|
56
|
+
"ora": "^8.0.0",
|
|
57
|
+
"prompts": "^2.4.2",
|
|
58
|
+
"semver": "^7.6.0",
|
|
59
|
+
"update-notifier": "^7.0.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"eslint": "^8.57.0",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
63
|
+
"jest": "^29.7.0",
|
|
64
|
+
"prettier": "^3.2.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
6
|
+
import { stat } from 'fs/promises';
|
|
7
|
+
import { getInstallDir } from '../utils/config.js';
|
|
8
|
+
import {
|
|
9
|
+
parseMongoUrl,
|
|
10
|
+
buildMongoUrl,
|
|
11
|
+
testConnection,
|
|
12
|
+
dumpDatabase,
|
|
13
|
+
restoreDatabase,
|
|
14
|
+
verifyDataRestored
|
|
15
|
+
} from '../utils/mongodb.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build MONGODB_URI for installed ANTE instance
|
|
19
|
+
* @param {string} installDir - Installation directory
|
|
20
|
+
* @returns {string} MONGODB_URI for local MongoDB
|
|
21
|
+
*/
|
|
22
|
+
function readMongoDbUri(installDir) {
|
|
23
|
+
const envFile = join(installDir, '.env');
|
|
24
|
+
|
|
25
|
+
if (!existsSync(envFile)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'.env file not found. This command is for installed ANTE instances only.\n' +
|
|
28
|
+
'For development environments, use the appropriate script.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const envContent = readFileSync(envFile, 'utf-8');
|
|
33
|
+
|
|
34
|
+
// Try to find MONGODB_URI or MONGODB_URL
|
|
35
|
+
let match = envContent.match(/^MONGODB_URI=(.+)$/m);
|
|
36
|
+
if (!match) {
|
|
37
|
+
match = envContent.match(/^MONGODB_URL=(.+)$/m);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!match) {
|
|
41
|
+
throw new Error('MONGODB_URI or MONGODB_URL not found in .env file');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mongoUri = match[1].trim();
|
|
45
|
+
|
|
46
|
+
// Validate it looks like a MongoDB URI
|
|
47
|
+
if (!mongoUri.startsWith('mongodb://') && !mongoUri.startsWith('mongodb+srv://')) {
|
|
48
|
+
throw new Error('Invalid MongoDB URI format in .env file');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return mongoUri;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clone remote MongoDB database to local database
|
|
56
|
+
* @param {string} sourceUrl - Source MongoDB URL
|
|
57
|
+
* @param {Object} options - Command options
|
|
58
|
+
*/
|
|
59
|
+
export async function cloneMongodb(sourceUrl, options = {}) {
|
|
60
|
+
try {
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(chalk.blue('╔════════════════════════════════════════════════════════════╗'));
|
|
63
|
+
console.log(chalk.blue('║ ANTE CLI - Clone MongoDB Database ║'));
|
|
64
|
+
console.log(chalk.blue('╚════════════════════════════════════════════════════════════╝'));
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
// Get ANTE installation directory
|
|
68
|
+
const installDir = getInstallDir();
|
|
69
|
+
const backupDir = join(installDir, 'backups', 'mongodb');
|
|
70
|
+
const composeFile = join(installDir, 'docker-compose.yml');
|
|
71
|
+
|
|
72
|
+
// Step 1: Parse source database URL
|
|
73
|
+
console.log(chalk.yellow('Step 1/5: Parsing source database URL...'));
|
|
74
|
+
let sourceInfo;
|
|
75
|
+
try {
|
|
76
|
+
sourceInfo = parseMongoUrl(sourceUrl);
|
|
77
|
+
console.log(chalk.green('✓ Source URL parsed successfully'));
|
|
78
|
+
console.log(chalk.gray(` Host: ${sourceInfo.host}${sourceInfo.port ? ':' + sourceInfo.port : ' (SRV)'}`));
|
|
79
|
+
console.log(chalk.gray(` Database: ${sourceInfo.database}`));
|
|
80
|
+
console.log(chalk.gray(` User: ${sourceInfo.user || '(none)'}`));
|
|
81
|
+
if (sourceInfo.authDatabase && sourceInfo.authDatabase !== sourceInfo.database) {
|
|
82
|
+
console.log(chalk.gray(` Auth Database: ${sourceInfo.authDatabase}`));
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Failed to parse source URL: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 2: Parse local database URL
|
|
90
|
+
console.log(chalk.yellow('Step 2/5: Reading local database configuration...'));
|
|
91
|
+
const localMongoUri = readMongoDbUri(installDir);
|
|
92
|
+
if (!localMongoUri) {
|
|
93
|
+
throw new Error('.env file not found or MONGODB_URI not set');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let localInfo;
|
|
97
|
+
try {
|
|
98
|
+
localInfo = parseMongoUrl(localMongoUri);
|
|
99
|
+
console.log(chalk.green('✓ Local database configuration loaded'));
|
|
100
|
+
console.log(chalk.gray(` Host: ${localInfo.host}${localInfo.port ? ':' + localInfo.port : ' (SRV)'}`));
|
|
101
|
+
console.log(chalk.gray(` Database: ${localInfo.database}`));
|
|
102
|
+
console.log(chalk.gray(` User: ${localInfo.user || '(none)'}`));
|
|
103
|
+
console.log('');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new Error(`Failed to parse local MONGODB_URI: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Step 3: Test connections
|
|
109
|
+
console.log(chalk.yellow('Step 3/5: Testing database connections...'));
|
|
110
|
+
|
|
111
|
+
// Test source connection
|
|
112
|
+
const sourceSpinner = ora('Testing source database connection...').start();
|
|
113
|
+
const sourceTest = await testConnection(sourceInfo);
|
|
114
|
+
if (!sourceTest.success) {
|
|
115
|
+
sourceSpinner.fail(chalk.red('Source database connection failed'));
|
|
116
|
+
throw new Error(`Cannot connect to source database: ${sourceTest.error}`);
|
|
117
|
+
}
|
|
118
|
+
sourceSpinner.succeed(chalk.green('Source database connection successful'));
|
|
119
|
+
|
|
120
|
+
// Test local connection (use Docker Compose for installed ANTE)
|
|
121
|
+
const localSpinner = ora('Testing local database connection...').start();
|
|
122
|
+
const localTest = await testConnection(localInfo, composeFile);
|
|
123
|
+
if (!localTest.success) {
|
|
124
|
+
localSpinner.fail(chalk.red('Local database connection failed'));
|
|
125
|
+
throw new Error(`Cannot connect to local database: ${localTest.error}`);
|
|
126
|
+
}
|
|
127
|
+
localSpinner.succeed(chalk.green('Local database connection successful'));
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
// Step 4: Confirmation
|
|
131
|
+
if (!options.force) {
|
|
132
|
+
console.log(chalk.yellow('⚠️ WARNING: This will DROP and REPLACE your local database collections!'));
|
|
133
|
+
console.log(chalk.yellow('⚠️ All existing local data will be LOST!'));
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(chalk.blue('Configuration Summary:'));
|
|
136
|
+
console.log(chalk.gray('┌─ Source Database:'));
|
|
137
|
+
console.log(chalk.gray(`│ Host: ${sourceInfo.host}${sourceInfo.port ? ':' + sourceInfo.port : ' (SRV)'}`));
|
|
138
|
+
console.log(chalk.gray(`│ Database: ${sourceInfo.database}`));
|
|
139
|
+
console.log(chalk.gray('│'));
|
|
140
|
+
console.log(chalk.gray('└─ Target Database (Local):'));
|
|
141
|
+
console.log(chalk.gray(` Host: ${localInfo.host}${localInfo.port ? ':' + localInfo.port : ' (SRV)'}`));
|
|
142
|
+
console.log(chalk.gray(` Database: ${localInfo.database}`));
|
|
143
|
+
console.log('');
|
|
144
|
+
|
|
145
|
+
const { confirm } = await inquirer.prompt([
|
|
146
|
+
{
|
|
147
|
+
type: 'confirm',
|
|
148
|
+
name: 'confirm',
|
|
149
|
+
message: 'Do you want to continue?',
|
|
150
|
+
default: false
|
|
151
|
+
}
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
if (!confirm) {
|
|
155
|
+
console.log(chalk.gray('\nClone cancelled by user.'));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Step 5: Dump source database (if not skipped)
|
|
162
|
+
let dumpDirectory;
|
|
163
|
+
if (options.skipDump && options.backupDir) {
|
|
164
|
+
console.log(chalk.yellow('Step 4/5: Using existing backup directory...'));
|
|
165
|
+
dumpDirectory = options.backupDir;
|
|
166
|
+
|
|
167
|
+
if (!existsSync(dumpDirectory)) {
|
|
168
|
+
throw new Error(`Backup directory not found: ${dumpDirectory}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Verify backup contains database folder with BSON files
|
|
172
|
+
const dbDumpDir = join(dumpDirectory, sourceInfo.database);
|
|
173
|
+
if (!existsSync(dbDumpDir)) {
|
|
174
|
+
throw new Error(`Database dump folder not found: ${dbDumpDir}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const files = readdirSync(dbDumpDir);
|
|
178
|
+
const bsonFiles = files.filter(f => f.endsWith('.bson'));
|
|
179
|
+
|
|
180
|
+
if (bsonFiles.length === 0) {
|
|
181
|
+
throw new Error(`No BSON files found in backup directory: ${dbDumpDir}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate total size
|
|
185
|
+
let totalSize = 0;
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const filePath = join(dbDumpDir, file);
|
|
188
|
+
const stats = await stat(filePath);
|
|
189
|
+
totalSize += stats.size;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const sizeInMB = (totalSize / (1024 * 1024)).toFixed(2);
|
|
193
|
+
const size = sizeInMB >= 1 ? `${sizeInMB} MB` : `${(totalSize / 1024).toFixed(2)} KB`;
|
|
194
|
+
|
|
195
|
+
console.log(chalk.green('✓ Using existing backup'));
|
|
196
|
+
console.log(chalk.gray(` Directory: ${dumpDirectory}`));
|
|
197
|
+
console.log(chalk.gray(` Collections: ${bsonFiles.length}`));
|
|
198
|
+
console.log(chalk.gray(` Size: ${size}`));
|
|
199
|
+
console.log('');
|
|
200
|
+
} else {
|
|
201
|
+
console.log(chalk.yellow('Step 4/5: Dumping source database...'));
|
|
202
|
+
console.log(chalk.gray('This may take several minutes depending on database size...'));
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
|
|
206
|
+
dumpDirectory = join(backupDir, `clone-backup-${timestamp}`);
|
|
207
|
+
|
|
208
|
+
const dumpSpinner = ora('Creating database dump...').start();
|
|
209
|
+
const dumpResult = await dumpDatabase(sourceInfo, dumpDirectory);
|
|
210
|
+
|
|
211
|
+
if (!dumpResult.success) {
|
|
212
|
+
dumpSpinner.fail(chalk.red('Database dump failed'));
|
|
213
|
+
throw new Error(dumpResult.error);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
dumpSpinner.succeed(chalk.green('Database dump completed'));
|
|
217
|
+
console.log(chalk.gray(` Directory: ${dumpResult.dir}`));
|
|
218
|
+
console.log(chalk.gray(` Collections: ${dumpResult.collections}`));
|
|
219
|
+
console.log(chalk.gray(` Size: ${dumpResult.size}`));
|
|
220
|
+
console.log('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Step 6: Restore to local database
|
|
224
|
+
console.log(chalk.yellow('Step 5/5: Restoring to local database...'));
|
|
225
|
+
console.log(chalk.gray('This may take several minutes...'));
|
|
226
|
+
console.log('');
|
|
227
|
+
|
|
228
|
+
const restoreSpinner = ora('Restoring database (collections will be dropped and recreated)...').start();
|
|
229
|
+
const restoreResult = await restoreDatabase(dumpDirectory, localInfo, true, composeFile);
|
|
230
|
+
|
|
231
|
+
if (!restoreResult.success) {
|
|
232
|
+
restoreSpinner.fail(chalk.red('Database restore failed'));
|
|
233
|
+
throw new Error(restoreResult.error);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
restoreSpinner.succeed(chalk.green('Database restore completed'));
|
|
237
|
+
console.log('');
|
|
238
|
+
|
|
239
|
+
// Step 6A: Verify data was restored
|
|
240
|
+
const verifySpinner = ora('Verifying data restoration...').start();
|
|
241
|
+
const verifyResult = await verifyDataRestored(localInfo, composeFile);
|
|
242
|
+
|
|
243
|
+
if (!verifyResult.success) {
|
|
244
|
+
verifySpinner.fail(chalk.red('Data verification failed'));
|
|
245
|
+
console.log(chalk.yellow(' Warning: Could not verify data restoration'));
|
|
246
|
+
} else {
|
|
247
|
+
verifySpinner.succeed(chalk.green(`Verified: ${verifyResult.collectionCount} collections, ${verifyResult.totalDocuments.toLocaleString()} documents restored`));
|
|
248
|
+
|
|
249
|
+
// Warn if no data was restored
|
|
250
|
+
if (verifyResult.totalDocuments === 0) {
|
|
251
|
+
console.log(chalk.red('\n⚠️ WARNING: No documents were restored!'));
|
|
252
|
+
console.log(chalk.yellow(' Possible causes:'));
|
|
253
|
+
console.log(chalk.gray(' - Source database was empty'));
|
|
254
|
+
console.log(chalk.gray(' - Authentication issues prevented data restore'));
|
|
255
|
+
console.log(chalk.gray(' - Permission issues with target database'));
|
|
256
|
+
console.log(chalk.gray(' - Data restore errors were silently ignored\n'));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
console.log('');
|
|
260
|
+
|
|
261
|
+
// Final summary
|
|
262
|
+
console.log(chalk.green('╔════════════════════════════════════════════════════════════╗'));
|
|
263
|
+
console.log(chalk.green('║ Database Clone Completed Successfully! ║'));
|
|
264
|
+
console.log(chalk.green('╚════════════════════════════════════════════════════════════╝'));
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(chalk.blue('Summary:'));
|
|
267
|
+
console.log(chalk.gray(` ✓ Source database dumped to: ${dumpDirectory}`));
|
|
268
|
+
console.log(chalk.gray(` ✓ Restored to local database: ${localInfo.database}`));
|
|
269
|
+
|
|
270
|
+
// Show data restoration summary
|
|
271
|
+
if (verifyResult && verifyResult.success) {
|
|
272
|
+
if (verifyResult.totalDocuments > 0) {
|
|
273
|
+
console.log(chalk.green(` ✓ Data restored: ${verifyResult.totalDocuments.toLocaleString()} documents across ${verifyResult.collectionCount} collections`));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(chalk.red(` ✗ No data restored (${verifyResult.collectionCount} collections created but empty)`));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log(chalk.cyan('Your local MongoDB database is now cloned from the source!'));
|
|
281
|
+
console.log(chalk.gray('You can start your services with: ante start'));
|
|
282
|
+
console.log('');
|
|
283
|
+
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.log('');
|
|
286
|
+
console.error(chalk.red('✗ MongoDB clone failed:'), error.message);
|
|
287
|
+
console.log('');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
}
|