ante-erp-cli 1.2.0 ā 1.3.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 +22 -0
- package/bin/ante-cli.js +31 -4
- package/bin/ante-cli.js.bak +198 -0
- package/package.json +1 -1
- package/src/commands/install.js +29 -24
- package/src/commands/logs.js.bak +33 -0
- package/src/commands/reset.js +264 -0
- package/src/commands/set-domain.js +174 -0
- package/src/templates/docker-compose.yml.js +3 -0
package/README.md
CHANGED
|
@@ -93,6 +93,28 @@ ante backup:list # List available backups
|
|
|
93
93
|
ante restore <file> # Restore from backup
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
### š System Reset
|
|
97
|
+
|
|
98
|
+
**ā ļø WARNING: This completely resets your ANTE ERP system!**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
ante reset # Reset system to initial state (with mandatory backup)
|
|
102
|
+
ante reset --force # Skip confirmation (backup still created)
|
|
103
|
+
ante reset --backup-only # Create backup without resetting
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The `ante reset` command:
|
|
107
|
+
- ā
**Creates mandatory backup** before resetting (cannot be skipped)
|
|
108
|
+
- ā **Deletes all companies, users, and data**
|
|
109
|
+
- š **Resets PostgreSQL and MongoDB databases**
|
|
110
|
+
- š **Prepares system for setup wizard**
|
|
111
|
+
- ā ļø **Requires typing "RESET ALL DATA" to confirm**
|
|
112
|
+
|
|
113
|
+
**After reset:**
|
|
114
|
+
1. Navigate to `http://localhost:9000`
|
|
115
|
+
2. Complete the setup wizard
|
|
116
|
+
3. Create your first admin account
|
|
117
|
+
|
|
96
118
|
### š® Service Management
|
|
97
119
|
|
|
98
120
|
```bash
|
package/bin/ante-cli.js
CHANGED
|
@@ -25,22 +25,25 @@ 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
27
|
import { restore } from '../src/commands/restore.js';
|
|
28
|
+
import { reset as systemReset } from '../src/commands/reset.js';
|
|
28
29
|
import { start, stop, restart } from '../src/commands/service.js';
|
|
29
30
|
import { logs } from '../src/commands/logs.js';
|
|
30
31
|
import { doctor } from '../src/commands/doctor.js';
|
|
31
32
|
import { uninstall } from '../src/commands/uninstall.js';
|
|
32
|
-
import { migrate, seed, shell, optimize, reset, info } from '../src/commands/database.js';
|
|
33
|
+
import { migrate, seed, shell, optimize, reset as dbReset, info } from '../src/commands/database.js';
|
|
34
|
+
import { setDomain } from '../src/commands/set-domain.js';
|
|
33
35
|
|
|
34
36
|
// Installation & Setup
|
|
35
37
|
program
|
|
36
38
|
.command('install')
|
|
37
39
|
.description('Install ANTE ERP')
|
|
38
40
|
.option('-d, --dir <path>', 'Installation directory', './ante-erp')
|
|
39
|
-
.option('--domain <
|
|
40
|
-
.option('--
|
|
41
|
+
.option('--frontend-domain <url>', 'Frontend domain/URL (e.g., https://app.example.com or http://IP:8080)')
|
|
42
|
+
.option('--api-domain <url>', 'API domain/URL (e.g., https://api.example.com or http://IP:3001)')
|
|
41
43
|
.option('--port <port>', 'Frontend port', '8080')
|
|
42
44
|
.option('--preset <type>', 'Installation preset (minimal, standard, enterprise)', 'standard')
|
|
43
45
|
.option('--no-interactive', 'Non-interactive mode with defaults')
|
|
46
|
+
.option('--skip-checks', 'Skip system requirements check')
|
|
44
47
|
.action(install);
|
|
45
48
|
|
|
46
49
|
// Update & Maintenance
|
|
@@ -86,6 +89,21 @@ program
|
|
|
86
89
|
.option('--force', 'Skip confirmation prompts')
|
|
87
90
|
.action(restore);
|
|
88
91
|
|
|
92
|
+
// System Reset
|
|
93
|
+
program
|
|
94
|
+
.command('reset')
|
|
95
|
+
.description('Reset ANTE ERP system to initial state (with mandatory backup)')
|
|
96
|
+
.option('--force', 'Skip confirmation prompts (backup still mandatory)')
|
|
97
|
+
.option('--backup-only', 'Create backup and exit without resetting')
|
|
98
|
+
.action(async (options) => {
|
|
99
|
+
try {
|
|
100
|
+
await systemReset(options);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(chalk.red('Reset failed:'), error.message);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
89
107
|
// Database Operations
|
|
90
108
|
const dbCmd = program
|
|
91
109
|
.command('db')
|
|
@@ -115,7 +133,7 @@ dbCmd
|
|
|
115
133
|
.command('reset')
|
|
116
134
|
.description('Reset database (destructive)')
|
|
117
135
|
.option('--force', 'Skip confirmation prompts')
|
|
118
|
-
.action(
|
|
136
|
+
.action(dbReset);
|
|
119
137
|
|
|
120
138
|
dbCmd
|
|
121
139
|
.command('info')
|
|
@@ -176,6 +194,15 @@ program
|
|
|
176
194
|
.option('--force', 'Skip confirmation prompts')
|
|
177
195
|
.action(uninstall);
|
|
178
196
|
|
|
197
|
+
// Configuration
|
|
198
|
+
program
|
|
199
|
+
.command('set-domain')
|
|
200
|
+
.description('Configure frontend and API domains')
|
|
201
|
+
.option('--frontend <url>', 'Frontend domain/URL')
|
|
202
|
+
.option('--api <url>', 'API domain/URL')
|
|
203
|
+
.option('--no-interactive', 'Non-interactive mode')
|
|
204
|
+
.action(setDomain);
|
|
205
|
+
|
|
179
206
|
// Help
|
|
180
207
|
program
|
|
181
208
|
.command('help [command]')
|
|
@@ -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
package/src/commands/install.js
CHANGED
|
@@ -92,27 +92,31 @@ export async function install(options) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// System checks
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
if (!options.skipChecks) {
|
|
96
|
+
console.log(chalk.bold('\nš Checking system requirements...\n'));
|
|
97
|
+
const { ok, checks } = await runSystemChecks();
|
|
98
|
+
|
|
99
|
+
// Display check results
|
|
100
|
+
console.log(chalk.bold('System Requirements:'));
|
|
101
|
+
console.log(`${checks.docker.ok ? chalk.green('ā') : chalk.red('ā')} Docker: ${checks.docker.message}`);
|
|
102
|
+
console.log(`${checks.dockerCompose.ok ? chalk.green('ā') : chalk.red('ā')} Docker Compose: ${checks.dockerCompose.message}`);
|
|
103
|
+
console.log(`${checks.node.ok ? chalk.green('ā') : chalk.red('ā')} Node.js: ${checks.node.message}`);
|
|
104
|
+
|
|
105
|
+
console.log(chalk.bold('\nResources:'));
|
|
106
|
+
console.log(`${checks.diskSpace.ok ? chalk.green('ā') : chalk.red('ā')} Disk Space: ${checks.diskSpace.message}`);
|
|
107
|
+
console.log(`${checks.memory.ok ? chalk.green('ā') : chalk.red('ā')} Memory: ${checks.memory.message}`);
|
|
108
|
+
console.log(`${checks.cpu.ok ? chalk.green('ā') : chalk.yellow('ā ')} CPU: ${checks.cpu.message}`);
|
|
109
|
+
|
|
110
|
+
if (!ok) {
|
|
111
|
+
console.log(chalk.red('\nā System requirements not met. Please resolve issues above.\n'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(chalk.green('\nā All requirements met!\n'));
|
|
116
|
+
} else {
|
|
117
|
+
console.log(chalk.yellow('\nā Skipping system requirements check (--skip-checks)\n'));
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
console.log(chalk.green('\nā All requirements met!\n'));
|
|
115
|
-
|
|
116
120
|
// Interactive prompts or use options
|
|
117
121
|
let config;
|
|
118
122
|
if (options.interactive) {
|
|
@@ -151,8 +155,8 @@ export async function install(options) {
|
|
|
151
155
|
config = {
|
|
152
156
|
installDir: options.dir,
|
|
153
157
|
preset: options.preset || 'standard',
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
frontendDomain: options.frontendDomain,
|
|
159
|
+
apiDomain: options.apiDomain
|
|
156
160
|
};
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -179,9 +183,10 @@ export async function install(options) {
|
|
|
179
183
|
});
|
|
180
184
|
|
|
181
185
|
const envContent = generateEnv(credentials, {
|
|
182
|
-
frontendUrl: config.
|
|
183
|
-
apiUrl: config.
|
|
184
|
-
socketUrl: config.
|
|
186
|
+
frontendUrl: config.frontendDomain || 'http://localhost:8080',
|
|
187
|
+
apiUrl: config.apiDomain || 'http://localhost:3001',
|
|
188
|
+
socketUrl: config.apiDomain || 'http://localhost:3001',
|
|
189
|
+
frontendPort: parseInt(options.port) || 8080
|
|
185
190
|
});
|
|
186
191
|
|
|
187
192
|
writeFileSync(join(config.installDir, 'docker-compose.yml'), dockerCompose);
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import boxen from 'boxen';
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { getInstallDir } from '../utils/config.js';
|
|
8
|
+
import { backup } from './backup.js';
|
|
9
|
+
import { execInContainer } from '../utils/docker.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reset ANTE ERP system to initial state
|
|
13
|
+
* - Creates mandatory backup before resetting
|
|
14
|
+
* - Resets PostgreSQL and MongoDB databases
|
|
15
|
+
* - Prepares system for setup wizard
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} options - Command options
|
|
18
|
+
* @param {boolean} options.force - Skip confirmation prompts (backup still mandatory)
|
|
19
|
+
* @param {boolean} options.backupOnly - Create backup and exit without resetting
|
|
20
|
+
*/
|
|
21
|
+
export async function reset(options = {}) {
|
|
22
|
+
// Display danger warning
|
|
23
|
+
console.log(boxen(
|
|
24
|
+
chalk.red.bold('ā ļø DANGER: COMPLETE SYSTEM RESET ā ļø\n\n') +
|
|
25
|
+
chalk.yellow('This will:\n') +
|
|
26
|
+
chalk.white('⢠Delete ALL companies, users, and data\n') +
|
|
27
|
+
chalk.white('⢠Reset PostgreSQL database\n') +
|
|
28
|
+
chalk.white('⢠Reset MongoDB database\n') +
|
|
29
|
+
chalk.white('⢠Require running setup wizard after reset\n\n') +
|
|
30
|
+
chalk.red('This action is IRREVERSIBLE!'),
|
|
31
|
+
{
|
|
32
|
+
padding: 1,
|
|
33
|
+
margin: 1,
|
|
34
|
+
borderStyle: 'double',
|
|
35
|
+
borderColor: 'red'
|
|
36
|
+
}
|
|
37
|
+
));
|
|
38
|
+
|
|
39
|
+
// Get installation directory
|
|
40
|
+
let installDir;
|
|
41
|
+
try {
|
|
42
|
+
installDir = getInstallDir();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(chalk.red('\nā Error: ANTE installation not found.'));
|
|
45
|
+
console.log(chalk.gray('Please run this command from an ANTE installation directory.'));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const composeFile = join(installDir, 'docker-compose.yml');
|
|
50
|
+
|
|
51
|
+
// Step 1: Check if system is already reset
|
|
52
|
+
console.log(chalk.cyan('\nš Checking system status...\n'));
|
|
53
|
+
|
|
54
|
+
let accountCount;
|
|
55
|
+
try {
|
|
56
|
+
accountCount = await checkAccountCount(composeFile);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(chalk.red('ā Failed to check system status:'), error.message);
|
|
59
|
+
console.log(chalk.yellow('\nā¹ļø Make sure Docker services are running:'));
|
|
60
|
+
console.log(chalk.gray(' ante start'));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (accountCount === 0) {
|
|
65
|
+
console.log(chalk.yellow('ā¹ļø System is already reset (no accounts found).'));
|
|
66
|
+
console.log(chalk.gray('Navigate to http://localhost:9000 to run the setup wizard.'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.white(`Found ${accountCount} account(s) in the system.\n`));
|
|
71
|
+
|
|
72
|
+
// Step 2: Backup-only mode
|
|
73
|
+
if (options.backupOnly) {
|
|
74
|
+
await createBackup(installDir);
|
|
75
|
+
console.log(chalk.green('\nā
Backup created. Exiting without reset.'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 3: Mandatory backup
|
|
80
|
+
console.log(boxen(
|
|
81
|
+
chalk.yellow.bold('š¦ MANDATORY BACKUP\n\n') +
|
|
82
|
+
chalk.white('A backup will be created before resetting.\n') +
|
|
83
|
+
chalk.gray('This cannot be skipped for safety reasons.'),
|
|
84
|
+
{
|
|
85
|
+
padding: 1,
|
|
86
|
+
borderStyle: 'round',
|
|
87
|
+
borderColor: 'yellow'
|
|
88
|
+
}
|
|
89
|
+
));
|
|
90
|
+
|
|
91
|
+
let backupFile;
|
|
92
|
+
try {
|
|
93
|
+
backupFile = await createBackup(installDir);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(chalk.red('\nā Backup creation failed:'), error.message);
|
|
96
|
+
console.log(chalk.yellow('\nā¹ļø Reset aborted for safety. Your data is safe.'));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Step 4: Confirmation prompt
|
|
101
|
+
if (!options.force) {
|
|
102
|
+
console.log(chalk.red.bold('\nā ļø FINAL CONFIRMATION REQUIRED\n'));
|
|
103
|
+
|
|
104
|
+
const { confirmText } = await inquirer.prompt([{
|
|
105
|
+
type: 'input',
|
|
106
|
+
name: 'confirmText',
|
|
107
|
+
message: chalk.yellow('Type "RESET ALL DATA" exactly to confirm system reset:'),
|
|
108
|
+
}]);
|
|
109
|
+
|
|
110
|
+
if (confirmText !== 'RESET ALL DATA') {
|
|
111
|
+
console.log(chalk.gray('\nā Reset cancelled. Your data is safe.'));
|
|
112
|
+
console.log(chalk.gray(`Backup saved at: ${backupFile}`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
console.log(chalk.yellow('\nā ļø Force mode enabled - skipping confirmation prompt\n'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Step 5: Execute reset
|
|
120
|
+
console.log(chalk.cyan('\nš Resetting system...\n'));
|
|
121
|
+
|
|
122
|
+
const spinner = ora('Preparing to reset databases...').start();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Reset PostgreSQL
|
|
126
|
+
spinner.text = 'Dropping PostgreSQL database...';
|
|
127
|
+
await resetPostgreSQL(composeFile);
|
|
128
|
+
|
|
129
|
+
spinner.text = 'Creating fresh PostgreSQL database...';
|
|
130
|
+
await createPostgreSQL(composeFile);
|
|
131
|
+
|
|
132
|
+
spinner.text = 'Running database migrations...';
|
|
133
|
+
await runMigrations(installDir);
|
|
134
|
+
|
|
135
|
+
// Reset MongoDB
|
|
136
|
+
spinner.text = 'Resetting MongoDB database...';
|
|
137
|
+
await resetMongoDB(composeFile);
|
|
138
|
+
|
|
139
|
+
spinner.succeed(chalk.green('ā
System reset complete!'));
|
|
140
|
+
} catch (error) {
|
|
141
|
+
spinner.fail(chalk.red('ā Reset failed!'));
|
|
142
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
143
|
+
console.log(chalk.yellow(`\nš¾ Your backup is safe at: ${backupFile}`));
|
|
144
|
+
console.log(chalk.cyan('\nYou can restore using: ') + chalk.white(`ante restore ${backupFile}`));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Step 6: Success message
|
|
149
|
+
console.log(boxen(
|
|
150
|
+
chalk.green.bold('ā
SYSTEM RESET SUCCESSFUL\n\n') +
|
|
151
|
+
chalk.white('Your system has been reset to initial state.\n\n') +
|
|
152
|
+
chalk.cyan('Next steps:\n') +
|
|
153
|
+
chalk.white('1. Navigate to http://localhost:9000\n') +
|
|
154
|
+
chalk.white('2. Complete the setup wizard\n') +
|
|
155
|
+
chalk.white('3. Create your first admin account\n\n') +
|
|
156
|
+
chalk.gray(`Backup saved: ${backupFile}`),
|
|
157
|
+
{
|
|
158
|
+
padding: 1,
|
|
159
|
+
margin: 1,
|
|
160
|
+
borderStyle: 'round',
|
|
161
|
+
borderColor: 'green'
|
|
162
|
+
}
|
|
163
|
+
));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create a backup before reset
|
|
168
|
+
* @param {string} installDir - Installation directory
|
|
169
|
+
* @returns {Promise<string>} Path to created backup file
|
|
170
|
+
*/
|
|
171
|
+
async function createBackup(installDir) {
|
|
172
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('_').substring(0, 19);
|
|
173
|
+
const backupFile = join(installDir, 'backups', `ante-backup-before-reset-${timestamp}.tar.gz`);
|
|
174
|
+
|
|
175
|
+
console.log(chalk.cyan('š¦ Creating backup...'));
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await backup({ output: backupFile, databasesOnly: false });
|
|
179
|
+
console.log(chalk.green(`ā
Backup created: ${backupFile}\n`));
|
|
180
|
+
return backupFile;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`Backup creation failed: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check the number of accounts in the system
|
|
188
|
+
* @param {string} composeFile - Path to docker-compose.yml
|
|
189
|
+
* @returns {Promise<number>} Number of accounts
|
|
190
|
+
*/
|
|
191
|
+
async function checkAccountCount(composeFile) {
|
|
192
|
+
try {
|
|
193
|
+
const result = await execInContainer(
|
|
194
|
+
composeFile,
|
|
195
|
+
'postgres',
|
|
196
|
+
['psql', '-U', 'postgres', '-d', 'ante_db', '-t', '-c', 'SELECT COUNT(*) FROM "Account";']
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return parseInt(result.trim(), 10) || 0;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
// If Account table doesn't exist, assume fresh install
|
|
202
|
+
if (error.message.includes('does not exist')) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Drop PostgreSQL database
|
|
211
|
+
* @param {string} composeFile - Path to docker-compose.yml
|
|
212
|
+
*/
|
|
213
|
+
async function resetPostgreSQL(composeFile) {
|
|
214
|
+
await execInContainer(composeFile, 'postgres', [
|
|
215
|
+
'psql', '-U', 'postgres', '-c', 'DROP DATABASE IF EXISTS ante_db;'
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create fresh PostgreSQL database
|
|
221
|
+
* @param {string} composeFile - Path to docker-compose.yml
|
|
222
|
+
*/
|
|
223
|
+
async function createPostgreSQL(composeFile) {
|
|
224
|
+
await execInContainer(composeFile, 'postgres', [
|
|
225
|
+
'psql', '-U', 'postgres', '-c', 'CREATE DATABASE ante_db;'
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Run Prisma migrations
|
|
231
|
+
* @param {string} installDir - Installation directory
|
|
232
|
+
*/
|
|
233
|
+
async function runMigrations(installDir) {
|
|
234
|
+
try {
|
|
235
|
+
await execa('npx', ['prisma', 'migrate', 'deploy'], {
|
|
236
|
+
cwd: join(installDir, 'backend'),
|
|
237
|
+
stdio: 'pipe' // Capture output to avoid cluttering console
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw new Error(`Migration failed: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reset MongoDB database
|
|
246
|
+
* @param {string} composeFile - Path to docker-compose.yml
|
|
247
|
+
*/
|
|
248
|
+
async function resetMongoDB(composeFile) {
|
|
249
|
+
try {
|
|
250
|
+
// Try mongosh first (MongoDB 5+)
|
|
251
|
+
await execInContainer(composeFile, 'mongodb', [
|
|
252
|
+
'mongosh', '--quiet', '--eval', 'db.getSiblingDB("ante").dropDatabase()'
|
|
253
|
+
]);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// Fallback to mongo command (MongoDB 4)
|
|
256
|
+
try {
|
|
257
|
+
await execInContainer(composeFile, 'mongodb', [
|
|
258
|
+
'mongo', '--quiet', '--eval', 'db.getSiblingDB("ante").dropDatabase()'
|
|
259
|
+
]);
|
|
260
|
+
} catch (fallbackError) {
|
|
261
|
+
throw new Error(`MongoDB reset failed: ${fallbackError.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { getInstallDir } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate URL format
|
|
10
|
+
* @param {string} url - URL to validate
|
|
11
|
+
* @returns {boolean|string} True if valid, error message if invalid
|
|
12
|
+
*/
|
|
13
|
+
function validateUrl(url) {
|
|
14
|
+
if (!url || url.trim() === '') {
|
|
15
|
+
return 'URL cannot be empty';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Allow http://IP:PORT format
|
|
20
|
+
if (url.match(/^https?:\/\/.+/)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return 'URL must start with http:// or https://';
|
|
24
|
+
} catch {
|
|
25
|
+
return 'Invalid URL format';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read current value from .env file
|
|
31
|
+
* @param {string} envPath - Path to .env file
|
|
32
|
+
* @param {string} key - Environment variable key
|
|
33
|
+
* @returns {string} Current value or empty string
|
|
34
|
+
*/
|
|
35
|
+
function getEnvValue(envPath, key) {
|
|
36
|
+
try {
|
|
37
|
+
const envContent = readFileSync(envPath, 'utf8');
|
|
38
|
+
const match = envContent.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
39
|
+
return match ? match[1] : '';
|
|
40
|
+
} catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Update .env file with new values
|
|
47
|
+
* @param {string} envPath - Path to .env file
|
|
48
|
+
* @param {Object} updates - Key-value pairs to update
|
|
49
|
+
*/
|
|
50
|
+
function updateEnvFile(envPath, updates) {
|
|
51
|
+
let envContent = readFileSync(envPath, 'utf8');
|
|
52
|
+
|
|
53
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
54
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
55
|
+
if (envContent.match(regex)) {
|
|
56
|
+
// Update existing key
|
|
57
|
+
envContent = envContent.replace(regex, `${key}=${value}`);
|
|
58
|
+
} else {
|
|
59
|
+
// Add new key at the end
|
|
60
|
+
envContent += `\n${key}=${value}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
writeFileSync(envPath, envContent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Configure frontend and API domains
|
|
69
|
+
*/
|
|
70
|
+
export async function setDomain(options) {
|
|
71
|
+
try {
|
|
72
|
+
console.log(chalk.bold('\nš Configure ANTE Domains\n'));
|
|
73
|
+
|
|
74
|
+
// Detect installation
|
|
75
|
+
const installDir = getInstallDir();
|
|
76
|
+
const envPath = join(installDir, '.env');
|
|
77
|
+
const composeFile = join(installDir, 'docker-compose.yml');
|
|
78
|
+
|
|
79
|
+
console.log(chalk.gray(`Installation: ${installDir}\n`));
|
|
80
|
+
|
|
81
|
+
let frontendUrl, apiUrl;
|
|
82
|
+
|
|
83
|
+
if (options.interactive !== false) {
|
|
84
|
+
// Show current values
|
|
85
|
+
const currentFrontendUrl = getEnvValue(envPath, 'FRONTEND_URL');
|
|
86
|
+
const currentApiUrl = getEnvValue(envPath, 'API_URL');
|
|
87
|
+
|
|
88
|
+
if (currentFrontendUrl) {
|
|
89
|
+
console.log(chalk.gray(`Current Frontend URL: ${currentFrontendUrl}`));
|
|
90
|
+
}
|
|
91
|
+
if (currentApiUrl) {
|
|
92
|
+
console.log(chalk.gray(`Current API URL: ${currentApiUrl}\n`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Interactive prompts
|
|
96
|
+
const answers = await inquirer.prompt([
|
|
97
|
+
{
|
|
98
|
+
type: 'input',
|
|
99
|
+
name: 'frontendUrl',
|
|
100
|
+
message: 'Frontend domain/URL (e.g., https://app.example.com or http://139.59.110.232:8080):',
|
|
101
|
+
default: currentFrontendUrl || 'http://localhost:8080',
|
|
102
|
+
validate: validateUrl
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'input',
|
|
106
|
+
name: 'apiUrl',
|
|
107
|
+
message: 'API domain/URL (e.g., https://api.example.com or http://139.59.110.232:3001):',
|
|
108
|
+
default: currentApiUrl || 'http://localhost:3001',
|
|
109
|
+
validate: validateUrl
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
frontendUrl = answers.frontendUrl;
|
|
114
|
+
apiUrl = answers.apiUrl;
|
|
115
|
+
} else {
|
|
116
|
+
// Non-interactive mode
|
|
117
|
+
frontendUrl = options.frontend;
|
|
118
|
+
apiUrl = options.api;
|
|
119
|
+
|
|
120
|
+
if (!frontendUrl || !apiUrl) {
|
|
121
|
+
console.log(chalk.red('Error: --frontend and --api are required in non-interactive mode'));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const frontendValidation = validateUrl(frontendUrl);
|
|
126
|
+
const apiValidation = validateUrl(apiUrl);
|
|
127
|
+
|
|
128
|
+
if (frontendValidation !== true) {
|
|
129
|
+
console.log(chalk.red(`Error: Invalid frontend URL - ${frontendValidation}`));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (apiValidation !== true) {
|
|
134
|
+
console.log(chalk.red(`Error: Invalid API URL - ${apiValidation}`));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Update .env file
|
|
140
|
+
console.log(chalk.gray('\nš Updating configuration...\n'));
|
|
141
|
+
|
|
142
|
+
updateEnvFile(envPath, {
|
|
143
|
+
FRONTEND_URL: frontendUrl,
|
|
144
|
+
API_URL: apiUrl,
|
|
145
|
+
SOCKET_URL: apiUrl // WebSocket uses same URL as API
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log(chalk.green('ā Configuration updated'));
|
|
149
|
+
|
|
150
|
+
// Restart frontend container
|
|
151
|
+
console.log(chalk.gray('\nš Restarting frontend container...\n'));
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await execa('docker', ['compose', '-f', composeFile, 'restart', 'frontend']);
|
|
155
|
+
console.log(chalk.green('ā Frontend restarted successfully'));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.log(chalk.yellow('ā Could not restart frontend automatically'));
|
|
158
|
+
console.log(chalk.gray('Run: ante restart frontend'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Show summary
|
|
162
|
+
console.log(chalk.bold.green('\nā Domain configuration complete!\n'));
|
|
163
|
+
console.log(chalk.cyan('Frontend:'), chalk.white(frontendUrl));
|
|
164
|
+
console.log(chalk.cyan('API: '), chalk.white(apiUrl));
|
|
165
|
+
console.log(chalk.cyan('WebSocket:'), chalk.white(apiUrl));
|
|
166
|
+
|
|
167
|
+
console.log(chalk.gray('\nš” Access your installation at:'));
|
|
168
|
+
console.log(chalk.white(` ${frontendUrl}\n`));
|
|
169
|
+
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(chalk.red('\nā Failed to set domain:'), error.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|