ante-erp-cli 1.0.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.
@@ -0,0 +1,277 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import inquirer from 'inquirer';
4
+ import ora from 'ora';
5
+ import { Listr } from 'listr2';
6
+ import { mkdirSync, writeFileSync, existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { runSystemChecks } from '../utils/validation.js';
9
+ import { generateCredentials } from '../utils/password.js';
10
+ import { saveInstallConfig, detectInstallation } from '../utils/config.js';
11
+ import { pullImages, startServices, waitForServiceHealthy } from '../utils/docker.js';
12
+ import { generateDockerCompose } from '../templates/docker-compose.yml.js';
13
+ import { generateEnv } from '../templates/env.js';
14
+
15
+ /**
16
+ * Show welcome message
17
+ */
18
+ function showWelcome() {
19
+ console.log(
20
+ boxen(
21
+ chalk.bold.cyan('ANTE ERP') + '\n' +
22
+ chalk.white('Self-Hosted Installation Tool\n') +
23
+ chalk.gray(`Version ${chalk.white('1.0.0')}`),
24
+ {
25
+ padding: 1,
26
+ margin: 1,
27
+ borderStyle: 'round',
28
+ borderColor: 'cyan'
29
+ }
30
+ )
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Show success message
36
+ */
37
+ function showSuccess(installDir, credentials) {
38
+ console.log(
39
+ boxen(
40
+ chalk.green.bold('āœ“ Installation Complete!') + '\n\n' +
41
+ chalk.white('Access Information:') + '\n' +
42
+ chalk.gray('━'.repeat(40)) + '\n' +
43
+ chalk.cyan('Frontend: ') + chalk.white('http://localhost:8080') + '\n' +
44
+ chalk.cyan('Backend: ') + chalk.white('http://localhost:3001') + '\n' +
45
+ chalk.cyan('WebSocket: ') + chalk.white('ws://localhost:4001') + '\n\n' +
46
+ chalk.yellow('⚠ IMPORTANT:') + '\n' +
47
+ chalk.white(`Credentials saved to:\n${installDir}/installation-credentials.txt`) + '\n' +
48
+ chalk.gray('Save this file securely!') + '\n\n' +
49
+ chalk.white('Next steps:') + '\n' +
50
+ chalk.gray('1. Open http://localhost:8080 in your browser') + '\n' +
51
+ chalk.gray('2. Create your admin account') + '\n' +
52
+ chalk.gray('3. Explore the documentation at https://docs.ante.ph'),
53
+ {
54
+ padding: 1,
55
+ margin: 1,
56
+ borderStyle: 'round',
57
+ borderColor: 'green'
58
+ }
59
+ )
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Install ANTE ERP
65
+ */
66
+ export async function install(options) {
67
+ try {
68
+ showWelcome();
69
+
70
+ // Check if already installed
71
+ const existing = detectInstallation();
72
+ if (existing && !options.force) {
73
+ console.log(chalk.yellow('\n⚠ ANTE is already installed at:'), chalk.white(existing));
74
+ const { continueAnyway } = await inquirer.prompt([
75
+ {
76
+ type: 'confirm',
77
+ name: 'continueAnyway',
78
+ message: 'Install anyway?',
79
+ default: false
80
+ }
81
+ ]);
82
+
83
+ if (!continueAnyway) {
84
+ console.log(chalk.gray('\nInstallation cancelled.'));
85
+ return;
86
+ }
87
+ }
88
+
89
+ // System checks
90
+ console.log(chalk.bold('\nšŸ” Checking system requirements...\n'));
91
+ const { ok, checks } = await runSystemChecks();
92
+
93
+ // Display check results
94
+ console.log(chalk.bold('System Requirements:'));
95
+ console.log(`${checks.docker.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Docker: ${checks.docker.message}`);
96
+ console.log(`${checks.dockerCompose.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Docker Compose: ${checks.dockerCompose.message}`);
97
+ console.log(`${checks.node.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Node.js: ${checks.node.message}`);
98
+
99
+ console.log(chalk.bold('\nResources:'));
100
+ console.log(`${checks.diskSpace.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Disk Space: ${checks.diskSpace.message}`);
101
+ console.log(`${checks.memory.ok ? chalk.green('āœ“') : chalk.red('āœ—')} Memory: ${checks.memory.message}`);
102
+ console.log(`${checks.cpu.ok ? chalk.green('āœ“') : chalk.yellow('⚠')} CPU: ${checks.cpu.message}`);
103
+
104
+ if (!ok) {
105
+ console.log(chalk.red('\nāœ— System requirements not met. Please resolve issues above.\n'));
106
+ process.exit(1);
107
+ }
108
+
109
+ console.log(chalk.green('\nāœ“ All requirements met!\n'));
110
+
111
+ // Interactive prompts or use options
112
+ let config;
113
+ if (options.interactive) {
114
+ config = await inquirer.prompt([
115
+ {
116
+ type: 'input',
117
+ name: 'installDir',
118
+ message: 'Installation directory:',
119
+ default: options.dir || './ante-erp'
120
+ },
121
+ {
122
+ type: 'list',
123
+ name: 'preset',
124
+ message: 'Choose installation type:',
125
+ choices: [
126
+ { name: 'Minimal (Evaluation)', value: 'minimal' },
127
+ { name: 'Standard (Recommended)', value: 'standard' },
128
+ { name: 'Enterprise (Full Features)', value: 'enterprise' }
129
+ ],
130
+ default: 'standard'
131
+ },
132
+ {
133
+ type: 'confirm',
134
+ name: 'useDomain',
135
+ message: 'Do you have a domain name?',
136
+ default: false
137
+ },
138
+ {
139
+ type: 'input',
140
+ name: 'domain',
141
+ message: 'Domain name:',
142
+ when: (answers) => answers.useDomain
143
+ }
144
+ ]);
145
+ } else {
146
+ config = {
147
+ installDir: options.dir,
148
+ preset: options.preset || 'standard',
149
+ useDomain: !!options.domain,
150
+ domain: options.domain
151
+ };
152
+ }
153
+
154
+ // Generate credentials
155
+ console.log(chalk.bold('\nšŸ” Generating secure credentials...\n'));
156
+ const credentials = generateCredentials();
157
+
158
+ // Installation tasks
159
+ const tasks = new Listr([
160
+ {
161
+ title: 'Creating installation directory',
162
+ task: () => {
163
+ mkdirSync(config.installDir, { recursive: true });
164
+ mkdirSync(join(config.installDir, 'backups'), { recursive: true });
165
+ mkdirSync(join(config.installDir, 'logs'), { recursive: true });
166
+ }
167
+ },
168
+ {
169
+ title: 'Generating configuration files',
170
+ task: () => {
171
+ const dockerCompose = generateDockerCompose({
172
+ frontendPort: parseInt(options.port) || 8080,
173
+ backendPort: 3001,
174
+ websocketPort: 4001
175
+ });
176
+
177
+ const envContent = generateEnv(credentials, {
178
+ frontendUrl: config.domain ? `https://${config.domain}` : 'http://localhost:8080',
179
+ apiUrl: config.domain ? `https://${config.domain}/api` : 'http://localhost:3001',
180
+ socketUrl: config.domain ? `wss://${config.domain}/socket.io` : 'ws://localhost:4001'
181
+ });
182
+
183
+ writeFileSync(join(config.installDir, 'docker-compose.yml'), dockerCompose);
184
+ writeFileSync(join(config.installDir, '.env'), envContent);
185
+
186
+ // Save credentials
187
+ const credentialsText = `ANTE ERP Installation Credentials
188
+ Generated: ${new Date().toISOString()}
189
+
190
+ āš ļø IMPORTANT: Keep this file secure and never commit to version control!
191
+
192
+ Database Credentials:
193
+ ━━━━━━━━━━━━━━━━━━━━
194
+ PostgreSQL Password: ${credentials.dbPassword}
195
+ Redis Password: ${credentials.redisPassword}
196
+ MongoDB Password: ${credentials.mongoPassword}
197
+
198
+ Security Keys:
199
+ ━━━━━━━━━━━━━━
200
+ JWT Secret: ${credentials.jwtSecret}
201
+ Developer Key: ${credentials.developerKey}
202
+ Encryption Key: ${credentials.encryptionKey}
203
+
204
+ Access Information:
205
+ ━━━━━━━━━━━━━━━━━━━
206
+ Frontend: http://localhost:${options.port || 8080}
207
+ Backend: http://localhost:3001
208
+ WebSocket: ws://localhost:4001
209
+
210
+ Next Steps:
211
+ ━━━━━━━━━━
212
+ 1. Access the frontend URL in your browser
213
+ 2. Create your admin account
214
+ 3. Configure your organization settings
215
+ 4. Start using ANTE ERP!
216
+
217
+ Documentation: https://docs.ante.ph
218
+ Support: support@ante.ph
219
+ `;
220
+
221
+ writeFileSync(join(config.installDir, 'installation-credentials.txt'), credentialsText);
222
+ }
223
+ },
224
+ {
225
+ title: 'Pulling Docker images',
226
+ task: async () => {
227
+ const composeFile = join(config.installDir, 'docker-compose.yml');
228
+ await pullImages(composeFile);
229
+ }
230
+ },
231
+ {
232
+ title: 'Starting services',
233
+ task: async () => {
234
+ const composeFile = join(config.installDir, 'docker-compose.yml');
235
+ await startServices(composeFile);
236
+ }
237
+ },
238
+ {
239
+ title: 'Waiting for services to be ready',
240
+ task: async () => {
241
+ const composeFile = join(config.installDir, 'docker-compose.yml');
242
+
243
+ // Wait for backend to be healthy
244
+ const backendHealthy = await waitForServiceHealthy(composeFile, 'backend', 120);
245
+ if (!backendHealthy) {
246
+ throw new Error('Backend service failed to start');
247
+ }
248
+
249
+ // Give it a few more seconds
250
+ await new Promise(resolve => setTimeout(resolve, 5000));
251
+ }
252
+ }
253
+ ], {
254
+ rendererOptions: {
255
+ collapseSubtasks: false
256
+ }
257
+ });
258
+
259
+ await tasks.run();
260
+
261
+ // Save installation configuration
262
+ saveInstallConfig({
263
+ installPath: config.installDir,
264
+ version: '1.0.0',
265
+ domain: config.domain
266
+ });
267
+
268
+ // Show success message
269
+ showSuccess(config.installDir, credentials);
270
+
271
+ } catch (error) {
272
+ console.error(chalk.red('\nāœ— Installation failed:'), error.message);
273
+ console.error(chalk.gray('\nFor help, visit: https://docs.ante.ph/self-hosting/troubleshooting\n'));
274
+ process.exit(1);
275
+ }
276
+ }
277
+
@@ -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,227 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import inquirer from 'inquirer';
4
+ import { join, basename } from 'path';
5
+ import { existsSync, mkdirSync, readdirSync } from 'fs';
6
+ import { execa } from 'execa';
7
+ import { getInstallDir } from '../utils/config.js';
8
+ import { execInContainer, stopServices, startServices } from '../utils/docker.js';
9
+
10
+ /**
11
+ * Restore ANTE ERP from backup
12
+ */
13
+ export async function restore(backupFile, options = {}) {
14
+ try {
15
+ const installDir = getInstallDir();
16
+ const composeFile = join(installDir, 'docker-compose.yml');
17
+
18
+ // Validate backup file
19
+ if (!backupFile) {
20
+ const backupDir = join(installDir, 'backups');
21
+
22
+ if (!existsSync(backupDir)) {
23
+ console.log(chalk.red('āœ— No backups directory found'));
24
+ console.log(chalk.gray(`Expected: ${backupDir}`));
25
+ return;
26
+ }
27
+
28
+ const backups = readdirSync(backupDir)
29
+ .filter(f => f.endsWith('.tar.gz'))
30
+ .sort()
31
+ .reverse();
32
+
33
+ if (backups.length === 0) {
34
+ console.log(chalk.red('āœ— No backups found'));
35
+ console.log(chalk.gray('Create a backup first: ante backup'));
36
+ return;
37
+ }
38
+
39
+ // Prompt user to select backup
40
+ const { selectedBackup } = await inquirer.prompt([
41
+ {
42
+ type: 'list',
43
+ name: 'selectedBackup',
44
+ message: 'Select backup to restore:',
45
+ choices: backups.map(b => ({
46
+ name: `${b} (${getSizeSync(join(backupDir, b))})`,
47
+ value: b
48
+ }))
49
+ }
50
+ ]);
51
+
52
+ backupFile = join(backupDir, selectedBackup);
53
+ }
54
+
55
+ if (!existsSync(backupFile)) {
56
+ console.log(chalk.red('āœ— Backup file not found:'), backupFile);
57
+ return;
58
+ }
59
+
60
+ // Warning and confirmation
61
+ console.log(chalk.yellow('\n⚠ WARNING: This will restore ANTE from backup'));
62
+ console.log(chalk.gray('Current data will be replaced with backup data'));
63
+ console.log(chalk.white('\nBackup file:'), chalk.cyan(basename(backupFile)));
64
+
65
+ if (!options.force) {
66
+ const { confirm } = await inquirer.prompt([
67
+ {
68
+ type: 'confirm',
69
+ name: 'confirm',
70
+ message: 'Continue with restore?',
71
+ default: false
72
+ }
73
+ ]);
74
+
75
+ if (!confirm) {
76
+ console.log(chalk.gray('\nRestore cancelled.'));
77
+ return;
78
+ }
79
+ }
80
+
81
+ const spinner = ora('Preparing restore...').start();
82
+
83
+ try {
84
+ // Create temporary directory
85
+ const tempDir = join(installDir, '.restore-temp');
86
+ if (existsSync(tempDir)) {
87
+ await execa('rm', ['-rf', tempDir]);
88
+ }
89
+ mkdirSync(tempDir, { recursive: true });
90
+
91
+ // Extract backup
92
+ spinner.text = 'Extracting backup...';
93
+ await execa('tar', ['-xzf', backupFile, '-C', tempDir]);
94
+
95
+ // Stop services
96
+ spinner.text = 'Stopping services...';
97
+ await stopServices(composeFile);
98
+
99
+ // Wait for services to stop
100
+ await new Promise(resolve => setTimeout(resolve, 3000));
101
+
102
+ // Start only database services for restore
103
+ spinner.text = 'Starting database services...';
104
+ await startServices(composeFile, ['postgres', 'mongodb', 'redis']);
105
+
106
+ // Wait for databases to be ready
107
+ await new Promise(resolve => setTimeout(resolve, 5000));
108
+
109
+ // Restore PostgreSQL
110
+ spinner.text = 'Restoring PostgreSQL database...';
111
+ const pgDumpFile = join(tempDir, 'postgres.dump');
112
+ if (existsSync(pgDumpFile)) {
113
+ // Drop and recreate database
114
+ await execInContainer(
115
+ composeFile,
116
+ 'postgres',
117
+ ['psql', '-U', 'ante', '-c', 'DROP DATABASE IF EXISTS ante_db;']
118
+ );
119
+ await execInContainer(
120
+ composeFile,
121
+ 'postgres',
122
+ ['psql', '-U', 'ante', '-c', 'CREATE DATABASE ante_db;']
123
+ );
124
+
125
+ // Restore from dump
126
+ const dumpContent = await execa('cat', [pgDumpFile]);
127
+ await execInContainer(
128
+ composeFile,
129
+ 'postgres',
130
+ ['pg_restore', '-U', 'ante', '-d', 'ante_db'],
131
+ { input: dumpContent.stdout }
132
+ );
133
+ }
134
+
135
+ // Restore MongoDB
136
+ spinner.text = 'Restoring MongoDB database...';
137
+ const mongoDumpDir = join(tempDir, 'mongodb');
138
+ if (existsSync(mongoDumpDir)) {
139
+ await execa('docker', [
140
+ 'cp',
141
+ mongoDumpDir,
142
+ 'ante-mongodb:/tmp/restore'
143
+ ]);
144
+
145
+ await execInContainer(
146
+ composeFile,
147
+ 'mongodb',
148
+ ['mongorestore', '--drop', '--db', 'ante', '/tmp/restore/ante']
149
+ );
150
+ }
151
+
152
+ // Restore uploaded files
153
+ spinner.text = 'Restoring uploaded files...';
154
+ const uploadsDir = join(tempDir, 'uploads');
155
+ if (existsSync(uploadsDir)) {
156
+ await execa('docker', [
157
+ 'cp',
158
+ uploadsDir,
159
+ 'ante-backend:/app/'
160
+ ]);
161
+ }
162
+
163
+ // Restore configuration files
164
+ spinner.text = 'Restoring configuration...';
165
+ const envBackup = join(tempDir, 'env.backup');
166
+ if (existsSync(envBackup)) {
167
+ await execa('cp', [envBackup, join(installDir, '.env')]);
168
+ }
169
+
170
+ const composeBackup = join(tempDir, 'docker-compose.yml');
171
+ if (existsSync(composeBackup)) {
172
+ await execa('cp', [composeBackup, composeFile]);
173
+ }
174
+
175
+ // Start all services
176
+ spinner.text = 'Starting all services...';
177
+ await startServices(composeFile);
178
+
179
+ // Cleanup
180
+ spinner.text = 'Cleaning up...';
181
+ await execa('rm', ['-rf', tempDir]);
182
+
183
+ spinner.succeed(chalk.green('Restore completed successfully!'));
184
+
185
+ console.log(chalk.white('\nāœ“ PostgreSQL database restored'));
186
+ console.log(chalk.white('āœ“ MongoDB database restored'));
187
+ console.log(chalk.white('āœ“ Uploaded files restored'));
188
+ console.log(chalk.white('āœ“ Configuration restored'));
189
+ console.log(chalk.white('āœ“ All services started'));
190
+
191
+ console.log(chalk.cyan('\nANTE is now running with restored data'));
192
+
193
+ } catch (error) {
194
+ spinner.fail(chalk.red('Restore failed'));
195
+ console.error(chalk.red('\nError:'), error.message);
196
+
197
+ // Try to start services anyway
198
+ try {
199
+ await startServices(composeFile);
200
+ } catch (startError) {
201
+ console.error(chalk.red('Failed to start services:'), startError.message);
202
+ }
203
+
204
+ throw error;
205
+ }
206
+
207
+ } catch (error) {
208
+ console.error(chalk.red('\nRestore failed:'), error.message);
209
+ process.exit(1);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Get file size in human-readable format
215
+ */
216
+ function getSizeSync(filePath) {
217
+ try {
218
+ const stats = require('fs').statSync(filePath);
219
+ const bytes = stats.size;
220
+ const sizes = ['B', 'KB', 'MB', 'GB'];
221
+ if (bytes === 0) return '0 B';
222
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
223
+ return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
224
+ } catch (error) {
225
+ return 'Unknown';
226
+ }
227
+ }
@@ -0,0 +1,72 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { join } from 'path';
4
+ import { getInstallDir } from '../utils/config.js';
5
+ import { startServices, stopServices, restartServices } from '../utils/docker.js';
6
+
7
+ /**
8
+ * Start ANTE services
9
+ */
10
+ export async function start(options) {
11
+ try {
12
+ const installDir = getInstallDir();
13
+ const composeFile = join(installDir, 'docker-compose.yml');
14
+
15
+ const spinner = ora('Starting ANTE services...').start();
16
+
17
+ const services = options.service ? [options.service] : [];
18
+ await startServices(composeFile, services);
19
+
20
+ spinner.succeed(services.length ? `${options.service} started` : 'All services started');
21
+ console.log(chalk.gray('\nRun "ante status" to check service status\n'));
22
+
23
+ } catch (error) {
24
+ console.error(chalk.red('\nError:'), error.message);
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Stop ANTE services
31
+ */
32
+ export async function stop(options) {
33
+ try {
34
+ const installDir = getInstallDir();
35
+ const composeFile = join(installDir, 'docker-compose.yml');
36
+
37
+ const spinner = ora('Stopping ANTE services...').start();
38
+
39
+ const services = options.service ? [options.service] : [];
40
+ await stopServices(composeFile, services);
41
+
42
+ spinner.succeed(services.length ? `${options.service} stopped` : 'All services stopped');
43
+ console.log();
44
+
45
+ } catch (error) {
46
+ console.error(chalk.red('\nError:'), error.message);
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Restart ANTE services
53
+ */
54
+ export async function restart(options) {
55
+ try {
56
+ const installDir = getInstallDir();
57
+ const composeFile = join(installDir, 'docker-compose.yml');
58
+
59
+ const spinner = ora('Restarting ANTE services...').start();
60
+
61
+ const services = options.service ? [options.service] : [];
62
+ await restartServices(composeFile, services);
63
+
64
+ spinner.succeed(services.length ? `${options.service} restarted` : 'All services restarted');
65
+ console.log(chalk.gray('\nRun "ante status" to check service status\n'));
66
+
67
+ } catch (error) {
68
+ console.error(chalk.red('\nError:'), error.message);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { join } from 'path';
4
+ import { getInstallDir } from '../utils/config.js';
5
+ import { getServiceStatus } from '../utils/docker.js';
6
+
7
+ /**
8
+ * Show ANTE service status
9
+ */
10
+ export async function status() {
11
+ try {
12
+ const installDir = getInstallDir();
13
+ const composeFile = join(installDir, 'docker-compose.yml');
14
+
15
+ console.log(chalk.bold('\nšŸ“Š ANTE ERP Status\n'));
16
+
17
+ const services = await getServiceStatus(composeFile);
18
+
19
+ if (services.length === 0) {
20
+ console.log(chalk.yellow('No services running'));
21
+ console.log(chalk.gray('Run "ante start" to start services\n'));
22
+ return;
23
+ }
24
+
25
+ const table = new Table({
26
+ head: [
27
+ chalk.cyan('Service'),
28
+ chalk.cyan('Status'),
29
+ chalk.cyan('Health'),
30
+ chalk.cyan('Ports')
31
+ ],
32
+ style: {
33
+ head: [],
34
+ border: ['gray']
35
+ }
36
+ });
37
+
38
+ for (const service of services) {
39
+ const statusIcon = service.State === 'running' ? chalk.green('ā—') : chalk.red('ā—');
40
+ const healthIcon = service.Health === 'healthy' ? chalk.green('āœ“') :
41
+ service.Health === 'unhealthy' ? chalk.red('āœ—') :
42
+ chalk.yellow('ā—‹');
43
+
44
+ table.push([
45
+ service.Service,
46
+ `${statusIcon} ${service.State}`,
47
+ service.Health ? `${healthIcon} ${service.Health}` : chalk.gray('N/A'),
48
+ service.Publishers?.map(p => `${p.PublishedPort}→${p.TargetPort}`).join(', ') || chalk.gray('internal')
49
+ ]);
50
+ }
51
+
52
+ console.log(table.toString());
53
+ console.log();
54
+
55
+ const allHealthy = services.every(s => s.State === 'running');
56
+
57
+ if (allHealthy) {
58
+ console.log(chalk.green('āœ“ All services running\n'));
59
+ } else {
60
+ console.log(chalk.yellow('⚠ Some services are not running'));
61
+ console.log(chalk.gray('Run "ante restart" to restart services\n'));
62
+ }
63
+
64
+ } catch (error) {
65
+ console.error(chalk.red('Error:'), error.message);
66
+ process.exit(1);
67
+ }
68
+ }
69
+