ante-erp-cli 1.11.30 → 1.11.31

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/bin/ante-cli.js CHANGED
@@ -241,10 +241,13 @@ dbCmd
241
241
  dbCmd
242
242
  .command('expose')
243
243
  .description('Expose database ports for external access (DEVELOPMENT ONLY)')
244
- .option('--postgres', 'Expose only PostgreSQL (5432)')
245
- .option('--redis', 'Expose only Redis (6379)')
246
- .option('--mongodb', 'Expose only MongoDB (27017)')
244
+ .option('--postgres', 'Expose only PostgreSQL')
245
+ .option('--redis', 'Expose only Redis')
246
+ .option('--mongodb', 'Expose only MongoDB')
247
247
  .option('--all', 'Expose all databases (default)', true)
248
+ .option('--postgres-port <port>', 'Custom host port for PostgreSQL (default: random)')
249
+ .option('--redis-port <port>', 'Custom host port for Redis (default: random)')
250
+ .option('--mongodb-port <port>', 'Custom host port for MongoDB (default: random)')
248
251
  .option('--force', 'Skip confirmation prompts')
249
252
  .option('--no-restart', 'Don\'t restart services automatically')
250
253
  .action(exposeDbPorts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.11.30",
3
+ "version": "1.11.31",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -169,11 +169,14 @@ export async function exposeDbPorts(options) {
169
169
  );
170
170
 
171
171
  // Add port bindings
172
- spinner.start('Adding port bindings...');
173
- const { exposed } = await exposeDatabasePorts(composeFile, {
172
+ spinner.start('Finding available ports and adding bindings...');
173
+ const { exposed, portMappings } = await exposeDatabasePorts(composeFile, {
174
174
  postgres: databases.includes('postgres'),
175
175
  redis: databases.includes('redis'),
176
176
  mongodb: databases.includes('mongodb'),
177
+ postgresPort: options.postgresPort ? parseInt(options.postgresPort) : null,
178
+ redisPort: options.redisPort ? parseInt(options.redisPort) : null,
179
+ mongodbPort: options.mongodbPort ? parseInt(options.mongodbPort) : null,
177
180
  });
178
181
  spinner.succeed(chalk.green('Port bindings added'));
179
182
 
@@ -195,10 +198,26 @@ export async function exposeDbPorts(options) {
195
198
  console.log(chalk.green.bold('✓ Database ports exposed!'));
196
199
  console.log();
197
200
 
201
+ // Display assigned ports
202
+ console.log(chalk.cyan.bold('Assigned Ports:'));
203
+ Object.entries(portMappings).forEach(([db, mapping]) => {
204
+ const dbInfo = {
205
+ postgres: 'PostgreSQL',
206
+ redis: 'Redis',
207
+ mongodb: 'MongoDB',
208
+ };
209
+ console.log(
210
+ chalk.cyan(
211
+ ` ${dbInfo[db]}: Host Port ${chalk.bold(mapping.hostPort)} → Container Port ${mapping.containerPort}`
212
+ )
213
+ );
214
+ });
215
+ console.log();
216
+
198
217
  // Display connection strings
199
218
  const serverIP = getServerIP();
200
219
  const credentials = getCredentials(installDir);
201
- const examples = getConnectionExamples(exposed, serverIP, credentials);
220
+ const examples = getConnectionExamples(portMappings, serverIP, credentials);
202
221
  console.log(chalk.cyan.bold('Connection strings:'));
203
222
  Object.values(examples).forEach((ex) => {
204
223
  console.log(chalk.cyan(` ${ex.name}: ${ex.example}`));
@@ -206,7 +225,7 @@ export async function exposeDbPorts(options) {
206
225
  console.log();
207
226
 
208
227
  // Display firewall configuration guide
209
- const rules = getFirewallRules(exposed);
228
+ const rules = getFirewallRules(portMappings);
210
229
  console.log(chalk.yellow.bold('💡 Configure firewall for security:'));
211
230
  rules.forEach((rule) => {
212
231
  console.log(chalk.yellow(` ${rule}`));
@@ -250,7 +269,9 @@ export async function secureDbPorts(options) {
250
269
  console.log(chalk.cyan('Currently exposed database ports:'));
251
270
  exposedDbs.forEach((db) => {
252
271
  console.log(
253
- chalk.cyan(` • ${status[db].name}: ${status[db].port}`)
272
+ chalk.cyan(
273
+ ` • ${status[db].name}: ${status[db].mapping} (Host: ${status[db].hostPort})`
274
+ )
254
275
  );
255
276
  });
256
277
  console.log();
@@ -332,12 +353,14 @@ export async function dbPortStatus() {
332
353
 
333
354
  let anyExposed = false;
334
355
 
335
- Object.entries(status).forEach(([db, info]) => {
356
+ Object.entries(status).forEach(([_db, info]) => {
336
357
  const statusIcon = info.exposed ? chalk.yellow('🔓') : chalk.green('🔒');
337
358
  const statusText = info.exposed
338
359
  ? chalk.yellow('EXPOSED')
339
360
  : chalk.green('SECURED');
340
- const portText = info.exposed ? chalk.cyan(`Port: ${info.port}`) : '';
361
+ const portText = info.exposed
362
+ ? chalk.cyan(`Port Mapping: ${info.mapping} (Host: ${info.hostPort})`)
363
+ : '';
341
364
 
342
365
  console.log(
343
366
  `${statusIcon} ${chalk.bold(info.name)}: ${statusText} ${portText}`
@@ -1,7 +1,57 @@
1
1
  import fs from 'fs/promises';
2
2
  import yaml from 'js-yaml';
3
- import path from 'path';
4
- import chalk from 'chalk';
3
+ import net from 'net';
4
+
5
+ /**
6
+ * Check if a port is available
7
+ * @param {number} port - Port number to check
8
+ * @returns {Promise<boolean>} True if port is available
9
+ */
10
+ async function isPortAvailable(port) {
11
+ return new Promise((resolve) => {
12
+ const server = net.createServer();
13
+
14
+ server.once('error', (err) => {
15
+ if (err.code === 'EADDRINUSE') {
16
+ resolve(false);
17
+ } else {
18
+ resolve(false);
19
+ }
20
+ });
21
+
22
+ server.once('listening', () => {
23
+ server.close();
24
+ resolve(true);
25
+ });
26
+
27
+ server.listen(port, '0.0.0.0');
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Find an available port within a range
33
+ * @param {number} preferredPort - Preferred port number (optional)
34
+ * @param {number} rangeStart - Start of port range (default: 49152)
35
+ * @param {number} rangeEnd - End of port range (default: 65535)
36
+ * @param {number} maxAttempts - Maximum number of attempts (default: 100)
37
+ * @returns {Promise<number>} Available port number
38
+ */
39
+ async function findAvailablePort(preferredPort = null, rangeStart = 49152, rangeEnd = 65535, maxAttempts = 100) {
40
+ // If preferred port is provided and available, use it
41
+ if (preferredPort && await isPortAvailable(preferredPort)) {
42
+ return preferredPort;
43
+ }
44
+
45
+ // Try to find a random available port
46
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
47
+ const randomPort = Math.floor(Math.random() * (rangeEnd - rangeStart + 1)) + rangeStart;
48
+ if (await isPortAvailable(randomPort)) {
49
+ return randomPort;
50
+ }
51
+ }
52
+
53
+ throw new Error(`Could not find an available port after ${maxAttempts} attempts`);
54
+ }
5
55
 
6
56
  /**
7
57
  * Database port configurations
@@ -10,20 +60,23 @@ const DATABASE_PORTS = {
10
60
  postgres: {
11
61
  name: 'PostgreSQL',
12
62
  service: 'postgres',
13
- port: '5432:5432',
14
- connectionExample: (host, password) => `postgresql://ante:${password}@${host}:5432/ante_db`,
63
+ containerPort: 5432,
64
+ defaultHostPort: 5432,
65
+ connectionExample: (host, hostPort, password) => `postgresql://ante:${password}@${host}:${hostPort}/ante_db`,
15
66
  },
16
67
  redis: {
17
68
  name: 'Redis',
18
69
  service: 'redis',
19
- port: '6379:6379',
20
- connectionExample: (host, password) => `redis://${host}:6379 (password: ${password})`,
70
+ containerPort: 6379,
71
+ defaultHostPort: 6379,
72
+ connectionExample: (host, hostPort, password) => `redis://${host}:${hostPort} (password: ${password})`,
21
73
  },
22
74
  mongodb: {
23
75
  name: 'MongoDB',
24
76
  service: 'mongodb',
25
- port: '27017:27017',
26
- connectionExample: (host, password) => `mongodb://ante:${password}@${host}:27017/ante`,
77
+ containerPort: 27017,
78
+ defaultHostPort: 27017,
79
+ connectionExample: (host, hostPort, password) => `mongodb://ante:${password}@${host}:${hostPort}/ante`,
27
80
  },
28
81
  };
29
82
 
@@ -69,10 +122,18 @@ async function writeComposeFile(composeFile, doc) {
69
122
  * Add port bindings to database services
70
123
  * @param {string} composeFile - Path to docker-compose.yml
71
124
  * @param {Object} options - Port exposure options
72
- * @returns {Promise<Object>} Result object with exposed services
125
+ * @returns {Promise<Object>} Result object with exposed services and their ports
73
126
  */
74
127
  export async function exposeDatabasePorts(composeFile, options = {}) {
75
- const { postgres = true, redis = true, mongodb = true } = options;
128
+ const {
129
+ postgres = true,
130
+ redis = true,
131
+ mongodb = true,
132
+ postgresPort = null,
133
+ redisPort = null,
134
+ mongodbPort = null,
135
+ } = options;
136
+
76
137
  const doc = await readComposeFile(composeFile);
77
138
 
78
139
  if (!doc.services) {
@@ -80,6 +141,7 @@ export async function exposeDatabasePorts(composeFile, options = {}) {
80
141
  }
81
142
 
82
143
  const exposed = [];
144
+ const portMappings = {};
83
145
 
84
146
  // Add PostgreSQL port binding
85
147
  if (postgres && doc.services.postgres) {
@@ -88,10 +150,16 @@ export async function exposeDatabasePorts(composeFile, options = {}) {
88
150
  }
89
151
  // Remove existing port binding if any
90
152
  doc.services.postgres.ports = doc.services.postgres.ports.filter(
91
- (p) => !p.toString().includes('5432')
153
+ (p) => !p.toString().includes(DATABASE_PORTS.postgres.containerPort.toString())
92
154
  );
93
- doc.services.postgres.ports.push(DATABASE_PORTS.postgres.port);
155
+
156
+ // Find available port
157
+ const hostPort = await findAvailablePort(postgresPort);
158
+ const portMapping = `${hostPort}:${DATABASE_PORTS.postgres.containerPort}`;
159
+
160
+ doc.services.postgres.ports.push(portMapping);
94
161
  exposed.push('postgres');
162
+ portMappings.postgres = { hostPort, containerPort: DATABASE_PORTS.postgres.containerPort };
95
163
  }
96
164
 
97
165
  // Add Redis port binding
@@ -101,10 +169,16 @@ export async function exposeDatabasePorts(composeFile, options = {}) {
101
169
  }
102
170
  // Remove existing port binding if any
103
171
  doc.services.redis.ports = doc.services.redis.ports.filter(
104
- (p) => !p.toString().includes('6379')
172
+ (p) => !p.toString().includes(DATABASE_PORTS.redis.containerPort.toString())
105
173
  );
106
- doc.services.redis.ports.push(DATABASE_PORTS.redis.port);
174
+
175
+ // Find available port
176
+ const hostPort = await findAvailablePort(redisPort);
177
+ const portMapping = `${hostPort}:${DATABASE_PORTS.redis.containerPort}`;
178
+
179
+ doc.services.redis.ports.push(portMapping);
107
180
  exposed.push('redis');
181
+ portMappings.redis = { hostPort, containerPort: DATABASE_PORTS.redis.containerPort };
108
182
  }
109
183
 
110
184
  // Add MongoDB port binding
@@ -114,15 +188,21 @@ export async function exposeDatabasePorts(composeFile, options = {}) {
114
188
  }
115
189
  // Remove existing port binding if any
116
190
  doc.services.mongodb.ports = doc.services.mongodb.ports.filter(
117
- (p) => !p.toString().includes('27017')
191
+ (p) => !p.toString().includes(DATABASE_PORTS.mongodb.containerPort.toString())
118
192
  );
119
- doc.services.mongodb.ports.push(DATABASE_PORTS.mongodb.port);
193
+
194
+ // Find available port
195
+ const hostPort = await findAvailablePort(mongodbPort);
196
+ const portMapping = `${hostPort}:${DATABASE_PORTS.mongodb.containerPort}`;
197
+
198
+ doc.services.mongodb.ports.push(portMapping);
120
199
  exposed.push('mongodb');
200
+ portMappings.mongodb = { hostPort, containerPort: DATABASE_PORTS.mongodb.containerPort };
121
201
  }
122
202
 
123
203
  await writeComposeFile(composeFile, doc);
124
204
 
125
- return { exposed, doc };
205
+ return { exposed, portMappings, doc };
126
206
  }
127
207
 
128
208
  /**
@@ -180,7 +260,7 @@ export async function secureDatabasePorts(composeFile) {
180
260
  /**
181
261
  * Get current port exposure status for database services
182
262
  * @param {string} composeFile - Path to docker-compose.yml
183
- * @returns {Promise<Object>} Status object
263
+ * @returns {Promise<Object>} Status object with actual port mappings
184
264
  */
185
265
  export async function getDatabasePortStatus(composeFile) {
186
266
  try {
@@ -194,38 +274,77 @@ export async function getDatabasePortStatus(composeFile) {
194
274
 
195
275
  // Check PostgreSQL
196
276
  if (doc.services.postgres) {
197
- const hasPort = doc.services.postgres.ports?.some((p) =>
198
- p.toString().includes('5432')
277
+ const portBinding = doc.services.postgres.ports?.find((p) =>
278
+ p.toString().includes(`:${DATABASE_PORTS.postgres.containerPort}`)
199
279
  );
200
- status.postgres = {
201
- name: DATABASE_PORTS.postgres.name,
202
- exposed: hasPort,
203
- port: hasPort ? '5432' : null,
204
- };
280
+ if (portBinding) {
281
+ const [hostPort, containerPort] = portBinding.toString().split(':');
282
+ status.postgres = {
283
+ name: DATABASE_PORTS.postgres.name,
284
+ exposed: true,
285
+ hostPort: parseInt(hostPort),
286
+ containerPort: parseInt(containerPort),
287
+ mapping: portBinding.toString(),
288
+ };
289
+ } else {
290
+ status.postgres = {
291
+ name: DATABASE_PORTS.postgres.name,
292
+ exposed: false,
293
+ hostPort: null,
294
+ containerPort: DATABASE_PORTS.postgres.containerPort,
295
+ mapping: null,
296
+ };
297
+ }
205
298
  }
206
299
 
207
300
  // Check Redis
208
301
  if (doc.services.redis) {
209
- const hasPort = doc.services.redis.ports?.some((p) =>
210
- p.toString().includes('6379')
302
+ const portBinding = doc.services.redis.ports?.find((p) =>
303
+ p.toString().includes(`:${DATABASE_PORTS.redis.containerPort}`)
211
304
  );
212
- status.redis = {
213
- name: DATABASE_PORTS.redis.name,
214
- exposed: hasPort,
215
- port: hasPort ? '6379' : null,
216
- };
305
+ if (portBinding) {
306
+ const [hostPort, containerPort] = portBinding.toString().split(':');
307
+ status.redis = {
308
+ name: DATABASE_PORTS.redis.name,
309
+ exposed: true,
310
+ hostPort: parseInt(hostPort),
311
+ containerPort: parseInt(containerPort),
312
+ mapping: portBinding.toString(),
313
+ };
314
+ } else {
315
+ status.redis = {
316
+ name: DATABASE_PORTS.redis.name,
317
+ exposed: false,
318
+ hostPort: null,
319
+ containerPort: DATABASE_PORTS.redis.containerPort,
320
+ mapping: null,
321
+ };
322
+ }
217
323
  }
218
324
 
219
325
  // Check MongoDB
220
326
  if (doc.services.mongodb) {
221
- const hasPort = doc.services.mongodb.ports?.some((p) =>
222
- p.toString().includes('27017')
327
+ const portBinding = doc.services.mongodb.ports?.find((p) =>
328
+ p.toString().includes(`:${DATABASE_PORTS.mongodb.containerPort}`)
223
329
  );
224
- status.mongodb = {
225
- name: DATABASE_PORTS.mongodb.name,
226
- exposed: hasPort,
227
- port: hasPort ? '27017' : null,
228
- };
330
+ if (portBinding) {
331
+ const [hostPort, containerPort] = portBinding.toString().split(':');
332
+ status.mongodb = {
333
+ name: DATABASE_PORTS.mongodb.name,
334
+ exposed: true,
335
+ hostPort: parseInt(hostPort),
336
+ containerPort: parseInt(containerPort),
337
+ mapping: portBinding.toString(),
338
+ };
339
+ } else {
340
+ status.mongodb = {
341
+ name: DATABASE_PORTS.mongodb.name,
342
+ exposed: false,
343
+ hostPort: null,
344
+ containerPort: DATABASE_PORTS.mongodb.containerPort,
345
+ mapping: null,
346
+ };
347
+ }
229
348
  }
230
349
 
231
350
  return status;
@@ -236,20 +355,23 @@ export async function getDatabasePortStatus(composeFile) {
236
355
 
237
356
  /**
238
357
  * Get connection examples for exposed databases
239
- * @param {Array<string>} exposed - List of exposed database services
358
+ * @param {Object} portMappings - Port mappings object from exposeDatabasePorts
240
359
  * @param {string} host - Server hostname or IP
241
360
  * @param {Object} credentials - Database credentials { postgres, redis, mongodb }
242
- * @returns {Object} Connection examples
361
+ * @returns {Object} Connection examples with actual host ports
243
362
  */
244
- export function getConnectionExamples(exposed, host = 'SERVER_IP', credentials = {}) {
363
+ export function getConnectionExamples(portMappings, host = 'SERVER_IP', credentials = {}) {
245
364
  const examples = {};
246
365
 
247
- exposed.forEach((db) => {
366
+ Object.keys(portMappings).forEach((db) => {
248
367
  if (DATABASE_PORTS[db]) {
249
368
  const password = credentials[db] || 'PASSWORD';
369
+ const { hostPort } = portMappings[db];
250
370
  examples[db] = {
251
371
  name: DATABASE_PORTS[db].name,
252
- example: DATABASE_PORTS[db].connectionExample(host, password),
372
+ hostPort,
373
+ containerPort: portMappings[db].containerPort,
374
+ example: DATABASE_PORTS[db].connectionExample(host, hostPort, password),
253
375
  };
254
376
  }
255
377
  });
@@ -259,16 +381,16 @@ export function getConnectionExamples(exposed, host = 'SERVER_IP', credentials =
259
381
 
260
382
  /**
261
383
  * Get firewall rules for database ports
262
- * @param {Array<string>} exposed - List of exposed database services
263
- * @returns {Array<string>} UFW command examples
384
+ * @param {Object} portMappings - Port mappings object from exposeDatabasePorts
385
+ * @returns {Array<string>} UFW command examples with actual host ports
264
386
  */
265
- export function getFirewallRules(exposed) {
387
+ export function getFirewallRules(portMappings) {
266
388
  const rules = [];
267
389
 
268
- exposed.forEach((db) => {
390
+ Object.keys(portMappings).forEach((db) => {
269
391
  if (DATABASE_PORTS[db]) {
270
- const port = DATABASE_PORTS[db].port.split(':')[0];
271
- rules.push(`ufw allow from YOUR_IP to any port ${port}`);
392
+ const { hostPort } = portMappings[db];
393
+ rules.push(`ufw allow from YOUR_IP to any port ${hostPort}`);
272
394
  }
273
395
  });
274
396