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 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 <domain>', 'Domain name for SSL')
40
- .option('--email <email>', 'Email for SSL certificate')
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(reset);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -92,27 +92,31 @@ export async function install(options) {
92
92
  }
93
93
 
94
94
  // System checks
95
- console.log(chalk.bold('\nšŸ” Checking system requirements...\n'));
96
- const { ok, checks } = await runSystemChecks();
97
-
98
- // Display check results
99
- console.log(chalk.bold('System Requirements:'));
100
- console.log(`${checks.docker.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Docker: ${checks.docker.message}`);
101
- console.log(`${checks.dockerCompose.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Docker Compose: ${checks.dockerCompose.message}`);
102
- console.log(`${checks.node.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Node.js: ${checks.node.message}`);
103
-
104
- console.log(chalk.bold('\nResources:'));
105
- console.log(`${checks.diskSpace.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Disk Space: ${checks.diskSpace.message}`);
106
- console.log(`${checks.memory.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Memory: ${checks.memory.message}`);
107
- console.log(`${checks.cpu.ok ? chalk.green('āœ“') : chalk.yellow('⚠')} CPU: ${checks.cpu.message}`);
108
-
109
- if (!ok) {
110
- console.log(chalk.red('\nāœ— System requirements not met. Please resolve issues above.\n'));
111
- process.exit(1);
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
- useDomain: !!options.domain,
155
- domain: options.domain
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.domain ? `https://${config.domain}` : 'http://localhost:8080',
183
- apiUrl: config.domain ? `https://${config.domain}/api` : 'http://localhost:3001',
184
- socketUrl: config.domain ? `https://${config.domain}` : 'http://localhost:3001'
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
+ }
@@ -158,6 +158,9 @@ services:
158
158
  depends_on:
159
159
  backend:
160
160
  condition: service_healthy
161
+ environment:
162
+ - API_URL=\${API_URL:-http://localhost:3001}
163
+ - SOCKET_URL=\${SOCKET_URL:-http://localhost:3001}
161
164
  ports:
162
165
  - "${frontendPort}:8080"
163
166
  networks: