ante-erp-cli 1.7.4 → 1.8.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/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.8.0] - 2025-11-01
11
+
12
+ ### Added
13
+ - **Multi-frontend installation support** - Install Gate and Guardian apps alongside Main Frontend
14
+ - Interactive checkbox prompt to select which frontends to install
15
+ - Gate App (School/Gate attendance) - Port 8081
16
+ - Guardian App (Parent portal) - Port 8082
17
+ - Company ID configuration for multi-tenant apps
18
+ - Conditional service deployment in docker-compose based on selection
19
+ - Updated success message to display all installed frontend URLs
20
+
21
+ ### Changed
22
+ - **GitHub workflow enhancements** - Build self-hosted images for all frontends
23
+ - Added build jobs for `ante-self-hosted-frontend-gate-app`
24
+ - Added build jobs for `ante-self-hosted-frontend-guardian-app`
25
+ - Updated notification messages to include all frontend images
26
+ - Path filters now trigger on gate-app and guardian-app changes
27
+
28
+ - **Docker Compose template improvements**
29
+ - Gate and Guardian services added with proper health checks
30
+ - Environment variables configured for Next.js apps
31
+ - Port mappings: 8081:3000 (gate), 8082:3000 (guardian)
32
+ - Services depend on backend and start conditionally
33
+
34
+ - **Environment template updates**
35
+ - Added gate and guardian app URL configurations
36
+ - Company ID for multi-tenant support
37
+ - Conditional port and URL variables based on selections
38
+
10
39
  ### Fixed
11
40
  - **Misleading success messages in `clone-db` command** - Now shows accurate operation status
12
41
  - Added result tracking for Prisma generate and migrate operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@ export async function backup(options) {
15
15
  const composeFile = join(installDir, 'docker-compose.yml');
16
16
  const backupDir = join(installDir, 'backups');
17
17
 
18
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
18
+ const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
19
19
  const backupFile = options.output || join(backupDir, `ante-backup-${timestamp}.tar.gz`);
20
20
 
21
21
  const spinner = ora('Creating backup...').start();
@@ -173,8 +173,8 @@ export async function cloneDb(sourceUrl, options = {}) {
173
173
  console.log(chalk.gray('This may take several minutes depending on database size...'));
174
174
  console.log('');
175
175
 
176
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('_').split('.')[0];
177
- dumpFile = join(backupDir, `clone_backup_${timestamp}.dump`);
176
+ const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
177
+ dumpFile = join(backupDir, `clone-backup-${timestamp}.dump`);
178
178
 
179
179
  const dumpSpinner = ora('Creating database dump...').start();
180
180
  const dumpResult = await dumpDatabase(sourceInfo, dumpFile);
@@ -113,15 +113,28 @@ function showWelcome() {
113
113
  function showSuccess(installDir, credentials, config) {
114
114
  const frontendUrl = config.frontendDomain || 'http://localhost:8080';
115
115
  const apiUrl = config.apiDomain || 'http://localhost:3001';
116
+ const gateAppUrl = config.gateAppDomain || 'http://localhost:8081';
117
+ const guardianAppUrl = config.guardianAppDomain || 'http://localhost:8082';
118
+
119
+ let accessInfo = chalk.white('Access Information:') + '\n' +
120
+ chalk.gray('━'.repeat(40)) + '\n' +
121
+ chalk.cyan('Frontend Main: ') + chalk.white(frontendUrl) + '\n';
122
+
123
+ if (config.installGate) {
124
+ accessInfo += chalk.cyan('Gate App: ') + chalk.white(gateAppUrl) + '\n';
125
+ }
126
+
127
+ if (config.installGuardian) {
128
+ accessInfo += chalk.cyan('Guardian App: ') + chalk.white(guardianAppUrl) + '\n';
129
+ }
130
+
131
+ accessInfo += chalk.cyan('Backend: ') + chalk.white(apiUrl) + '\n' +
132
+ chalk.cyan('WebSocket: ') + chalk.white(apiUrl) + '\n\n';
116
133
 
117
134
  console.log(
118
135
  boxen(
119
136
  chalk.green.bold('✓ Installation Complete!') + '\n\n' +
120
- chalk.white('Access Information:') + '\n' +
121
- chalk.gray('━'.repeat(40)) + '\n' +
122
- chalk.cyan('Frontend: ') + chalk.white(frontendUrl) + '\n' +
123
- chalk.cyan('Backend: ') + chalk.white(apiUrl) + '\n' +
124
- chalk.cyan('WebSocket: ') + chalk.white(apiUrl) + '\n\n' +
137
+ accessInfo +
125
138
  chalk.yellow('⚠ IMPORTANT:') + '\n' +
126
139
  chalk.white(`Credentials saved to:\n${installDir}/installation-credentials.txt`) + '\n' +
127
140
  chalk.gray('Save this file securely!') + '\n\n' +
@@ -233,6 +246,37 @@ export async function install(options) {
233
246
  }
234
247
  ]);
235
248
 
249
+ // Frontend selection prompts
250
+ const frontendConfig = await inquirer.prompt([
251
+ {
252
+ type: 'checkbox',
253
+ name: 'frontends',
254
+ message: 'Which frontends do you want to install?',
255
+ choices: [
256
+ { name: 'Main Frontend (Required)', value: 'main', checked: true, disabled: true },
257
+ { name: 'Gate App (School/Gate attendance)', value: 'gate', checked: false },
258
+ { name: 'Guardian App (Parent portal)', value: 'guardian', checked: false }
259
+ ],
260
+ validate: (answer) => {
261
+ if (answer.length < 1) {
262
+ return 'You must select at least one frontend (Main Frontend is required).';
263
+ }
264
+ return true;
265
+ }
266
+ },
267
+ {
268
+ type: 'number',
269
+ name: 'companyId',
270
+ message: 'Company ID (for multi-tenant apps):',
271
+ default: 1,
272
+ when: (answers) => answers.frontends.includes('gate') || answers.frontends.includes('guardian'),
273
+ validate: (input) => {
274
+ if (input < 1) return 'Company ID must be at least 1';
275
+ return true;
276
+ }
277
+ }
278
+ ]);
279
+
236
280
  // Network configuration prompts
237
281
  const networkConfig = await inquirer.prompt([
238
282
  {
@@ -290,20 +334,48 @@ export async function install(options) {
290
334
  if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
291
335
  return true;
292
336
  }
337
+ },
338
+ {
339
+ type: 'number',
340
+ name: 'gateAppPort',
341
+ message: 'Gate App port:',
342
+ default: 8081,
343
+ when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('gate'),
344
+ validate: (input) => {
345
+ if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
346
+ return true;
347
+ }
348
+ },
349
+ {
350
+ type: 'number',
351
+ name: 'guardianAppPort',
352
+ message: 'Guardian App port:',
353
+ default: 8082,
354
+ when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('guardian'),
355
+ validate: (input) => {
356
+ if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
357
+ return true;
358
+ }
293
359
  }
294
360
  ]);
295
361
 
296
362
  // Build URLs based on configuration
297
- let frontendUrl, apiUrl;
363
+ let frontendUrl, apiUrl, gateAppUrl, guardianAppUrl;
298
364
  const frontendPort = networkConfig.frontendPort || 8080;
299
365
  const apiPort = networkConfig.apiPort || 3001;
366
+ const gateAppPort = networkConfig.gateAppPort || 8081;
367
+ const guardianAppPort = networkConfig.guardianAppPort || 8082;
300
368
 
301
369
  if (networkConfig.networkType === 'localhost') {
302
370
  frontendUrl = buildURL('localhost', frontendPort);
303
371
  apiUrl = buildURL('localhost', apiPort);
372
+ gateAppUrl = buildURL('localhost', gateAppPort);
373
+ guardianAppUrl = buildURL('localhost', guardianAppPort);
304
374
  } else if (networkConfig.networkType === 'ip') {
305
375
  frontendUrl = buildURL(networkConfig.publicIP, frontendPort);
306
376
  apiUrl = buildURL(networkConfig.publicIP, apiPort);
377
+ gateAppUrl = buildURL(networkConfig.publicIP, gateAppPort);
378
+ guardianAppUrl = buildURL(networkConfig.publicIP, guardianAppPort);
307
379
  } else {
308
380
  // Domain - use standard HTTP/HTTPS ports (NGINX will handle reverse proxy)
309
381
  const isHttps = await inquirer.prompt([{
@@ -329,14 +401,23 @@ export async function install(options) {
329
401
  const protocol = isHttps.useHttps ? 'https' : 'http';
330
402
  frontendUrl = `${protocol}://${networkConfig.domainName}`;
331
403
  apiUrl = `${protocol}://${apiSubdomain.subdomain}.${networkConfig.domainName}`;
404
+ gateAppUrl = `${protocol}://gate.${networkConfig.domainName}`;
405
+ guardianAppUrl = `${protocol}://guardian.${networkConfig.domainName}`;
332
406
  }
333
407
 
334
408
  config = {
335
409
  ...baseConfig,
410
+ ...frontendConfig,
336
411
  frontendDomain: frontendUrl,
337
412
  apiDomain: apiUrl,
413
+ gateAppDomain: gateAppUrl,
414
+ guardianAppDomain: guardianAppUrl,
338
415
  frontendPort,
339
- apiPort
416
+ apiPort,
417
+ gateAppPort,
418
+ guardianAppPort,
419
+ installGate: frontendConfig.frontends.includes('gate'),
420
+ installGuardian: frontendConfig.frontends.includes('guardian')
340
421
  };
341
422
  } else {
342
423
  // Non-interactive mode - use options or defaults with IP detection
@@ -384,7 +465,15 @@ export async function install(options) {
384
465
  frontendDomain,
385
466
  apiDomain,
386
467
  frontendPort: parseInt(options.port) || 8080,
387
- apiPort: 3001
468
+ apiPort: 3001,
469
+ gateAppPort: 8081,
470
+ guardianAppPort: 8082,
471
+ gateAppDomain: buildURL(detectedIP || 'localhost', 8081),
472
+ guardianAppDomain: buildURL(detectedIP || 'localhost', 8082),
473
+ companyId: 1,
474
+ installGate: false,
475
+ installGuardian: false,
476
+ frontends: ['main']
388
477
  };
389
478
  }
390
479
 
@@ -392,8 +481,17 @@ export async function install(options) {
392
481
  console.log(chalk.bold('\n📋 Installation Configuration:\n'));
393
482
  console.log(chalk.cyan(' Directory:'), chalk.white(config.installDir));
394
483
  console.log(chalk.cyan(' Preset:'), chalk.white(config.preset));
395
- console.log(chalk.cyan(' Frontend:'), chalk.white(config.frontendDomain));
484
+ console.log(chalk.cyan(' Frontend Main:'), chalk.white(config.frontendDomain));
485
+ if (config.installGate) {
486
+ console.log(chalk.cyan(' Gate App:'), chalk.white(config.gateAppDomain));
487
+ }
488
+ if (config.installGuardian) {
489
+ console.log(chalk.cyan(' Guardian App:'), chalk.white(config.guardianAppDomain));
490
+ }
396
491
  console.log(chalk.cyan(' API:'), chalk.white(config.apiDomain));
492
+ if (config.companyId) {
493
+ console.log(chalk.cyan(' Company ID:'), chalk.white(config.companyId));
494
+ }
397
495
  console.log();
398
496
 
399
497
  // Check for existing configuration files
@@ -442,14 +540,28 @@ export async function install(options) {
442
540
  task: () => {
443
541
  const dockerCompose = generateDockerCompose({
444
542
  frontendPort: config.frontendPort || 8080,
445
- backendPort: config.apiPort || 3001
543
+ backendPort: config.apiPort || 3001,
544
+ gateAppPort: config.gateAppPort || 8081,
545
+ guardianAppPort: config.guardianAppPort || 8082,
546
+ installMain: true,
547
+ installGate: config.installGate || false,
548
+ installGuardian: config.installGuardian || false,
549
+ companyId: config.companyId || 1
446
550
  });
447
551
 
448
552
  const envContent = generateEnv(credentials, {
449
553
  frontendUrl: config.frontendDomain || 'http://localhost:8080',
450
554
  apiUrl: config.apiDomain || 'http://localhost:3001',
451
555
  socketUrl: config.apiDomain || 'http://localhost:3001',
452
- frontendPort: config.frontendPort || 8080
556
+ frontendPort: config.frontendPort || 8080,
557
+ backendPort: config.apiPort || 3001,
558
+ gateAppPort: config.gateAppPort || 8081,
559
+ guardianAppPort: config.guardianAppPort || 8082,
560
+ gateAppUrl: config.gateAppDomain || 'http://localhost:8081',
561
+ guardianAppUrl: config.guardianAppDomain || 'http://localhost:8082',
562
+ companyId: config.companyId || 1,
563
+ installGate: config.installGate || false,
564
+ installGuardian: config.installGuardian || false
453
565
  });
454
566
 
455
567
  writeFileSync(join(config.installDir, 'docker-compose.yml'), dockerCompose);
@@ -46,7 +46,8 @@ export async function update(options) {
46
46
  title: 'Creating backup',
47
47
  skip: () => options.skipBackup,
48
48
  task: async () => {
49
- await backup({ output: join(installDir, 'backups', `pre-update-${Date.now()}.tar.gz`) });
49
+ const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
50
+ await backup({ output: join(installDir, 'backups', `pre-update-${timestamp}.tar.gz`) });
50
51
  }
51
52
  },
52
53
  {
@@ -6,7 +6,13 @@
6
6
  export function generateDockerCompose(options = {}) {
7
7
  const {
8
8
  frontendPort = 8080,
9
- backendPort = 3001
9
+ backendPort = 3001,
10
+ gateAppPort = 8081,
11
+ guardianAppPort = 8082,
12
+ installMain = true,
13
+ installGate = false,
14
+ installGuardian = false,
15
+ companyId = 1
10
16
  } = options;
11
17
 
12
18
  return `# ANTE ERP - Docker Compose Configuration
@@ -153,7 +159,7 @@ services:
153
159
  max-size: "50m"
154
160
  max-file: "5"
155
161
 
156
- # ANTE Frontend
162
+ # ANTE Frontend Main
157
163
  frontend:
158
164
  image: ghcr.io/gtplusnet/ante-self-hosted-frontend-main:latest
159
165
  container_name: ante-frontend
@@ -162,8 +168,8 @@ services:
162
168
  backend:
163
169
  condition: service_healthy
164
170
  environment:
165
- - API_URL=\${API_URL:-http://localhost:3001}
166
- - SOCKET_URL=\${SOCKET_URL:-http://localhost:3001}
171
+ - API_URL=\${API_URL:-http://localhost:${backendPort}}
172
+ - SOCKET_URL=\${SOCKET_URL:-http://localhost:${backendPort}}
167
173
  ports:
168
174
  - "${frontendPort}:8080"
169
175
  networks:
@@ -178,7 +184,64 @@ services:
178
184
  options:
179
185
  max-size: "10m"
180
186
  max-file: "3"
181
-
187
+ ${installGate ? `
188
+ # ANTE Gate App
189
+ gate-app:
190
+ image: ghcr.io/gtplusnet/ante-self-hosted-frontend-gate-app:latest
191
+ container_name: ante-gate-app
192
+ restart: unless-stopped
193
+ depends_on:
194
+ backend:
195
+ condition: service_healthy
196
+ environment:
197
+ - NEXT_PUBLIC_API_URL=\${API_URL:-http://localhost:${backendPort}}
198
+ - NEXT_PUBLIC_BACKEND_URL=\${API_URL:-http://localhost:${backendPort}}
199
+ - NEXT_PUBLIC_SOCKET_URL=\${SOCKET_URL:-ws://localhost:${backendPort}}
200
+ - NEXT_PUBLIC_COMPANY_ID=${companyId}
201
+ - NEXT_PUBLIC_APP_NAME=Ante Gate
202
+ ports:
203
+ - "${gateAppPort}:3000"
204
+ networks:
205
+ - ante-network
206
+ healthcheck:
207
+ test: ["CMD", "curl", "-f", "http://localhost:3000"]
208
+ interval: 30s
209
+ timeout: 10s
210
+ retries: 3
211
+ logging:
212
+ driver: "json-file"
213
+ options:
214
+ max-size: "10m"
215
+ max-file: "3"
216
+ ` : ''}${installGuardian ? `
217
+ # ANTE Guardian App
218
+ guardian-app:
219
+ image: ghcr.io/gtplusnet/ante-self-hosted-frontend-guardian-app:latest
220
+ container_name: ante-guardian-app
221
+ restart: unless-stopped
222
+ depends_on:
223
+ backend:
224
+ condition: service_healthy
225
+ environment:
226
+ - NEXT_PUBLIC_API_URL=\${API_URL:-http://localhost:${backendPort}}
227
+ - NEXT_PUBLIC_SOCKET_URL=\${SOCKET_URL:-ws://localhost:${backendPort}}
228
+ - NEXT_PUBLIC_COMPANY_ID=${companyId}
229
+ - NEXT_PUBLIC_APP_NAME=Ante Guardian
230
+ ports:
231
+ - "${guardianAppPort}:3000"
232
+ networks:
233
+ - ante-network
234
+ healthcheck:
235
+ test: ["CMD", "curl", "-f", "http://localhost:3000"]
236
+ interval: 30s
237
+ timeout: 10s
238
+ retries: 3
239
+ logging:
240
+ driver: "json-file"
241
+ options:
242
+ max-size: "10m"
243
+ max-file: "3"
244
+ ` : ''}
182
245
  volumes:
183
246
  postgres_data:
184
247
  driver: local
@@ -11,6 +11,13 @@ export function generateEnv(credentials, options = {}) {
11
11
  socketUrl = 'http://localhost:3001', // WebSocket uses same port as backend
12
12
  frontendPort = 8080,
13
13
  backendPort = 3001,
14
+ gateAppPort = 8081,
15
+ guardianAppPort = 8082,
16
+ gateAppUrl = 'http://localhost:8081',
17
+ guardianAppUrl = 'http://localhost:8082',
18
+ companyId = 1,
19
+ installGate = false,
20
+ installGuardian = false,
14
21
  smtpHost = '',
15
22
  smtpPort = 587,
16
23
  smtpUsername = '',
@@ -51,9 +58,18 @@ SOCKET_URL=${socketUrl}
51
58
  # ------------------------------------------------------------------------------
52
59
  FRONTEND_PORT=${frontendPort}
53
60
  BACKEND_PORT=${backendPort}
61
+ ${installGate ? `GATE_APP_PORT=${gateAppPort}` : '# GATE_APP_PORT=8081'}
62
+ ${installGuardian ? `GUARDIAN_APP_PORT=${guardianAppPort}` : '# GUARDIAN_APP_PORT=8082'}
54
63
  # Note: WebSocket runs on the same port as backend (BACKEND_PORT)
55
64
 
65
+ ${(installGate || installGuardian) ? `# ------------------------------------------------------------------------------
66
+ # FRONTEND APP CONFIGURATION
56
67
  # ------------------------------------------------------------------------------
68
+ COMPANY_ID=${companyId}
69
+ ${installGate ? `GATE_APP_URL=${gateAppUrl}` : '# GATE_APP_URL=http://localhost:8081'}
70
+ ${installGuardian ? `GUARDIAN_APP_URL=${guardianAppUrl}` : '# GUARDIAN_APP_URL=http://localhost:8082'}
71
+
72
+ ` : ''}# ------------------------------------------------------------------------------
57
73
  # EMAIL CONFIGURATION
58
74
  # ------------------------------------------------------------------------------
59
75
  SMTP_HOST=${smtpHost}