ante-erp-cli 1.11.16 → 1.11.17
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/package.json +1 -1
- package/src/commands/install.js +173 -529
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import boxen from 'boxen';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
3
|
import ora from 'ora';
|
|
5
4
|
import { Listr } from 'listr2';
|
|
6
5
|
import { execa } from 'execa';
|
|
7
|
-
import { mkdirSync, writeFileSync, existsSync, renameSync } from 'fs';
|
|
6
|
+
import { mkdirSync, writeFileSync, existsSync, renameSync, readFileSync } from 'fs';
|
|
8
7
|
import { join, dirname } from 'path';
|
|
9
8
|
import { fileURLToPath } from 'url';
|
|
10
9
|
import { runSystemChecks, checkAndInstallDocker } from '../utils/validation.js';
|
|
11
10
|
import { generateCredentials } from '../utils/password.js';
|
|
12
11
|
import { saveInstallConfig, detectInstallation } from '../utils/config.js';
|
|
13
|
-
import {
|
|
12
|
+
import { pullImagesSilent, startServicesSilent, waitForServiceHealthy, runMigrations } from '../utils/docker.js';
|
|
14
13
|
import { generateDockerCompose } from '../templates/docker-compose.yml.js';
|
|
15
14
|
import { generateEnv } from '../templates/env.js';
|
|
16
|
-
import { detectPublicIPWithFeedback, buildURL
|
|
15
|
+
import { detectPublicIPWithFeedback, buildURL } from '../utils/network.js';
|
|
17
16
|
import { configureNginx, requiresNginx } from '../utils/nginx.js';
|
|
18
17
|
|
|
19
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -42,50 +41,14 @@ function backupEnvFile(envPath) {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {
|
|
47
|
-
* @
|
|
44
|
+
* Format step title with numbering
|
|
45
|
+
* @param {number} step - Current step number
|
|
46
|
+
* @param {number} total - Total number of steps
|
|
47
|
+
* @param {string} description - Step description
|
|
48
|
+
* @returns {string} Formatted title
|
|
48
49
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!existsSync(envPath)) {
|
|
53
|
-
return true; // No existing config, safe to continue
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
console.log(chalk.yellow('\n⚠ Existing configuration detected!'));
|
|
57
|
-
console.log(chalk.white(` Found: ${envPath}`));
|
|
58
|
-
console.log(chalk.gray(' This file contains API URLs, credentials, and other settings.\n'));
|
|
59
|
-
|
|
60
|
-
const { action } = await inquirer.prompt([
|
|
61
|
-
{
|
|
62
|
-
type: 'list',
|
|
63
|
-
name: 'action',
|
|
64
|
-
message: 'How would you like to proceed?',
|
|
65
|
-
choices: [
|
|
66
|
-
{ name: 'Backup old config and create new (Recommended)', value: 'backup' },
|
|
67
|
-
{ name: 'Overwrite with new configuration', value: 'overwrite' },
|
|
68
|
-
{ name: 'Cancel installation', value: 'cancel' }
|
|
69
|
-
],
|
|
70
|
-
default: 'backup'
|
|
71
|
-
}
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
if (action === 'cancel') {
|
|
75
|
-
console.log(chalk.gray('\nInstallation cancelled.\n'));
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (action === 'backup') {
|
|
80
|
-
const backupPath = backupEnvFile(envPath);
|
|
81
|
-
if (backupPath) {
|
|
82
|
-
console.log(chalk.green(` ✓ Configuration backed up to: ${backupPath}\n`));
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
console.log(chalk.yellow(' ⚠ Existing configuration will be overwritten\n'));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return true;
|
|
50
|
+
function formatStepTitle(step, total, description) {
|
|
51
|
+
return `[${step}/${total}] ${description}`;
|
|
89
52
|
}
|
|
90
53
|
|
|
91
54
|
/**
|
|
@@ -165,34 +128,43 @@ function showSuccess(installDir, credentials, config) {
|
|
|
165
128
|
}
|
|
166
129
|
|
|
167
130
|
/**
|
|
168
|
-
* Install ANTE ERP
|
|
131
|
+
* Install ANTE ERP (Non-Interactive Mode)
|
|
169
132
|
*/
|
|
170
133
|
export async function install(options) {
|
|
171
134
|
try {
|
|
172
135
|
showWelcome();
|
|
173
|
-
|
|
136
|
+
|
|
137
|
+
console.log(chalk.bold('\n🚀 ANTE ERP Installation\n'));
|
|
138
|
+
|
|
174
139
|
// Check if already installed
|
|
175
140
|
const existing = detectInstallation();
|
|
176
141
|
if (existing && !options.force) {
|
|
177
|
-
console.log(chalk.yellow('
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
type: 'confirm',
|
|
181
|
-
name: 'continueAnyway',
|
|
182
|
-
message: 'Install anyway?',
|
|
183
|
-
default: false
|
|
184
|
-
}
|
|
185
|
-
]);
|
|
186
|
-
|
|
187
|
-
if (!continueAnyway) {
|
|
188
|
-
console.log(chalk.gray('\nInstallation cancelled.'));
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
142
|
+
console.log(chalk.yellow('⚠ ANTE is already installed at:'), chalk.white(existing));
|
|
143
|
+
console.log(chalk.gray('Proceeding with re-installation...\n'));
|
|
191
144
|
}
|
|
192
|
-
|
|
193
|
-
//
|
|
145
|
+
|
|
146
|
+
// Calculate total steps
|
|
147
|
+
let totalSteps = 10; // Base steps
|
|
148
|
+
|
|
149
|
+
// Pre-calculate step numbers
|
|
150
|
+
let currentStep = 0;
|
|
151
|
+
const stepSystemCheck = !options.skipChecks ? ++currentStep : null;
|
|
152
|
+
const stepNetworkDetect = ++currentStep;
|
|
153
|
+
const stepGenerateCredentials = ++currentStep;
|
|
154
|
+
const stepCreateDir = ++currentStep;
|
|
155
|
+
const stepGenerateConfig = ++currentStep;
|
|
156
|
+
const stepPullImages = ++currentStep;
|
|
157
|
+
const stepStartServices = ++currentStep;
|
|
158
|
+
const stepWaitServices = ++currentStep;
|
|
159
|
+
const stepInitDatabase = ++currentStep;
|
|
160
|
+
const stepRunMigrations = ++currentStep;
|
|
161
|
+
|
|
162
|
+
// Start installation
|
|
163
|
+
let stepNum = 0;
|
|
164
|
+
|
|
165
|
+
// Step: System checks
|
|
194
166
|
if (!options.skipChecks) {
|
|
195
|
-
console.log(chalk.
|
|
167
|
+
console.log(chalk.cyan(formatStepTitle(stepSystemCheck, totalSteps, 'Checking system requirements')));
|
|
196
168
|
|
|
197
169
|
// Check Docker first and offer installation if needed
|
|
198
170
|
const dockerResult = await checkAndInstallDocker(!options.skipDockerInstall);
|
|
@@ -203,394 +175,93 @@ export async function install(options) {
|
|
|
203
175
|
// Update checks with Docker installation result
|
|
204
176
|
checks.docker = dockerResult;
|
|
205
177
|
|
|
206
|
-
// Display check results
|
|
207
|
-
console.log(chalk.
|
|
208
|
-
console.log(`${checks.docker.ok ? chalk.green('✓') : chalk.red('✗')} Docker: ${checks.docker.message}`);
|
|
178
|
+
// Display check results (compact)
|
|
179
|
+
console.log(` ${checks.docker.ok ? chalk.green('✓') : chalk.red('✗')} Docker: ${checks.docker.message}`);
|
|
209
180
|
if (checks.docker.installed) {
|
|
210
|
-
console.log(chalk.green('
|
|
181
|
+
console.log(chalk.green(' ↳ Docker was installed during this setup'));
|
|
211
182
|
}
|
|
212
|
-
console.log(
|
|
213
|
-
console.log(
|
|
214
|
-
|
|
215
|
-
console.log(chalk.
|
|
216
|
-
console.log(`${checks.diskSpace.ok ? chalk.green('✓') : chalk.red('✗')} Disk Space: ${checks.diskSpace.message}`);
|
|
217
|
-
console.log(`${checks.memory.ok ? chalk.green('✓') : chalk.red('✗')} Memory: ${checks.memory.message}`);
|
|
218
|
-
console.log(`${checks.cpu.ok ? chalk.green('✓') : chalk.yellow('⚠')} CPU: ${checks.cpu.message}`);
|
|
183
|
+
console.log(` ${checks.dockerCompose.ok ? chalk.green('✓') : chalk.red('✗')} Docker Compose: ${checks.dockerCompose.message}`);
|
|
184
|
+
console.log(` ${checks.node.ok ? chalk.green('✓') : chalk.red('✗')} Node.js: ${checks.node.message}`);
|
|
185
|
+
console.log(` ${checks.diskSpace.ok ? chalk.green('✓') : chalk.red('✗')} Disk Space: ${checks.diskSpace.message}`);
|
|
186
|
+
console.log(` ${checks.memory.ok ? chalk.green('✓') : chalk.red('✗')} Memory: ${checks.memory.message}`);
|
|
219
187
|
|
|
220
188
|
if (!ok || !checks.docker.ok) {
|
|
221
189
|
console.log(chalk.red('\n✗ System requirements not met. Please resolve issues above.\n'));
|
|
222
190
|
process.exit(1);
|
|
223
191
|
}
|
|
224
192
|
|
|
225
|
-
console.log(chalk.green('
|
|
226
|
-
} else {
|
|
227
|
-
console.log(chalk.yellow('\n⚠ Skipping system requirements check (--skip-checks)\n'));
|
|
193
|
+
console.log(chalk.green(`✓ ${formatStepTitle(stepSystemCheck, totalSteps, 'System requirements met')}\n`));
|
|
228
194
|
}
|
|
229
195
|
|
|
230
|
-
//
|
|
231
|
-
console.log(chalk.
|
|
232
|
-
const spinner = ora();
|
|
196
|
+
// Step: Network detection
|
|
197
|
+
console.log(chalk.cyan(formatStepTitle(stepNetworkDetect, totalSteps, 'Detecting network configuration')));
|
|
198
|
+
const spinner = ora({ text: 'Detecting public IP...', prefixText: ' ' }).start();
|
|
233
199
|
const detectedIP = await detectPublicIPWithFeedback(spinner);
|
|
234
200
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
{ name: 'Main Frontend (Required)', value: 'main', checked: true, disabled: true },
|
|
267
|
-
{ name: 'Gate App (School/Gate attendance)', value: 'gate', checked: false },
|
|
268
|
-
{ name: 'Guardian App (Parent portal)', value: 'guardian', checked: false },
|
|
269
|
-
{ name: 'Facial Web (Employee face recognition)', value: 'facial', checked: false },
|
|
270
|
-
{ name: 'POS App (Point of Sale)', value: 'pos', checked: false }
|
|
271
|
-
],
|
|
272
|
-
validate: (answer) => {
|
|
273
|
-
if (answer.length < 1) {
|
|
274
|
-
return 'You must select at least one frontend (Main Frontend is required).';
|
|
275
|
-
}
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
type: 'number',
|
|
281
|
-
name: 'companyId',
|
|
282
|
-
message: 'Company ID (for multi-tenant apps):',
|
|
283
|
-
default: 1,
|
|
284
|
-
when: (answers) => answers.frontends.includes('gate') || answers.frontends.includes('guardian') || answers.frontends.includes('facial') || answers.frontends.includes('pos'),
|
|
285
|
-
validate: (input) => {
|
|
286
|
-
if (input < 1) return 'Company ID must be at least 1';
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
]);
|
|
291
|
-
|
|
292
|
-
// Network configuration prompts
|
|
293
|
-
const networkConfig = await inquirer.prompt([
|
|
294
|
-
{
|
|
295
|
-
type: 'list',
|
|
296
|
-
name: 'networkType',
|
|
297
|
-
message: 'How will users access this installation?',
|
|
298
|
-
choices: [
|
|
299
|
-
{ name: 'Local development (localhost)', value: 'localhost' },
|
|
300
|
-
{ name: 'Public IP address', value: 'ip' },
|
|
301
|
-
{ name: 'Domain name', value: 'domain' }
|
|
302
|
-
],
|
|
303
|
-
default: detectedIP ? 'ip' : 'localhost'
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
type: 'input',
|
|
307
|
-
name: 'publicIP',
|
|
308
|
-
message: 'Enter the public IP address:',
|
|
309
|
-
default: detectedIP || '',
|
|
310
|
-
when: (answers) => answers.networkType === 'ip',
|
|
311
|
-
validate: (input) => {
|
|
312
|
-
if (!input) return 'IP address is required';
|
|
313
|
-
if (!isValidIPv4(input)) return 'Please enter a valid IPv4 address';
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
type: 'input',
|
|
319
|
-
name: 'domainName',
|
|
320
|
-
message: 'Enter your domain name (e.g., erp.example.com):',
|
|
321
|
-
when: (answers) => answers.networkType === 'domain',
|
|
322
|
-
validate: (input) => {
|
|
323
|
-
if (!input) return 'Domain name is required';
|
|
324
|
-
if (!isValidDomain(input)) return 'Please enter a valid domain name';
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
type: 'number',
|
|
330
|
-
name: 'frontendPort',
|
|
331
|
-
message: 'Frontend port:',
|
|
332
|
-
default: 8080,
|
|
333
|
-
when: (answers) => answers.networkType !== 'domain',
|
|
334
|
-
validate: (input) => {
|
|
335
|
-
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
type: 'number',
|
|
341
|
-
name: 'apiPort',
|
|
342
|
-
message: 'API port:',
|
|
343
|
-
default: 3001,
|
|
344
|
-
when: (answers) => answers.networkType !== 'domain',
|
|
345
|
-
validate: (input) => {
|
|
346
|
-
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
type: 'number',
|
|
352
|
-
name: 'gateAppPort',
|
|
353
|
-
message: 'Gate App port:',
|
|
354
|
-
default: 8081,
|
|
355
|
-
when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('gate'),
|
|
356
|
-
validate: (input) => {
|
|
357
|
-
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
type: 'number',
|
|
363
|
-
name: 'guardianAppPort',
|
|
364
|
-
message: 'Guardian App port:',
|
|
365
|
-
default: 8082,
|
|
366
|
-
when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('guardian'),
|
|
367
|
-
validate: (input) => {
|
|
368
|
-
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
369
|
-
return true;
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
type: 'number',
|
|
374
|
-
name: 'facialWebPort',
|
|
375
|
-
message: 'Facial Web port:',
|
|
376
|
-
default: 8083,
|
|
377
|
-
when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('facial'),
|
|
378
|
-
validate: (input) => {
|
|
379
|
-
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
type: 'number',
|
|
385
|
-
name: 'posAppPort',
|
|
386
|
-
message: 'POS App port:',
|
|
387
|
-
default: 8084,
|
|
388
|
-
when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('pos'),
|
|
389
|
-
validate: (input) => {
|
|
390
|
-
if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
|
|
391
|
-
return true;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
]);
|
|
395
|
-
|
|
396
|
-
// Build URLs based on configuration
|
|
397
|
-
let frontendUrl, apiUrl, gateAppUrl, guardianAppUrl, facialWebUrl, posAppUrl;
|
|
398
|
-
const frontendPort = networkConfig.frontendPort || 8080;
|
|
399
|
-
const apiPort = networkConfig.apiPort || 3001;
|
|
400
|
-
const gateAppPort = networkConfig.gateAppPort || 8081;
|
|
401
|
-
const guardianAppPort = networkConfig.guardianAppPort || 8082;
|
|
402
|
-
const facialWebPort = networkConfig.facialWebPort || 8083;
|
|
403
|
-
const posAppPort = networkConfig.posAppPort || 8084;
|
|
404
|
-
|
|
405
|
-
if (networkConfig.networkType === 'localhost') {
|
|
406
|
-
frontendUrl = buildURL('localhost', frontendPort);
|
|
407
|
-
apiUrl = buildURL('localhost', apiPort);
|
|
408
|
-
gateAppUrl = buildURL('localhost', gateAppPort);
|
|
409
|
-
guardianAppUrl = buildURL('localhost', guardianAppPort);
|
|
410
|
-
facialWebUrl = buildURL('localhost', facialWebPort);
|
|
411
|
-
posAppUrl = buildURL('localhost', posAppPort);
|
|
412
|
-
} else if (networkConfig.networkType === 'ip') {
|
|
413
|
-
frontendUrl = buildURL(networkConfig.publicIP, frontendPort);
|
|
414
|
-
apiUrl = buildURL(networkConfig.publicIP, apiPort);
|
|
415
|
-
gateAppUrl = buildURL(networkConfig.publicIP, gateAppPort);
|
|
416
|
-
guardianAppUrl = buildURL(networkConfig.publicIP, guardianAppPort);
|
|
417
|
-
facialWebUrl = buildURL(networkConfig.publicIP, facialWebPort);
|
|
418
|
-
posAppUrl = buildURL(networkConfig.publicIP, posAppPort);
|
|
419
|
-
} else {
|
|
420
|
-
// Domain - use standard HTTP/HTTPS ports (NGINX will handle reverse proxy)
|
|
421
|
-
const isHttps = await inquirer.prompt([{
|
|
422
|
-
type: 'confirm',
|
|
423
|
-
name: 'useHttps',
|
|
424
|
-
message: 'Is SSL/TLS configured for this domain (e.g., via Cloudflare)?',
|
|
425
|
-
default: true
|
|
426
|
-
}]);
|
|
427
|
-
|
|
428
|
-
// Ask for API subdomain
|
|
429
|
-
const apiSubdomain = await inquirer.prompt([{
|
|
430
|
-
type: 'input',
|
|
431
|
-
name: 'subdomain',
|
|
432
|
-
message: 'API subdomain (e.g., "api" for api.example.com):',
|
|
433
|
-
default: 'api',
|
|
434
|
-
validate: (input) => {
|
|
435
|
-
if (!input) return 'Subdomain is required';
|
|
436
|
-
if (!/^[a-z0-9-]+$/.test(input)) return 'Invalid subdomain format';
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
439
|
-
}]);
|
|
440
|
-
|
|
441
|
-
const protocol = isHttps.useHttps ? 'https' : 'http';
|
|
442
|
-
frontendUrl = `${protocol}://${networkConfig.domainName}`;
|
|
443
|
-
apiUrl = `${protocol}://${apiSubdomain.subdomain}.${networkConfig.domainName}`;
|
|
444
|
-
gateAppUrl = `${protocol}://gate.${networkConfig.domainName}`;
|
|
445
|
-
guardianAppUrl = `${protocol}://guardian.${networkConfig.domainName}`;
|
|
446
|
-
facialWebUrl = `${protocol}://facial.${networkConfig.domainName}`;
|
|
447
|
-
posAppUrl = `${protocol}://pos.${networkConfig.domainName}`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
config = {
|
|
451
|
-
...baseConfig,
|
|
452
|
-
...frontendConfig,
|
|
453
|
-
frontendDomain: frontendUrl,
|
|
454
|
-
apiDomain: apiUrl,
|
|
455
|
-
gateAppDomain: gateAppUrl,
|
|
456
|
-
guardianAppDomain: guardianAppUrl,
|
|
457
|
-
facialAppDomain: facialWebUrl,
|
|
458
|
-
posAppDomain: posAppUrl,
|
|
459
|
-
frontendPort,
|
|
460
|
-
apiPort,
|
|
461
|
-
gateAppPort,
|
|
462
|
-
guardianAppPort,
|
|
463
|
-
facialWebPort,
|
|
464
|
-
posAppPort,
|
|
465
|
-
installGate: frontendConfig.frontends.includes('gate'),
|
|
466
|
-
installGuardian: frontendConfig.frontends.includes('guardian'),
|
|
467
|
-
installFacial: frontendConfig.frontends.includes('facial'),
|
|
468
|
-
installPos: frontendConfig.frontends.includes('pos')
|
|
469
|
-
};
|
|
470
|
-
} else {
|
|
471
|
-
// Non-interactive mode - use options or defaults with IP detection
|
|
472
|
-
let frontendDomain = options.frontendDomain;
|
|
473
|
-
let apiDomain = options.apiDomain;
|
|
474
|
-
|
|
475
|
-
// If no domains provided and IP detected, use detected IP
|
|
476
|
-
if (!frontendDomain && detectedIP) {
|
|
477
|
-
frontendDomain = buildURL(detectedIP, parseInt(options.port) || 8080);
|
|
478
|
-
console.log(chalk.cyan(' → Using detected IP for frontend:'), chalk.white(frontendDomain));
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (!apiDomain && detectedIP) {
|
|
482
|
-
apiDomain = buildURL(detectedIP, 3001);
|
|
483
|
-
console.log(chalk.cyan(' → Using detected IP for API:'), chalk.white(apiDomain));
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// If still no domain and no IP detected, prompt user
|
|
487
|
-
if (!frontendDomain || !apiDomain) {
|
|
488
|
-
console.log(chalk.yellow('\n⚠ Could not auto-detect public IP address.'));
|
|
489
|
-
console.log(chalk.white('Please provide domain/IP configuration manually:\n'));
|
|
490
|
-
|
|
491
|
-
const manualConfig = await inquirer.prompt([
|
|
492
|
-
{
|
|
493
|
-
type: 'input',
|
|
494
|
-
name: 'hostIp',
|
|
495
|
-
message: 'Enter public IP address or domain:',
|
|
496
|
-
validate: (input) => {
|
|
497
|
-
if (!input) return 'IP address or domain is required';
|
|
498
|
-
if (!isValidIPv4(input) && !isValidDomain(input)) {
|
|
499
|
-
return 'Please enter a valid IP address or domain name';
|
|
500
|
-
}
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
]);
|
|
505
|
-
|
|
506
|
-
frontendDomain = buildURL(manualConfig.hostIp, parseInt(options.port) || 8080);
|
|
507
|
-
apiDomain = buildURL(manualConfig.hostIp, 3001);
|
|
508
|
-
}
|
|
201
|
+
// Build configuration (non-interactive, enterprise preset with all frontends)
|
|
202
|
+
const host = detectedIP || 'localhost';
|
|
203
|
+
const frontendPort = parseInt(options.port) || 8080;
|
|
204
|
+
const apiPort = 3001;
|
|
205
|
+
const gateAppPort = 8081;
|
|
206
|
+
const guardianAppPort = 8082;
|
|
207
|
+
const facialWebPort = 8083;
|
|
208
|
+
const posAppPort = 8084;
|
|
209
|
+
|
|
210
|
+
const config = {
|
|
211
|
+
installDir: options.dir || './ante-erp',
|
|
212
|
+
preset: 'enterprise', // Always install all features
|
|
213
|
+
frontendDomain: buildURL(host, frontendPort),
|
|
214
|
+
apiDomain: buildURL(host, apiPort),
|
|
215
|
+
gateAppDomain: buildURL(host, gateAppPort),
|
|
216
|
+
guardianAppDomain: buildURL(host, guardianAppPort),
|
|
217
|
+
facialAppDomain: buildURL(host, facialWebPort),
|
|
218
|
+
posAppDomain: buildURL(host, posAppPort),
|
|
219
|
+
frontendPort,
|
|
220
|
+
apiPort,
|
|
221
|
+
gateAppPort,
|
|
222
|
+
guardianAppPort,
|
|
223
|
+
facialWebPort,
|
|
224
|
+
posAppPort,
|
|
225
|
+
companyId: 1,
|
|
226
|
+
installGate: true, // Always install all frontends
|
|
227
|
+
installGuardian: true,
|
|
228
|
+
installFacial: true,
|
|
229
|
+
installPos: true,
|
|
230
|
+
frontends: ['main', 'gate', 'guardian', 'facial', 'pos']
|
|
231
|
+
};
|
|
509
232
|
|
|
510
|
-
|
|
511
|
-
const installAllFrontends = options.withAllFrontends;
|
|
512
|
-
const installGate = installAllFrontends || options.withGate;
|
|
513
|
-
const installGuardian = installAllFrontends || options.withGuardian;
|
|
514
|
-
const installFacial = installAllFrontends || options.withFacial;
|
|
515
|
-
const installPos = installAllFrontends || options.withPos;
|
|
516
|
-
|
|
517
|
-
// Build frontends array
|
|
518
|
-
const frontends = ['main'];
|
|
519
|
-
if (installGate) frontends.push('gate');
|
|
520
|
-
if (installGuardian) frontends.push('guardian');
|
|
521
|
-
if (installFacial) frontends.push('facial');
|
|
522
|
-
if (installPos) frontends.push('pos');
|
|
523
|
-
|
|
524
|
-
config = {
|
|
525
|
-
installDir: options.dir,
|
|
526
|
-
preset: options.preset || 'standard',
|
|
527
|
-
frontendDomain,
|
|
528
|
-
apiDomain,
|
|
529
|
-
frontendPort: parseInt(options.port) || 8080,
|
|
530
|
-
apiPort: 3001,
|
|
531
|
-
gateAppPort: 8081,
|
|
532
|
-
guardianAppPort: 8082,
|
|
533
|
-
facialWebPort: 8083,
|
|
534
|
-
posAppPort: 8084,
|
|
535
|
-
gateAppDomain: buildURL(detectedIP || 'localhost', 8081),
|
|
536
|
-
guardianAppDomain: buildURL(detectedIP || 'localhost', 8082),
|
|
537
|
-
facialAppDomain: buildURL(detectedIP || 'localhost', 8083),
|
|
538
|
-
posAppDomain: buildURL(detectedIP || 'localhost', 8084),
|
|
539
|
-
companyId: 1,
|
|
540
|
-
installGate,
|
|
541
|
-
installGuardian,
|
|
542
|
-
installFacial,
|
|
543
|
-
installPos,
|
|
544
|
-
frontends
|
|
545
|
-
};
|
|
546
|
-
}
|
|
233
|
+
console.log(chalk.green(`✓ ${formatStepTitle(stepNetworkDetect, totalSteps, `Network detected: ${host}`)}\n`));
|
|
547
234
|
|
|
548
|
-
// Display configuration summary
|
|
549
|
-
console.log(chalk.bold('
|
|
235
|
+
// Display configuration summary (compact)
|
|
236
|
+
console.log(chalk.bold('📋 Installation Configuration:\n'));
|
|
550
237
|
console.log(chalk.cyan(' Directory:'), chalk.white(config.installDir));
|
|
551
|
-
console.log(chalk.cyan(' Preset:'), chalk.white(
|
|
238
|
+
console.log(chalk.cyan(' Preset:'), chalk.white('Enterprise (All Features)'));
|
|
552
239
|
console.log(chalk.cyan(' Frontend Main:'), chalk.white(config.frontendDomain));
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
console.log(chalk.cyan(' Guardian App:'), chalk.white(config.guardianAppDomain));
|
|
558
|
-
}
|
|
559
|
-
if (config.installFacial) {
|
|
560
|
-
console.log(chalk.cyan(' Facial Web:'), chalk.white(config.facialAppDomain));
|
|
561
|
-
}
|
|
562
|
-
if (config.installPos) {
|
|
563
|
-
console.log(chalk.cyan(' POS App:'), chalk.white(config.posAppDomain));
|
|
564
|
-
}
|
|
240
|
+
console.log(chalk.cyan(' Gate App:'), chalk.white(config.gateAppDomain));
|
|
241
|
+
console.log(chalk.cyan(' Guardian App:'), chalk.white(config.guardianAppDomain));
|
|
242
|
+
console.log(chalk.cyan(' Facial Web:'), chalk.white(config.facialAppDomain));
|
|
243
|
+
console.log(chalk.cyan(' POS App:'), chalk.white(config.posAppDomain));
|
|
565
244
|
console.log(chalk.cyan(' API:'), chalk.white(config.apiDomain));
|
|
566
|
-
if (config.companyId) {
|
|
567
|
-
console.log(chalk.cyan(' Company ID:'), chalk.white(config.companyId));
|
|
568
|
-
}
|
|
569
245
|
console.log();
|
|
570
246
|
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (!canContinue) {
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
} else {
|
|
578
|
-
// Force mode: backup existing .env without prompting
|
|
579
|
-
const envPath = join(config.installDir, '.env');
|
|
247
|
+
// Auto-backup existing config if exists (no prompt)
|
|
248
|
+
const envPath = join(config.installDir, '.env');
|
|
249
|
+
if (existsSync(envPath)) {
|
|
580
250
|
const backupPath = backupEnvFile(envPath);
|
|
581
251
|
if (backupPath) {
|
|
582
|
-
console.log(chalk.yellow(`⚠
|
|
252
|
+
console.log(chalk.yellow(`⚠ Existing config backed up: ${backupPath}\n`));
|
|
583
253
|
}
|
|
584
254
|
}
|
|
585
255
|
|
|
586
|
-
// Generate credentials
|
|
587
|
-
console.log(chalk.
|
|
256
|
+
// Step: Generate credentials
|
|
257
|
+
console.log(chalk.cyan(formatStepTitle(stepGenerateCredentials, totalSteps, 'Generating secure credentials')));
|
|
588
258
|
const credentials = generateCredentials();
|
|
589
|
-
|
|
590
|
-
|
|
259
|
+
console.log(chalk.green(`✓ ${formatStepTitle(stepGenerateCredentials, totalSteps, 'Secure credentials generated')}\n`));
|
|
260
|
+
|
|
261
|
+
// Installation tasks with Listr2
|
|
591
262
|
const tasks = new Listr([
|
|
592
263
|
{
|
|
593
|
-
title: 'Creating installation directory',
|
|
264
|
+
title: formatStepTitle(stepCreateDir, totalSteps, 'Creating installation directory'),
|
|
594
265
|
task: () => {
|
|
595
266
|
mkdirSync(config.installDir, { recursive: true });
|
|
596
267
|
mkdirSync(join(config.installDir, 'backups'), { recursive: true });
|
|
@@ -598,59 +269,47 @@ export async function install(options) {
|
|
|
598
269
|
}
|
|
599
270
|
},
|
|
600
271
|
{
|
|
601
|
-
title: '
|
|
602
|
-
task: async () => {
|
|
603
|
-
try {
|
|
604
|
-
// Set system timezone to Asia/Manila
|
|
605
|
-
await execa('timedatectl', ['set-timezone', 'Asia/Manila'], { stdio: 'pipe' });
|
|
606
|
-
} catch (error) {
|
|
607
|
-
// Non-critical: log warning but continue installation
|
|
608
|
-
console.warn(' ⚠ Could not set system timezone (may require sudo)');
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
{
|
|
613
|
-
title: 'Generating configuration files',
|
|
272
|
+
title: formatStepTitle(stepGenerateConfig, totalSteps, 'Generating configuration files'),
|
|
614
273
|
task: () => {
|
|
615
274
|
const dockerCompose = generateDockerCompose({
|
|
616
|
-
frontendPort: config.frontendPort
|
|
617
|
-
backendPort: config.apiPort
|
|
618
|
-
gateAppPort: config.gateAppPort
|
|
619
|
-
guardianAppPort: config.guardianAppPort
|
|
620
|
-
facialWebPort: config.facialWebPort
|
|
621
|
-
posAppPort: config.posAppPort
|
|
275
|
+
frontendPort: config.frontendPort,
|
|
276
|
+
backendPort: config.apiPort,
|
|
277
|
+
gateAppPort: config.gateAppPort,
|
|
278
|
+
guardianAppPort: config.guardianAppPort,
|
|
279
|
+
facialWebPort: config.facialWebPort,
|
|
280
|
+
posAppPort: config.posAppPort,
|
|
622
281
|
installMain: true,
|
|
623
|
-
installGate: config.installGate
|
|
624
|
-
installGuardian: config.installGuardian
|
|
625
|
-
installFacial: config.installFacial
|
|
626
|
-
installPos: config.installPos
|
|
627
|
-
companyId: config.companyId
|
|
282
|
+
installGate: config.installGate,
|
|
283
|
+
installGuardian: config.installGuardian,
|
|
284
|
+
installFacial: config.installFacial,
|
|
285
|
+
installPos: config.installPos,
|
|
286
|
+
companyId: config.companyId
|
|
628
287
|
});
|
|
629
288
|
|
|
630
289
|
const envContent = generateEnv(credentials, {
|
|
631
|
-
frontendUrl: config.frontendDomain
|
|
632
|
-
apiUrl: config.apiDomain
|
|
633
|
-
socketUrl: config.apiDomain
|
|
634
|
-
frontendPort: config.frontendPort
|
|
635
|
-
backendPort: config.apiPort
|
|
636
|
-
gateAppPort: config.gateAppPort
|
|
637
|
-
guardianAppPort: config.guardianAppPort
|
|
638
|
-
facialWebPort: config.facialWebPort
|
|
639
|
-
posAppPort: config.posAppPort
|
|
640
|
-
gateAppUrl: config.gateAppDomain
|
|
641
|
-
guardianAppUrl: config.guardianAppDomain
|
|
642
|
-
facialWebUrl: config.facialAppDomain
|
|
643
|
-
posAppUrl: config.posAppDomain
|
|
644
|
-
companyId: config.companyId
|
|
645
|
-
installGate: config.installGate
|
|
646
|
-
installGuardian: config.installGuardian
|
|
647
|
-
installFacial: config.installFacial
|
|
648
|
-
installPos: config.installPos
|
|
290
|
+
frontendUrl: config.frontendDomain,
|
|
291
|
+
apiUrl: config.apiDomain,
|
|
292
|
+
socketUrl: config.apiDomain,
|
|
293
|
+
frontendPort: config.frontendPort,
|
|
294
|
+
backendPort: config.apiPort,
|
|
295
|
+
gateAppPort: config.gateAppPort,
|
|
296
|
+
guardianAppPort: config.guardianAppPort,
|
|
297
|
+
facialWebPort: config.facialWebPort,
|
|
298
|
+
posAppPort: config.posAppPort,
|
|
299
|
+
gateAppUrl: config.gateAppDomain,
|
|
300
|
+
guardianAppUrl: config.guardianAppDomain,
|
|
301
|
+
facialWebUrl: config.facialAppDomain,
|
|
302
|
+
posAppUrl: config.posAppDomain,
|
|
303
|
+
companyId: config.companyId,
|
|
304
|
+
installGate: config.installGate,
|
|
305
|
+
installGuardian: config.installGuardian,
|
|
306
|
+
installFacial: config.installFacial,
|
|
307
|
+
installPos: config.installPos
|
|
649
308
|
});
|
|
650
|
-
|
|
309
|
+
|
|
651
310
|
writeFileSync(join(config.installDir, 'docker-compose.yml'), dockerCompose);
|
|
652
311
|
writeFileSync(join(config.installDir, '.env'), envContent);
|
|
653
|
-
|
|
312
|
+
|
|
654
313
|
// Save credentials
|
|
655
314
|
const credentialsText = `ANTE ERP Installation Credentials
|
|
656
315
|
Generated: ${new Date().toISOString()}
|
|
@@ -671,9 +330,9 @@ Encryption Key: ${credentials.encryptionKey}
|
|
|
671
330
|
|
|
672
331
|
Access Information:
|
|
673
332
|
━━━━━━━━━━━━━━━━━━━
|
|
674
|
-
Frontend: ${config.frontendDomain
|
|
675
|
-
Backend: ${config.apiDomain
|
|
676
|
-
WebSocket: ${config.apiDomain
|
|
333
|
+
Frontend: ${config.frontendDomain}
|
|
334
|
+
Backend: ${config.apiDomain}
|
|
335
|
+
WebSocket: ${config.apiDomain}
|
|
677
336
|
|
|
678
337
|
Next Steps:
|
|
679
338
|
━━━━━━━━━━
|
|
@@ -686,50 +345,49 @@ Next Steps:
|
|
|
686
345
|
Documentation: https://docs.ante.ph
|
|
687
346
|
Support: support@ante.ph
|
|
688
347
|
`;
|
|
689
|
-
|
|
348
|
+
|
|
690
349
|
writeFileSync(join(config.installDir, 'installation-credentials.txt'), credentialsText);
|
|
691
350
|
}
|
|
692
351
|
},
|
|
693
352
|
{
|
|
694
|
-
title: 'Pulling Docker images',
|
|
353
|
+
title: formatStepTitle(stepPullImages, totalSteps, 'Pulling Docker images'),
|
|
695
354
|
task: async () => {
|
|
696
355
|
const composeFile = join(config.installDir, 'docker-compose.yml');
|
|
697
|
-
await
|
|
356
|
+
await pullImagesSilent(composeFile);
|
|
698
357
|
}
|
|
699
358
|
},
|
|
700
359
|
{
|
|
701
|
-
title: 'Starting services',
|
|
360
|
+
title: formatStepTitle(stepStartServices, totalSteps, 'Starting services'),
|
|
702
361
|
task: async () => {
|
|
703
362
|
const composeFile = join(config.installDir, 'docker-compose.yml');
|
|
704
|
-
await
|
|
363
|
+
await startServicesSilent(composeFile);
|
|
705
364
|
}
|
|
706
365
|
},
|
|
707
366
|
{
|
|
708
|
-
title: 'Waiting for services to be ready',
|
|
367
|
+
title: formatStepTitle(stepWaitServices, totalSteps, 'Waiting for services to be ready'),
|
|
709
368
|
task: async () => {
|
|
710
369
|
const composeFile = join(config.installDir, 'docker-compose.yml');
|
|
711
|
-
|
|
370
|
+
|
|
712
371
|
// Wait for backend to be healthy
|
|
713
372
|
const backendHealthy = await waitForServiceHealthy(composeFile, 'backend', 120);
|
|
714
373
|
if (!backendHealthy) {
|
|
715
374
|
throw new Error('Backend service failed to start');
|
|
716
375
|
}
|
|
717
|
-
|
|
718
|
-
// Give it a few more seconds
|
|
376
|
+
|
|
377
|
+
// Give it a few more seconds for full initialization
|
|
719
378
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
720
379
|
}
|
|
721
380
|
},
|
|
722
381
|
{
|
|
723
|
-
title: 'Initializing database
|
|
382
|
+
title: formatStepTitle(stepInitDatabase, totalSteps, 'Initializing database'),
|
|
724
383
|
task: async () => {
|
|
725
384
|
const composeFile = join(config.installDir, 'docker-compose.yml');
|
|
726
385
|
const schemaFile = join(__dirname, '../templates/init-schema.sql');
|
|
727
386
|
|
|
728
387
|
try {
|
|
729
|
-
const { readFileSync } = await import('fs');
|
|
730
388
|
const schemaSQL = readFileSync(schemaFile, 'utf8');
|
|
731
389
|
|
|
732
|
-
// Execute schema SQL in postgres container
|
|
390
|
+
// Execute schema SQL in postgres container
|
|
733
391
|
await execa('docker', [
|
|
734
392
|
'compose',
|
|
735
393
|
'-f', composeFile,
|
|
@@ -740,21 +398,12 @@ Support: support@ante.ph
|
|
|
740
398
|
'-U', 'ante',
|
|
741
399
|
'-d', 'ante_db'
|
|
742
400
|
], {
|
|
743
|
-
input: schemaSQL
|
|
401
|
+
input: schemaSQL,
|
|
402
|
+
stdout: 'pipe',
|
|
403
|
+
stderr: 'pipe'
|
|
744
404
|
});
|
|
745
|
-
} catch (error) {
|
|
746
|
-
throw new Error(`Schema initialization failed: ${error.message}`);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
},
|
|
750
|
-
{
|
|
751
|
-
title: 'Seeding initial data',
|
|
752
|
-
task: async () => {
|
|
753
|
-
const composeFile = join(config.installDir, 'docker-compose.yml');
|
|
754
405
|
|
|
755
|
-
|
|
756
|
-
// Run production seed command in the backend container
|
|
757
|
-
// Uses seed:prod which compiles TypeScript and runs with node (production-safe)
|
|
406
|
+
// Run production seed command
|
|
758
407
|
await execa('docker', [
|
|
759
408
|
'compose',
|
|
760
409
|
'-f', composeFile,
|
|
@@ -764,51 +413,52 @@ Support: support@ante.ph
|
|
|
764
413
|
'npm',
|
|
765
414
|
'run',
|
|
766
415
|
'seed:prod'
|
|
767
|
-
]
|
|
416
|
+
], {
|
|
417
|
+
stdout: 'pipe',
|
|
418
|
+
stderr: 'pipe'
|
|
419
|
+
});
|
|
768
420
|
} catch (error) {
|
|
769
|
-
|
|
770
|
-
console.log(chalk.yellow(' ⚠ Seeding skipped (optional)'));
|
|
421
|
+
throw new Error(`Database initialization failed: ${error.message}`);
|
|
771
422
|
}
|
|
772
423
|
}
|
|
773
424
|
},
|
|
774
425
|
{
|
|
775
|
-
title: 'Running database migrations',
|
|
776
|
-
task: async (
|
|
426
|
+
title: formatStepTitle(stepRunMigrations, totalSteps, 'Running database migrations'),
|
|
427
|
+
task: async () => {
|
|
777
428
|
const composeFile = join(config.installDir, 'docker-compose.yml');
|
|
778
429
|
const result = await runMigrations(composeFile);
|
|
779
430
|
|
|
780
431
|
if (!result.success) {
|
|
781
432
|
throw new Error(`Migration failed:\n${result.output}`);
|
|
782
433
|
}
|
|
783
|
-
|
|
784
|
-
// Show migration output if there were migrations run
|
|
785
|
-
if (result.output && result.output.trim()) {
|
|
786
|
-
task.output = result.output;
|
|
787
|
-
}
|
|
788
434
|
}
|
|
789
435
|
}
|
|
790
436
|
], {
|
|
437
|
+
renderer: 'default',
|
|
791
438
|
rendererOptions: {
|
|
792
|
-
|
|
793
|
-
|
|
439
|
+
collapse: false,
|
|
440
|
+
showSubtasks: false,
|
|
441
|
+
clearOutput: false,
|
|
442
|
+
showTimer: false,
|
|
443
|
+
removeEmptyLines: true
|
|
444
|
+
},
|
|
445
|
+
concurrent: false,
|
|
446
|
+
exitOnError: true
|
|
794
447
|
});
|
|
795
|
-
|
|
796
|
-
await tasks.run();
|
|
797
448
|
|
|
798
|
-
|
|
799
|
-
if (requiresNginx(config.frontendDomain || 'http://localhost:8080', config.apiDomain || 'http://localhost:3001')) {
|
|
800
|
-
console.log(chalk.gray('\n🔧 Setting up reverse proxy...\n'));
|
|
449
|
+
await tasks.run();
|
|
801
450
|
|
|
451
|
+
// Configure NGINX if needed (silent)
|
|
452
|
+
if (requiresNginx(config.frontendDomain, config.apiDomain)) {
|
|
802
453
|
try {
|
|
803
454
|
await configureNginx({
|
|
804
|
-
frontendDomain: config.frontendDomain
|
|
805
|
-
apiDomain: config.apiDomain
|
|
806
|
-
frontendPort: config.frontendPort
|
|
807
|
-
apiPort: config.apiPort
|
|
455
|
+
frontendDomain: config.frontendDomain,
|
|
456
|
+
apiDomain: config.apiDomain,
|
|
457
|
+
frontendPort: config.frontendPort,
|
|
458
|
+
apiPort: config.apiPort
|
|
808
459
|
});
|
|
809
460
|
} catch (error) {
|
|
810
|
-
console.log(chalk.yellow('
|
|
811
|
-
console.log(chalk.gray('You may need to configure reverse proxy manually\n'));
|
|
461
|
+
console.log(chalk.yellow('⚠ NGINX configuration skipped (manual setup may be required)'));
|
|
812
462
|
}
|
|
813
463
|
}
|
|
814
464
|
|
|
@@ -818,20 +468,14 @@ Support: support@ante.ph
|
|
|
818
468
|
version: '1.0.0'
|
|
819
469
|
};
|
|
820
470
|
|
|
821
|
-
// Only include domain if it exists
|
|
822
|
-
if (config.domain) {
|
|
823
|
-
installConfig.domain = config.domain;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
471
|
saveInstallConfig(installConfig);
|
|
827
472
|
|
|
828
473
|
// Show success message
|
|
829
474
|
showSuccess(config.installDir, credentials, config);
|
|
830
|
-
|
|
475
|
+
|
|
831
476
|
} catch (error) {
|
|
832
477
|
console.error(chalk.red('\n✗ Installation failed:'), error.message);
|
|
833
478
|
console.error(chalk.gray('\nFor help, visit: https://docs.ante.ph/self-hosting/troubleshooting\n'));
|
|
834
479
|
process.exit(1);
|
|
835
480
|
}
|
|
836
481
|
}
|
|
837
|
-
|