ante-erp-cli 1.0.2 ā 1.1.0
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/README.md +24 -6
- package/bin/ante-cli.js +81 -3
- package/bin/ante-cli.js.bak +198 -0
- package/package.json +1 -1
- package/src/commands/backup-enhancements.js +233 -0
- package/src/commands/backup.js +2 -1
- package/src/commands/config.js +304 -0
- package/src/commands/logs.js +196 -7
- package/src/commands/logs.js.bak +33 -0
- package/src/templates/docker-compose.yml.js +1 -1
- package/src/templates/env.js +1 -0
- package/src/utils/validation.js +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,10 @@ ante status # Show service status
|
|
|
72
72
|
ante health # Run health checks (alias: ante doctor)
|
|
73
73
|
ante logs # View logs
|
|
74
74
|
ante logs --service backend --follow
|
|
75
|
+
ante logs export # Export logs to file
|
|
76
|
+
ante logs errors # Show only error logs
|
|
77
|
+
ante logs search <term> # Search logs for keyword
|
|
78
|
+
ante logs clean # Clean old logs
|
|
75
79
|
ante ps # Show running containers
|
|
76
80
|
```
|
|
77
81
|
|
|
@@ -89,7 +93,10 @@ ante update --version 2.0.0
|
|
|
89
93
|
ante backup # Create backup
|
|
90
94
|
ante dump # Alias for backup
|
|
91
95
|
ante backup -o /backups/my-backup.tar.gz
|
|
92
|
-
ante backup
|
|
96
|
+
ante backup list # List available backups
|
|
97
|
+
ante backup verify # Verify backup integrity
|
|
98
|
+
ante backup clean # Clean old backups
|
|
99
|
+
ante backup size # Show backup storage usage
|
|
93
100
|
ante restore <file> # Restore from backup
|
|
94
101
|
```
|
|
95
102
|
|
|
@@ -105,11 +112,22 @@ ante restart # Restart all services
|
|
|
105
112
|
### šļø Database Operations
|
|
106
113
|
|
|
107
114
|
```bash
|
|
108
|
-
ante db
|
|
109
|
-
ante db
|
|
110
|
-
ante db
|
|
111
|
-
ante db
|
|
112
|
-
ante db
|
|
115
|
+
ante db migrate # Run database migrations
|
|
116
|
+
ante db seed # Seed database
|
|
117
|
+
ante db shell # Open PostgreSQL shell
|
|
118
|
+
ante db optimize # Optimize database
|
|
119
|
+
ante db reset # Reset database (ā ļø destructive)
|
|
120
|
+
ante db info # Show database information
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### āļø Configuration Management
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
ante config show # Display configuration (masks secrets)
|
|
127
|
+
ante config edit # Edit configuration file (auto-backup)
|
|
128
|
+
ante config validate # Validate configuration
|
|
129
|
+
ante config get <key> # Get specific configuration value
|
|
130
|
+
ante config set <key=value> # Set configuration value (auto-backup)
|
|
113
131
|
```
|
|
114
132
|
|
|
115
133
|
### š SSL & Security
|
package/bin/ante-cli.js
CHANGED
|
@@ -24,12 +24,14 @@ import { status } from '../src/commands/status.js';
|
|
|
24
24
|
import { update } from '../src/commands/update.js';
|
|
25
25
|
import { updateCli } from '../src/commands/update-cli.js';
|
|
26
26
|
import { backup, listBackups } from '../src/commands/backup.js';
|
|
27
|
+
import { verifyBackup, cleanBackups, backupSize } from '../src/commands/backup-enhancements.js';
|
|
27
28
|
import { restore } from '../src/commands/restore.js';
|
|
28
29
|
import { start, stop, restart } from '../src/commands/service.js';
|
|
29
|
-
import { logs } from '../src/commands/logs.js';
|
|
30
|
+
import { logs, exportLogs, showErrors, cleanLogs, searchLogs } from '../src/commands/logs.js';
|
|
30
31
|
import { doctor } from '../src/commands/doctor.js';
|
|
31
32
|
import { uninstall } from '../src/commands/uninstall.js';
|
|
32
33
|
import { migrate, seed, shell, optimize, reset, info } from '../src/commands/database.js';
|
|
34
|
+
import { showConfig, editConfig, validateConfig, getConfigValue, setConfigValue } from '../src/commands/config.js';
|
|
33
35
|
|
|
34
36
|
// Installation & Setup
|
|
35
37
|
program
|
|
@@ -80,6 +82,23 @@ backupCmd
|
|
|
80
82
|
.description('List available backups')
|
|
81
83
|
.action(listBackups);
|
|
82
84
|
|
|
85
|
+
backupCmd
|
|
86
|
+
.command('verify [file]')
|
|
87
|
+
.description('Verify backup integrity')
|
|
88
|
+
.action(verifyBackup);
|
|
89
|
+
|
|
90
|
+
backupCmd
|
|
91
|
+
.command('clean')
|
|
92
|
+
.description('Clean old backups')
|
|
93
|
+
.option('-d, --days <days>', 'Delete backups older than N days', '30')
|
|
94
|
+
.option('--force', 'Skip confirmation')
|
|
95
|
+
.action(cleanBackups);
|
|
96
|
+
|
|
97
|
+
backupCmd
|
|
98
|
+
.command('size')
|
|
99
|
+
.description('Show backup storage usage')
|
|
100
|
+
.action(backupSize);
|
|
101
|
+
|
|
83
102
|
program
|
|
84
103
|
.command('restore [file]')
|
|
85
104
|
.description('Restore ANTE from backup')
|
|
@@ -122,6 +141,36 @@ dbCmd
|
|
|
122
141
|
.description('Show database information')
|
|
123
142
|
.action(info);
|
|
124
143
|
|
|
144
|
+
// Configuration Management
|
|
145
|
+
const configCmd = program
|
|
146
|
+
.command('config')
|
|
147
|
+
.description('Configuration management commands');
|
|
148
|
+
|
|
149
|
+
configCmd
|
|
150
|
+
.command('show')
|
|
151
|
+
.description('Display current configuration')
|
|
152
|
+
.action(showConfig);
|
|
153
|
+
|
|
154
|
+
configCmd
|
|
155
|
+
.command('edit')
|
|
156
|
+
.description('Edit configuration file')
|
|
157
|
+
.action(editConfig);
|
|
158
|
+
|
|
159
|
+
configCmd
|
|
160
|
+
.command('validate')
|
|
161
|
+
.description('Validate configuration')
|
|
162
|
+
.action(validateConfig);
|
|
163
|
+
|
|
164
|
+
configCmd
|
|
165
|
+
.command('get <key>')
|
|
166
|
+
.description('Get specific configuration value')
|
|
167
|
+
.action(getConfigValue);
|
|
168
|
+
|
|
169
|
+
configCmd
|
|
170
|
+
.command('set <key=value>')
|
|
171
|
+
.description('Set configuration value')
|
|
172
|
+
.action(setConfigValue);
|
|
173
|
+
|
|
125
174
|
// Status & Monitoring
|
|
126
175
|
program
|
|
127
176
|
.command('status')
|
|
@@ -134,7 +183,7 @@ program
|
|
|
134
183
|
.description('Run comprehensive health checks')
|
|
135
184
|
.action(doctor);
|
|
136
185
|
|
|
137
|
-
program
|
|
186
|
+
const logsCmd = program
|
|
138
187
|
.command('logs')
|
|
139
188
|
.description('View ANTE logs')
|
|
140
189
|
.option('-s, --service <name>', 'Specific service (backend, frontend, postgres, redis, mongodb)')
|
|
@@ -143,6 +192,36 @@ program
|
|
|
143
192
|
.option('--since <time>', 'Show logs since timestamp')
|
|
144
193
|
.action(logs);
|
|
145
194
|
|
|
195
|
+
logsCmd
|
|
196
|
+
.command('export')
|
|
197
|
+
.description('Export logs to file')
|
|
198
|
+
.option('-o, --output <file>', 'Output file path')
|
|
199
|
+
.option('-s, --service <name>', 'Specific service')
|
|
200
|
+
.option('-n, --lines <number>', 'Number of lines to export')
|
|
201
|
+
.option('--since <time>', 'Export logs since timestamp')
|
|
202
|
+
.action(exportLogs);
|
|
203
|
+
|
|
204
|
+
logsCmd
|
|
205
|
+
.command('errors')
|
|
206
|
+
.description('Show only error logs')
|
|
207
|
+
.option('-s, --service <name>', 'Specific service')
|
|
208
|
+
.option('-n, --lines <number>', 'Number of lines to check', '500')
|
|
209
|
+
.action(showErrors);
|
|
210
|
+
|
|
211
|
+
logsCmd
|
|
212
|
+
.command('clean')
|
|
213
|
+
.description('Clean old logs')
|
|
214
|
+
.option('-d, --days <days>', 'Delete logs older than N days', '7')
|
|
215
|
+
.option('--force', 'Skip confirmation')
|
|
216
|
+
.action(cleanLogs);
|
|
217
|
+
|
|
218
|
+
logsCmd
|
|
219
|
+
.command('search <keyword>')
|
|
220
|
+
.description('Search logs for keyword')
|
|
221
|
+
.option('-s, --service <name>', 'Specific service')
|
|
222
|
+
.option('-n, --lines <number>', 'Number of lines to search', '1000')
|
|
223
|
+
.action(searchLogs);
|
|
224
|
+
|
|
146
225
|
program
|
|
147
226
|
.command('ps')
|
|
148
227
|
.description('Show running containers')
|
|
@@ -195,4 +274,3 @@ program.parse();
|
|
|
195
274
|
if (!process.argv.slice(2).length) {
|
|
196
275
|
program.outputHelp();
|
|
197
276
|
}
|
|
198
|
-
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import updateNotifier from 'update-notifier';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
12
|
+
|
|
13
|
+
// Check for updates
|
|
14
|
+
updateNotifier({ pkg }).notify();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('ante')
|
|
18
|
+
.description(chalk.bold('ANTE ERP Management CLI'))
|
|
19
|
+
.version(pkg.version);
|
|
20
|
+
|
|
21
|
+
// Import commands
|
|
22
|
+
import { install } from '../src/commands/install.js';
|
|
23
|
+
import { status } from '../src/commands/status.js';
|
|
24
|
+
import { update } from '../src/commands/update.js';
|
|
25
|
+
import { updateCli } from '../src/commands/update-cli.js';
|
|
26
|
+
import { backup, listBackups } from '../src/commands/backup.js';
|
|
27
|
+
import { restore } from '../src/commands/restore.js';
|
|
28
|
+
import { start, stop, restart } from '../src/commands/service.js';
|
|
29
|
+
import { logs } from '../src/commands/logs.js';
|
|
30
|
+
import { doctor } from '../src/commands/doctor.js';
|
|
31
|
+
import { uninstall } from '../src/commands/uninstall.js';
|
|
32
|
+
import { migrate, seed, shell, optimize, reset, info } from '../src/commands/database.js';
|
|
33
|
+
|
|
34
|
+
// Installation & Setup
|
|
35
|
+
program
|
|
36
|
+
.command('install')
|
|
37
|
+
.description('Install ANTE ERP')
|
|
38
|
+
.option('-d, --dir <path>', 'Installation directory', './ante-erp')
|
|
39
|
+
.option('--domain <domain>', 'Domain name for SSL')
|
|
40
|
+
.option('--email <email>', 'Email for SSL certificate')
|
|
41
|
+
.option('--port <port>', 'Frontend port', '8080')
|
|
42
|
+
.option('--preset <type>', 'Installation preset (minimal, standard, enterprise)', 'standard')
|
|
43
|
+
.option('--no-interactive', 'Non-interactive mode with defaults')
|
|
44
|
+
.action(install);
|
|
45
|
+
|
|
46
|
+
// Update & Maintenance
|
|
47
|
+
program
|
|
48
|
+
.command('update')
|
|
49
|
+
.description('Update ANTE to latest version')
|
|
50
|
+
.option('-v, --version <version>', 'Specific version to install')
|
|
51
|
+
.option('--skip-backup', 'Skip automatic backup before update')
|
|
52
|
+
.option('--force', 'Force update without confirmation')
|
|
53
|
+
.action(update);
|
|
54
|
+
|
|
55
|
+
program
|
|
56
|
+
.command('upgrade')
|
|
57
|
+
.description('Alias for update')
|
|
58
|
+
.option('-v, --version <version>', 'Specific version to install')
|
|
59
|
+
.action(update);
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('update-cli')
|
|
63
|
+
.description('Update ANTE CLI tool to latest version')
|
|
64
|
+
.option('--check', 'Check for updates without installing')
|
|
65
|
+
.option('--force', 'Skip confirmation prompt')
|
|
66
|
+
.action(updateCli);
|
|
67
|
+
|
|
68
|
+
// Backup & Restore
|
|
69
|
+
const backupCmd = program
|
|
70
|
+
.command('backup')
|
|
71
|
+
.alias('dump')
|
|
72
|
+
.description('Create backup of ANTE ERP')
|
|
73
|
+
.option('-o, --output <path>', 'Output file path')
|
|
74
|
+
.option('--compress', 'Compress backup', true)
|
|
75
|
+
.option('--databases-only', 'Only backup databases')
|
|
76
|
+
.action(backup);
|
|
77
|
+
|
|
78
|
+
backupCmd
|
|
79
|
+
.command('list')
|
|
80
|
+
.description('List available backups')
|
|
81
|
+
.action(listBackups);
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command('restore [file]')
|
|
85
|
+
.description('Restore ANTE from backup')
|
|
86
|
+
.option('--force', 'Skip confirmation prompts')
|
|
87
|
+
.action(restore);
|
|
88
|
+
|
|
89
|
+
// Database Operations
|
|
90
|
+
const dbCmd = program
|
|
91
|
+
.command('db')
|
|
92
|
+
.description('Database management commands');
|
|
93
|
+
|
|
94
|
+
dbCmd
|
|
95
|
+
.command('migrate')
|
|
96
|
+
.description('Run database migrations')
|
|
97
|
+
.action(migrate);
|
|
98
|
+
|
|
99
|
+
dbCmd
|
|
100
|
+
.command('seed')
|
|
101
|
+
.description('Seed database with initial data')
|
|
102
|
+
.action(seed);
|
|
103
|
+
|
|
104
|
+
dbCmd
|
|
105
|
+
.command('shell')
|
|
106
|
+
.description('Open PostgreSQL shell')
|
|
107
|
+
.action(shell);
|
|
108
|
+
|
|
109
|
+
dbCmd
|
|
110
|
+
.command('optimize')
|
|
111
|
+
.description('Optimize database performance')
|
|
112
|
+
.action(optimize);
|
|
113
|
+
|
|
114
|
+
dbCmd
|
|
115
|
+
.command('reset')
|
|
116
|
+
.description('Reset database (destructive)')
|
|
117
|
+
.option('--force', 'Skip confirmation prompts')
|
|
118
|
+
.action(reset);
|
|
119
|
+
|
|
120
|
+
dbCmd
|
|
121
|
+
.command('info')
|
|
122
|
+
.description('Show database information')
|
|
123
|
+
.action(info);
|
|
124
|
+
|
|
125
|
+
// Status & Monitoring
|
|
126
|
+
program
|
|
127
|
+
.command('status')
|
|
128
|
+
.description('Show ANTE service status')
|
|
129
|
+
.action(status);
|
|
130
|
+
|
|
131
|
+
program
|
|
132
|
+
.command('health')
|
|
133
|
+
.alias('doctor')
|
|
134
|
+
.description('Run comprehensive health checks')
|
|
135
|
+
.action(doctor);
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command('logs')
|
|
139
|
+
.description('View ANTE logs')
|
|
140
|
+
.option('-s, --service <name>', 'Specific service (backend, frontend, postgres, redis, mongodb)')
|
|
141
|
+
.option('-f, --follow', 'Follow log output')
|
|
142
|
+
.option('-n, --lines <number>', 'Number of lines to show', '100')
|
|
143
|
+
.option('--since <time>', 'Show logs since timestamp')
|
|
144
|
+
.action(logs);
|
|
145
|
+
|
|
146
|
+
program
|
|
147
|
+
.command('ps')
|
|
148
|
+
.description('Show running containers')
|
|
149
|
+
.action(status);
|
|
150
|
+
|
|
151
|
+
// Service Management
|
|
152
|
+
program
|
|
153
|
+
.command('start')
|
|
154
|
+
.description('Start ANTE services')
|
|
155
|
+
.option('-s, --service <name>', 'Specific service to start')
|
|
156
|
+
.action(start);
|
|
157
|
+
|
|
158
|
+
program
|
|
159
|
+
.command('stop')
|
|
160
|
+
.description('Stop ANTE services')
|
|
161
|
+
.option('-s, --service <name>', 'Specific service to stop')
|
|
162
|
+
.action(stop);
|
|
163
|
+
|
|
164
|
+
program
|
|
165
|
+
.command('restart')
|
|
166
|
+
.description('Restart ANTE services')
|
|
167
|
+
.option('-s, --service <name>', 'Specific service to restart')
|
|
168
|
+
.action(restart);
|
|
169
|
+
|
|
170
|
+
// Cleanup
|
|
171
|
+
program
|
|
172
|
+
.command('uninstall')
|
|
173
|
+
.description('Uninstall ANTE ERP')
|
|
174
|
+
.option('--keep-data', 'Keep database volumes')
|
|
175
|
+
.option('--keep-files', 'Keep installation files')
|
|
176
|
+
.option('--force', 'Skip confirmation prompts')
|
|
177
|
+
.action(uninstall);
|
|
178
|
+
|
|
179
|
+
// Help
|
|
180
|
+
program
|
|
181
|
+
.command('help [command]')
|
|
182
|
+
.description('Display help for command')
|
|
183
|
+
.action((command) => {
|
|
184
|
+
if (command) {
|
|
185
|
+
program.commands.find(cmd => cmd.name() === command)?.help();
|
|
186
|
+
} else {
|
|
187
|
+
program.help();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Parse command line arguments
|
|
192
|
+
program.parse();
|
|
193
|
+
|
|
194
|
+
// Show help if no command provided
|
|
195
|
+
if (!process.argv.slice(2).length) {
|
|
196
|
+
program.outputHelp();
|
|
197
|
+
}
|
|
198
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { readdirSync, statSync, existsSync } from 'fs';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import { getInstallDir } from '../utils/config.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Verify backup integrity
|
|
12
|
+
*/
|
|
13
|
+
export async function verifyBackup(file, options) {
|
|
14
|
+
try {
|
|
15
|
+
const installDir = getInstallDir();
|
|
16
|
+
const backupDir = join(installDir, 'backups');
|
|
17
|
+
const backupFile = file || await selectBackup(backupDir);
|
|
18
|
+
|
|
19
|
+
if (!backupFile) {
|
|
20
|
+
console.log(chalk.yellow('No backup selected\n'));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fullPath = backupFile.startsWith('/') ? backupFile : join(backupDir, backupFile);
|
|
25
|
+
|
|
26
|
+
if (!existsSync(fullPath)) {
|
|
27
|
+
console.error(chalk.red(`Backup file not found: ${fullPath}\n`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold('\nš Verifying Backup\n'));
|
|
32
|
+
console.log(chalk.gray(`File: ${fullPath}\n`));
|
|
33
|
+
|
|
34
|
+
const spinner = ora('Checking file integrity...').start();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Test tar.gz integrity
|
|
38
|
+
await execa('tar', ['-tzf', fullPath], { stdout: 'pipe' });
|
|
39
|
+
spinner.succeed('Archive integrity: OK');
|
|
40
|
+
|
|
41
|
+
// Check file size
|
|
42
|
+
const stats = statSync(fullPath);
|
|
43
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
44
|
+
console.log(chalk.green(`File size: ${sizeMB} MB`));
|
|
45
|
+
|
|
46
|
+
// List contents
|
|
47
|
+
spinner.start('Checking contents...');
|
|
48
|
+
const result = await execa('tar', ['-tzf', fullPath]);
|
|
49
|
+
const files = result.stdout.split('\n').filter(Boolean);
|
|
50
|
+
|
|
51
|
+
const hasPostgres = files.some(f => f.includes('postgres.dump'));
|
|
52
|
+
const hasEnv = files.some(f => f.includes('env.backup'));
|
|
53
|
+
const hasCompose = files.some(f => f.includes('docker-compose.yml'));
|
|
54
|
+
|
|
55
|
+
spinner.stop();
|
|
56
|
+
console.log(chalk.green(`PostgreSQL dump: ${hasPostgres ? 'Found' : 'Missing'}`));
|
|
57
|
+
console.log(chalk.green(`Environment config: ${hasEnv ? 'Found' : 'Missing'}`));
|
|
58
|
+
console.log(chalk.green(`Docker compose: ${hasCompose ? 'Found' : 'Missing'}`));
|
|
59
|
+
console.log(chalk.gray(`\nTotal files: ${files.length}`));
|
|
60
|
+
|
|
61
|
+
if (!hasPostgres || !hasEnv) {
|
|
62
|
+
console.log(chalk.yellow('\nWarning: Backup may be incomplete\n'));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.green.bold('\nBackup is valid\n'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
spinner.fail('Backup verification failed');
|
|
69
|
+
console.error(chalk.red('Archive is corrupted or invalid\n'));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(chalk.red('Error:'), error.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clean old backups
|
|
81
|
+
*/
|
|
82
|
+
export async function cleanBackups(options) {
|
|
83
|
+
try {
|
|
84
|
+
const installDir = getInstallDir();
|
|
85
|
+
const backupDir = join(installDir, 'backups');
|
|
86
|
+
const days = options.days || 30;
|
|
87
|
+
|
|
88
|
+
console.log(chalk.bold(`\nš§¹ Cleaning Backups Older Than ${days} Days\n`));
|
|
89
|
+
|
|
90
|
+
const files = readdirSync(backupDir).filter(f => f.endsWith('.tar.gz'));
|
|
91
|
+
|
|
92
|
+
if (files.length === 0) {
|
|
93
|
+
console.log(chalk.yellow('No backups found\n'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const cutoff = days * 24 * 60 * 60 * 1000;
|
|
99
|
+
const oldFiles = [];
|
|
100
|
+
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const filePath = join(backupDir, file);
|
|
103
|
+
const stats = statSync(filePath);
|
|
104
|
+
const age = now - stats.mtimeMs;
|
|
105
|
+
|
|
106
|
+
if (age > cutoff) {
|
|
107
|
+
oldFiles.push({ file, age: Math.floor(age / (24 * 60 * 60 * 1000)), size: stats.size });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (oldFiles.length === 0) {
|
|
112
|
+
console.log(chalk.green(`No backups older than ${days} days\n`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(chalk.yellow(`Found ${oldFiles.length} old backup(s):\n`));
|
|
117
|
+
oldFiles.forEach(item => {
|
|
118
|
+
console.log(chalk.gray(` - ${item.file} (${item.age} days old)`));
|
|
119
|
+
});
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
if (!options.force) {
|
|
123
|
+
const answers = await inquirer.prompt([{
|
|
124
|
+
type: 'confirm',
|
|
125
|
+
name: 'confirm',
|
|
126
|
+
message: `Delete ${oldFiles.length} old backup(s)?`,
|
|
127
|
+
default: false
|
|
128
|
+
}]);
|
|
129
|
+
|
|
130
|
+
if (!answers.confirm) {
|
|
131
|
+
console.log(chalk.yellow('Cancelled\n'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let deletedSize = 0;
|
|
137
|
+
for (const item of oldFiles) {
|
|
138
|
+
const filePath = join(backupDir, item.file);
|
|
139
|
+
await execa('rm', [filePath]);
|
|
140
|
+
deletedSize += item.size;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const freedMB = (deletedSize / 1024 / 1024).toFixed(2);
|
|
144
|
+
console.log(chalk.green(`\nDeleted ${oldFiles.length} backup(s)`));
|
|
145
|
+
console.log(chalk.gray(` Freed: ${freedMB} MB\n`));
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(chalk.red('Error:'), error.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Show total backup storage usage
|
|
155
|
+
*/
|
|
156
|
+
export async function backupSize() {
|
|
157
|
+
try {
|
|
158
|
+
const installDir = getInstallDir();
|
|
159
|
+
const backupDir = join(installDir, 'backups');
|
|
160
|
+
|
|
161
|
+
console.log(chalk.bold('\nš¾ Backup Storage Usage\n'));
|
|
162
|
+
|
|
163
|
+
const files = readdirSync(backupDir).filter(f => f.endsWith('.tar.gz'));
|
|
164
|
+
|
|
165
|
+
if (files.length === 0) {
|
|
166
|
+
console.log(chalk.yellow('No backups found\n'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let totalSize = 0;
|
|
171
|
+
const fileDetails = [];
|
|
172
|
+
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
const filePath = join(backupDir, file);
|
|
175
|
+
const stats = statSync(filePath);
|
|
176
|
+
totalSize += stats.size;
|
|
177
|
+
|
|
178
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
179
|
+
const age = Math.floor((Date.now() - stats.mtimeMs) / (24 * 60 * 60 * 1000));
|
|
180
|
+
|
|
181
|
+
fileDetails.push({ file, size: sizeMB, age });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sort by size (largest first)
|
|
185
|
+
fileDetails.sort((a, b) => parseFloat(b.size) - parseFloat(a.size));
|
|
186
|
+
|
|
187
|
+
const table = new Table({
|
|
188
|
+
head: [chalk.cyan('Backup'), chalk.cyan('Size (MB)'), chalk.cyan('Age (days)')],
|
|
189
|
+
style: { head: [], border: ['gray'] }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
fileDetails.slice(0, 10).forEach(item => {
|
|
193
|
+
table.push([item.file, item.size, item.age]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
console.log(table.toString());
|
|
197
|
+
|
|
198
|
+
const totalMB = (totalSize / 1024 / 1024).toFixed(2);
|
|
199
|
+
const totalGB = (totalSize / 1024 / 1024 / 1024).toFixed(2);
|
|
200
|
+
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(chalk.bold(`Total: ${files.length} backup(s)`));
|
|
203
|
+
console.log(chalk.bold(`Size: ${totalMB} MB (${totalGB} GB)`));
|
|
204
|
+
console.log(chalk.gray(`\nLocation: ${backupDir}\n`));
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(chalk.red('Error:'), error.message);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Helper function to select backup interactively
|
|
214
|
+
*/
|
|
215
|
+
async function selectBackup(backupDir) {
|
|
216
|
+
const files = readdirSync(backupDir)
|
|
217
|
+
.filter(f => f.endsWith('.tar.gz'))
|
|
218
|
+
.sort()
|
|
219
|
+
.reverse();
|
|
220
|
+
|
|
221
|
+
if (files.length === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const answers = await inquirer.prompt([{
|
|
226
|
+
type: 'list',
|
|
227
|
+
name: 'backup',
|
|
228
|
+
message: 'Select backup to verify:',
|
|
229
|
+
choices: files
|
|
230
|
+
}]);
|
|
231
|
+
|
|
232
|
+
return answers.backup;
|
|
233
|
+
}
|
package/src/commands/backup.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { readdirSync } from 'fs';
|
|
4
|
+
import { readdirSync, statSync, existsSync } from 'fs';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import Table from 'cli-table3';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
7
8
|
import { getInstallDir } from '../utils/config.js';
|
|
8
9
|
import { execInContainer } from '../utils/docker.js';
|
|
9
10
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { getInstallDir } from '../utils/config.js';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Show current configuration
|
|
10
|
+
*/
|
|
11
|
+
export async function showConfig() {
|
|
12
|
+
try {
|
|
13
|
+
const installDir = getInstallDir();
|
|
14
|
+
const envFile = join(installDir, '.env');
|
|
15
|
+
|
|
16
|
+
if (!existsSync(envFile)) {
|
|
17
|
+
console.log(chalk.yellow('ā ļø No .env file found'));
|
|
18
|
+
console.log(chalk.gray(`Expected location: ${envFile}`));
|
|
19
|
+
console.log(chalk.gray('\nRun "ante install" first to create the configuration.'));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(chalk.bold('\nš ANTE Configuration\n'));
|
|
24
|
+
console.log(chalk.gray(`Location: ${envFile}\n`));
|
|
25
|
+
|
|
26
|
+
const envContent = readFileSync(envFile, 'utf8');
|
|
27
|
+
const lines = envContent.split('\n');
|
|
28
|
+
|
|
29
|
+
let currentSection = '';
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (line.trim() === '' || line.trim().startsWith('#')) {
|
|
32
|
+
// Section header
|
|
33
|
+
if (line.trim().startsWith('#') && !line.includes('=')) {
|
|
34
|
+
currentSection = line.trim().replace(/^#+\s*/, '');
|
|
35
|
+
console.log(chalk.cyan(`\n${currentSection}`));
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [key, ...valueParts] = line.split('=');
|
|
41
|
+
const value = valueParts.join('=');
|
|
42
|
+
|
|
43
|
+
if (key && value !== undefined) {
|
|
44
|
+
// Mask sensitive values
|
|
45
|
+
const sensitiveKeys = ['PASSWORD', 'SECRET', 'KEY', 'TOKEN'];
|
|
46
|
+
const isSensitive = sensitiveKeys.some(k => key.toUpperCase().includes(k));
|
|
47
|
+
const displayValue = isSensitive ? '********' : value;
|
|
48
|
+
|
|
49
|
+
console.log(chalk.gray(` ${key}=`) + displayValue);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(chalk.gray('\nš” Tip: Use "ante config edit" to modify configuration\n'));
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(chalk.red('Error:'), error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Edit configuration
|
|
63
|
+
*/
|
|
64
|
+
export async function editConfig() {
|
|
65
|
+
try {
|
|
66
|
+
const installDir = getInstallDir();
|
|
67
|
+
const envFile = join(installDir, '.env');
|
|
68
|
+
|
|
69
|
+
if (!existsSync(envFile)) {
|
|
70
|
+
console.log(chalk.yellow('ā ļø No .env file found'));
|
|
71
|
+
console.log(chalk.gray(`Expected location: ${envFile}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create backup
|
|
76
|
+
const backupFile = `${envFile}.backup.${Date.now()}`;
|
|
77
|
+
writeFileSync(backupFile, readFileSync(envFile));
|
|
78
|
+
console.log(chalk.green(`ā Created backup: ${backupFile}`));
|
|
79
|
+
|
|
80
|
+
// Determine editor
|
|
81
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
|
|
82
|
+
|
|
83
|
+
console.log(chalk.bold(`\nš Opening configuration in ${editor}...`));
|
|
84
|
+
console.log(chalk.gray('Save and exit when done\n'));
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
execSync(`${editor} "${envFile}"`, { stdio: 'inherit' });
|
|
88
|
+
|
|
89
|
+
console.log(chalk.green('\nā Configuration saved'));
|
|
90
|
+
|
|
91
|
+
// Prompt to restart
|
|
92
|
+
const { shouldRestart } = await inquirer.prompt([{
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'shouldRestart',
|
|
95
|
+
message: 'Restart services to apply changes?',
|
|
96
|
+
default: true
|
|
97
|
+
}]);
|
|
98
|
+
|
|
99
|
+
if (shouldRestart) {
|
|
100
|
+
console.log(chalk.gray('\nRestarting services...\n'));
|
|
101
|
+
execSync('ante restart', { stdio: 'inherit' });
|
|
102
|
+
} else {
|
|
103
|
+
console.log(chalk.yellow('\nā ļø Remember to restart services: ante restart\n'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(chalk.red('\nEditor error:'), error.message);
|
|
108
|
+
console.log(chalk.yellow(`\nRestore backup if needed: cp ${backupFile} ${envFile}`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(chalk.red('Error:'), error.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate configuration
|
|
120
|
+
*/
|
|
121
|
+
export async function validateConfig() {
|
|
122
|
+
try {
|
|
123
|
+
const installDir = getInstallDir();
|
|
124
|
+
const envFile = join(installDir, '.env');
|
|
125
|
+
|
|
126
|
+
if (!existsSync(envFile)) {
|
|
127
|
+
console.log(chalk.red('ā No .env file found'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(chalk.bold('\nš Validating Configuration\n'));
|
|
132
|
+
|
|
133
|
+
const envContent = readFileSync(envFile, 'utf8');
|
|
134
|
+
const config = {};
|
|
135
|
+
|
|
136
|
+
// Parse .env file
|
|
137
|
+
for (const line of envContent.split('\n')) {
|
|
138
|
+
const trimmed = line.trim();
|
|
139
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
140
|
+
|
|
141
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
142
|
+
if (key) {
|
|
143
|
+
config[key.trim()] = valueParts.join('=').trim();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const errors = [];
|
|
148
|
+
const warnings = [];
|
|
149
|
+
|
|
150
|
+
// Required configurations
|
|
151
|
+
const required = {
|
|
152
|
+
'DATABASE_URL': 'PostgreSQL connection string',
|
|
153
|
+
'DIRECT_URL': 'PostgreSQL direct connection',
|
|
154
|
+
'MONGODB_URI': 'MongoDB connection string',
|
|
155
|
+
'JWT_SECRET': 'JWT secret key',
|
|
156
|
+
'REDIS_HOST': 'Redis host',
|
|
157
|
+
'REDIS_PORT': 'Redis port'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
for (const [key, description] of Object.entries(required)) {
|
|
161
|
+
if (!config[key]) {
|
|
162
|
+
errors.push(`Missing ${chalk.yellow(key)} - ${description}`);
|
|
163
|
+
} else {
|
|
164
|
+
console.log(chalk.green('ā'), key);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Warnings for default values
|
|
169
|
+
if (config.JWT_SECRET && config.JWT_SECRET.includes('change-this')) {
|
|
170
|
+
warnings.push('JWT_SECRET contains default value - change in production');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (config.DATABASE_URL && config.DATABASE_URL.includes('your-password')) {
|
|
174
|
+
warnings.push('DATABASE_URL contains placeholder password');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check JWT secret length
|
|
178
|
+
if (config.JWT_SECRET && config.JWT_SECRET.length < 32) {
|
|
179
|
+
warnings.push('JWT_SECRET should be at least 32 characters');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Print results
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
if (errors.length > 0) {
|
|
186
|
+
console.log(chalk.red.bold('Errors:'));
|
|
187
|
+
errors.forEach(err => console.log(chalk.red(' ā'), err));
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (warnings.length > 0) {
|
|
192
|
+
console.log(chalk.yellow.bold('Warnings:'));
|
|
193
|
+
warnings.forEach(warn => console.log(chalk.yellow(' ā ļø '), warn));
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
198
|
+
console.log(chalk.green.bold('ā Configuration is valid\n'));
|
|
199
|
+
} else if (errors.length === 0) {
|
|
200
|
+
console.log(chalk.yellow.bold('ā ļø Configuration has warnings\n'));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(chalk.red.bold('ā Configuration has errors\n'));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(chalk.red('Error:'), error.message);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get specific configuration value
|
|
214
|
+
*/
|
|
215
|
+
export async function getConfigValue(key) {
|
|
216
|
+
try {
|
|
217
|
+
const installDir = getInstallDir();
|
|
218
|
+
const envFile = join(installDir, '.env');
|
|
219
|
+
|
|
220
|
+
if (!existsSync(envFile)) {
|
|
221
|
+
console.log(chalk.red('ā No .env file found'));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const envContent = readFileSync(envFile, 'utf8');
|
|
226
|
+
|
|
227
|
+
for (const line of envContent.split('\n')) {
|
|
228
|
+
const trimmed = line.trim();
|
|
229
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
230
|
+
|
|
231
|
+
const [envKey, ...valueParts] = trimmed.split('=');
|
|
232
|
+
if (envKey.trim() === key) {
|
|
233
|
+
const value = valueParts.join('=').trim();
|
|
234
|
+
console.log(value);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.error(chalk.red(`ā Key "${key}" not found in configuration`));
|
|
240
|
+
process.exit(1);
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(chalk.red('Error:'), error.message);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Set specific configuration value
|
|
250
|
+
*/
|
|
251
|
+
export async function setConfigValue(keyValue) {
|
|
252
|
+
try {
|
|
253
|
+
const installDir = getInstallDir();
|
|
254
|
+
const envFile = join(installDir, '.env');
|
|
255
|
+
|
|
256
|
+
if (!existsSync(envFile)) {
|
|
257
|
+
console.log(chalk.red('ā No .env file found'));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const [key, ...valueParts] = keyValue.split('=');
|
|
262
|
+
const value = valueParts.join('=');
|
|
263
|
+
|
|
264
|
+
if (!key || value === undefined) {
|
|
265
|
+
console.error(chalk.red('ā Invalid format. Use: KEY=VALUE'));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Create backup
|
|
270
|
+
const backupFile = `${envFile}.backup.${Date.now()}`;
|
|
271
|
+
const originalContent = readFileSync(envFile, 'utf8');
|
|
272
|
+
writeFileSync(backupFile, originalContent);
|
|
273
|
+
|
|
274
|
+
const lines = originalContent.split('\n');
|
|
275
|
+
let found = false;
|
|
276
|
+
|
|
277
|
+
const newLines = lines.map(line => {
|
|
278
|
+
const trimmed = line.trim();
|
|
279
|
+
if (!trimmed || trimmed.startsWith('#')) return line;
|
|
280
|
+
|
|
281
|
+
const [envKey] = trimmed.split('=');
|
|
282
|
+
if (envKey.trim() === key.trim()) {
|
|
283
|
+
found = true;
|
|
284
|
+
return `${key}=${value}`;
|
|
285
|
+
}
|
|
286
|
+
return line;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!found) {
|
|
290
|
+
// Add new key
|
|
291
|
+
newLines.push(`${key}=${value}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
writeFileSync(envFile, newLines.join('\n'));
|
|
295
|
+
|
|
296
|
+
console.log(chalk.green(`ā Set ${key}=${value}`));
|
|
297
|
+
console.log(chalk.gray(`Backup created: ${backupFile}`));
|
|
298
|
+
console.log(chalk.yellow('\nā ļø Restart services to apply: ante restart\n'));
|
|
299
|
+
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error(chalk.red('Error:'), error.message);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/commands/logs.js
CHANGED
|
@@ -1,33 +1,222 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { writeFileSync } from 'fs';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
3
6
|
import { getInstallDir } from '../utils/config.js';
|
|
4
7
|
import { getLogs } from '../utils/docker.js';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
|
-
* View ANTE logs
|
|
13
|
+
* View ANTE logs (original function)
|
|
8
14
|
*/
|
|
9
15
|
export async function logs(options) {
|
|
10
16
|
try {
|
|
11
17
|
const installDir = getInstallDir();
|
|
12
18
|
const composeFile = join(installDir, 'docker-compose.yml');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
|
|
20
|
+
const serviceLabel = options.service ? ` (${options.service})` : '';
|
|
21
|
+
console.log(chalk.bold(`\nš ANTE Logs${serviceLabel}\n`));
|
|
22
|
+
|
|
16
23
|
const logOptions = {
|
|
17
24
|
tail: options.lines ? parseInt(options.lines) : 100,
|
|
18
25
|
follow: options.follow,
|
|
19
26
|
since: options.since
|
|
20
27
|
};
|
|
21
|
-
|
|
28
|
+
|
|
22
29
|
const logOutput = await getLogs(composeFile, options.service, logOptions);
|
|
23
|
-
|
|
30
|
+
|
|
24
31
|
if (!options.follow && logOutput) {
|
|
25
32
|
console.log(logOutput);
|
|
26
33
|
}
|
|
27
|
-
|
|
34
|
+
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(chalk.red('Error:'), error.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Export logs to file
|
|
43
|
+
*/
|
|
44
|
+
export async function exportLogs(options) {
|
|
45
|
+
try {
|
|
46
|
+
const installDir = getInstallDir();
|
|
47
|
+
const composeFile = join(installDir, 'docker-compose.yml');
|
|
48
|
+
|
|
49
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
|
50
|
+
const defaultFilename = `ante-logs-${timestamp}.txt`;
|
|
51
|
+
const outputFile = options.output || defaultFilename;
|
|
52
|
+
|
|
53
|
+
console.log(chalk.bold('\nš¤ Exporting Logs\n'));
|
|
54
|
+
|
|
55
|
+
const logOptions = {
|
|
56
|
+
tail: options.lines ? parseInt(options.lines) : undefined,
|
|
57
|
+
since: options.since
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const logOutput = await getLogs(composeFile, options.service, logOptions);
|
|
61
|
+
|
|
62
|
+
if (logOutput) {
|
|
63
|
+
writeFileSync(outputFile, logOutput);
|
|
64
|
+
console.log(chalk.green(`ā Logs exported to: ${outputFile}`));
|
|
65
|
+
|
|
66
|
+
const result = await execAsync(`du -h "${outputFile}"`);
|
|
67
|
+
const size = result.stdout.split('\t')[0];
|
|
68
|
+
console.log(chalk.gray(` Size: ${size}`));
|
|
69
|
+
console.log('');
|
|
70
|
+
} else {
|
|
71
|
+
console.log(chalk.yellow('ā ļø No logs to export'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(chalk.red('Error:'), error.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Show only error logs
|
|
82
|
+
*/
|
|
83
|
+
export async function showErrors(options) {
|
|
84
|
+
try {
|
|
85
|
+
const installDir = getInstallDir();
|
|
86
|
+
const composeFile = join(installDir, 'docker-compose.yml');
|
|
87
|
+
|
|
88
|
+
console.log(chalk.bold.red('\nš“ Error Logs\n'));
|
|
89
|
+
|
|
90
|
+
const logOptions = {
|
|
91
|
+
tail: options.lines ? parseInt(options.lines) : 500
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const logOutput = await getLogs(composeFile, options.service, logOptions);
|
|
95
|
+
|
|
96
|
+
if (logOutput) {
|
|
97
|
+
const errorLines = logOutput
|
|
98
|
+
.split('\n')
|
|
99
|
+
.filter(line =>
|
|
100
|
+
line.toLowerCase().includes('error') ||
|
|
101
|
+
line.toLowerCase().includes('fatal') ||
|
|
102
|
+
line.toLowerCase().includes('exception')
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (errorLines.length > 0) {
|
|
106
|
+
console.log(errorLines.join('\n'));
|
|
107
|
+
console.log(chalk.yellow(`\nā ļø Found ${errorLines.length} error(s)\n`));
|
|
108
|
+
} else {
|
|
109
|
+
console.log(chalk.green('ā No errors found in recent logs\n'));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
28
113
|
} catch (error) {
|
|
29
114
|
console.error(chalk.red('Error:'), error.message);
|
|
30
115
|
process.exit(1);
|
|
31
116
|
}
|
|
32
117
|
}
|
|
33
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Clean old logs
|
|
121
|
+
*/
|
|
122
|
+
export async function cleanLogs(options) {
|
|
123
|
+
try {
|
|
124
|
+
const installDir = getInstallDir();
|
|
125
|
+
const days = options.days || 7;
|
|
126
|
+
|
|
127
|
+
console.log(chalk.bold(`\nš§¹ Cleaning Logs Older Than ${days} Days\n`));
|
|
128
|
+
|
|
129
|
+
if (!options.force) {
|
|
130
|
+
const answers = await inquirer.prompt([{
|
|
131
|
+
type: 'confirm',
|
|
132
|
+
name: 'confirm',
|
|
133
|
+
message: `Delete logs older than ${days} days?`,
|
|
134
|
+
default: false
|
|
135
|
+
}]);
|
|
136
|
+
|
|
137
|
+
if (!answers.confirm) {
|
|
138
|
+
console.log(chalk.yellow('Cancelled\n'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(chalk.gray('Rotating Docker container logs...'));
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const result = await execAsync('docker ps --format "{{.Names}}"');
|
|
147
|
+
const containers = result.stdout.trim().split('\n').filter(Boolean);
|
|
148
|
+
|
|
149
|
+
for (const container of containers) {
|
|
150
|
+
if (container.includes('ante')) {
|
|
151
|
+
await execAsync(`docker logs ${container} 2>&1 >/dev/null`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(chalk.green('ā Docker logs rotated'));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.log(chalk.yellow('ā ļø Could not rotate some logs'));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const logsDir = join(installDir, 'logs');
|
|
161
|
+
try {
|
|
162
|
+
const findCmd = `find "${logsDir}" -name "*.log" -mtime +${days} -type f 2>/dev/null || true`;
|
|
163
|
+
const result = await execAsync(findCmd);
|
|
164
|
+
const oldFiles = result.stdout.trim().split('\n').filter(Boolean);
|
|
165
|
+
|
|
166
|
+
if (oldFiles.length > 0) {
|
|
167
|
+
const deleteCmd = `find "${logsDir}" -name "*.log" -mtime +${days} -type f -delete 2>/dev/null || true`;
|
|
168
|
+
await execAsync(deleteCmd);
|
|
169
|
+
console.log(chalk.green(`ā Deleted ${oldFiles.length} old log file(s)`));
|
|
170
|
+
} else {
|
|
171
|
+
console.log(chalk.gray('No old log files found'));
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.log(chalk.gray('No local log files to clean'));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(chalk.green('\nā Log cleanup completed\n'));
|
|
178
|
+
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(chalk.red('Error:'), error.message);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Search logs for keyword
|
|
187
|
+
*/
|
|
188
|
+
export async function searchLogs(keyword, options) {
|
|
189
|
+
try {
|
|
190
|
+
const installDir = getInstallDir();
|
|
191
|
+
const composeFile = join(installDir, 'docker-compose.yml');
|
|
192
|
+
|
|
193
|
+
console.log(chalk.bold(`\nš Searching for: "${keyword}"\n`));
|
|
194
|
+
|
|
195
|
+
const logOptions = {
|
|
196
|
+
tail: options.lines ? parseInt(options.lines) : 1000
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const logOutput = await getLogs(composeFile, options.service, logOptions);
|
|
200
|
+
|
|
201
|
+
if (logOutput) {
|
|
202
|
+
const matchingLines = logOutput
|
|
203
|
+
.split('\n')
|
|
204
|
+
.filter(line => line.toLowerCase().includes(keyword.toLowerCase()));
|
|
205
|
+
|
|
206
|
+
if (matchingLines.length > 0) {
|
|
207
|
+
matchingLines.forEach(line => {
|
|
208
|
+
const regex = new RegExp(keyword, 'gi');
|
|
209
|
+
const highlighted = line.replace(regex, (match) => chalk.yellow.bold(match));
|
|
210
|
+
console.log(highlighted);
|
|
211
|
+
});
|
|
212
|
+
console.log(chalk.gray(`\nFound ${matchingLines.length} match(es)\n`));
|
|
213
|
+
} else {
|
|
214
|
+
console.log(chalk.yellow(`No matches found for "${keyword}"\n`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(chalk.red('Error:'), error.message);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { getInstallDir } from '../utils/config.js';
|
|
4
|
+
import { getLogs } from '../utils/docker.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* View ANTE logs
|
|
8
|
+
*/
|
|
9
|
+
export async function logs(options) {
|
|
10
|
+
try {
|
|
11
|
+
const installDir = getInstallDir();
|
|
12
|
+
const composeFile = join(installDir, 'docker-compose.yml');
|
|
13
|
+
|
|
14
|
+
console.log(chalk.bold(`\nš ANTE Logs${options.service ? ` (${options.service})` : ''}\n`));
|
|
15
|
+
|
|
16
|
+
const logOptions = {
|
|
17
|
+
tail: options.lines ? parseInt(options.lines) : 100,
|
|
18
|
+
follow: options.follow,
|
|
19
|
+
since: options.since
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const logOutput = await getLogs(composeFile, options.service, logOptions);
|
|
23
|
+
|
|
24
|
+
if (!options.follow && logOutput) {
|
|
25
|
+
console.log(logOutput);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(chalk.red('Error:'), error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -117,7 +117,7 @@ services:
|
|
|
117
117
|
REDIS_PASSWORD: \${REDIS_PASSWORD}
|
|
118
118
|
REDIS_DB: 0
|
|
119
119
|
REDIS_TLS: "false"
|
|
120
|
-
MONGODB_URI: mongodb://ante:\${
|
|
120
|
+
MONGODB_URI: mongodb://ante:\${MONGO_PASSWORD_ENCODED}@mongodb:27017/ante?authSource=admin
|
|
121
121
|
JWT_SECRET: \${JWT_SECRET}
|
|
122
122
|
JWT_EXPIRATION: \${JWT_EXPIRATION:-24h}
|
|
123
123
|
DEVELOPER_KEY: \${DEVELOPER_KEY}
|
package/src/templates/env.js
CHANGED
|
@@ -30,6 +30,7 @@ export function generateEnv(credentials, options = {}) {
|
|
|
30
30
|
DB_PASSWORD=${credentials.dbPassword}
|
|
31
31
|
REDIS_PASSWORD=${credentials.redisPassword}
|
|
32
32
|
MONGO_PASSWORD=${credentials.mongoPassword}
|
|
33
|
+
MONGO_PASSWORD_ENCODED=${encodeURIComponent(credentials.mongoPassword)}
|
|
33
34
|
|
|
34
35
|
# ------------------------------------------------------------------------------
|
|
35
36
|
# SECURITY KEYS
|
package/src/utils/validation.js
CHANGED
|
@@ -103,8 +103,8 @@ export function checkDiskSpace(path = '/', requiredGB = 20) {
|
|
|
103
103
|
* @returns {{ok: boolean, message: string, availableGB: number}}
|
|
104
104
|
*/
|
|
105
105
|
export function checkMemory(requiredGB = 2) {
|
|
106
|
-
const totalGB = Math.
|
|
107
|
-
const freeGB = Math.
|
|
106
|
+
const totalGB = Math.round(totalmem() / (1024 ** 3));
|
|
107
|
+
const freeGB = Math.round(freemem() / (1024 ** 3));
|
|
108
108
|
|
|
109
109
|
return {
|
|
110
110
|
ok: totalGB >= requiredGB,
|