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 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:list # List available backups
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:migrate # Run database migrations
109
- ante db:seed # Seed database
110
- ante db:shell # Open PostgreSQL shell
111
- ante db:optimize # Optimize database
112
- ante db:reset # Reset database (āš ļø destructive)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }
@@ -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
+ }
@@ -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
- console.log(chalk.bold(`\nšŸ“„ ANTE Logs${options.service ? ` (${options.service})` : ''}\n`));
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:\${MONGO_PASSWORD}@mongodb:27017/ante?authSource=admin
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}
@@ -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
@@ -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.floor(totalmem() / (1024 ** 3));
107
- const freeGB = Math.floor(freemem() / (1024 ** 3));
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,