@simplens/onboard 1.0.8 → 1.0.10

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/src/index.ts DELETED
@@ -1,683 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import path from 'path';
5
- import {
6
- displayBanner,
7
- logSuccess,
8
- logInfo,
9
- initLogger,
10
- logDebug,
11
- printStepHeader,
12
- printSummaryCard,
13
- printCommandHints,
14
- logWarning,
15
- } from './utils.js';
16
- import { text, confirm, select } from '@clack/prompts';
17
- import { intro, outro, handleCancel, log, note } from './ui.js';
18
- import {
19
- validatePrerequisites,
20
- validatePublicDomain,
21
- validateEmailAddress,
22
- } from './validators.js';
23
- import {
24
- promptInfraServicesWithBasePath,
25
- generateInfraCompose,
26
- writeAppCompose,
27
- generateNginxConfig,
28
- } from './infra.js';
29
- import {
30
- promptEnvVariables,
31
- generateEnvFile,
32
- appendPluginEnv,
33
- promptBasePath,
34
- normalizeBasePath,
35
- validateBasePath,
36
- DEFAULT_BASE_PATH,
37
- } from './env-config.js';
38
- import {
39
- fetchAvailablePlugins,
40
- promptPluginSelection,
41
- generatePluginConfig,
42
- parseConfigCredentials,
43
- promptPluginCredentials,
44
- generateDefaultPluginCredentials,
45
- } from './plugins.js';
46
- import {
47
- promptStartServices,
48
- startInfraServices,
49
- waitForInfraHealth,
50
- startAppServices,
51
- displayServiceStatus,
52
- setupSslCertificates,
53
- getSslManualCommands,
54
- reloadNginxConfiguration,
55
- } from './services.js';
56
-
57
- const program = new Command();
58
-
59
- program
60
- .name('@simplens/onboard')
61
- .description('A CLI tool to setup a SimpleNS instance on your machine/server')
62
- .version('1.0.0')
63
- .option('--full', 'Non-interactive mode - all options must be provided via CLI')
64
- .option('--infra [services...]', 'Infrastructure services (mongo, kafka, kafka-ui, redis, nginx, loki, grafana)')
65
- .option('--env <mode>', 'Environment setup mode: "default" or "interactive"')
66
- .option('--dir <path>', 'Target directory for setup')
67
- .option('--base-path <path>', 'Dashboard BASE_PATH (example: /dashboard, default: root)')
68
- .option('--core-version <version>', 'Override CORE_VERSION in generated .env (primarily for --full mode)')
69
- .option('--dashboard-version <version>', 'Override DASHBOARD_VERSION in generated .env (primarily for --full mode)')
70
- .option('--plugin [plugins...]', 'Plugins to install (e.g., @simplens/mock @simplens/nodemailer-gmail)')
71
- .option('--ssl', 'Enable optional SSL certificate setup using Dockerized Certbot')
72
- .option('--ssl-domain <domain>', 'Public domain for SSL certificate (required with --ssl in --full mode)')
73
- .option('--ssl-email <email>', 'Email for Let\'s Encrypt registration (required with --ssl in --full mode)')
74
- .option('--no-output', 'Suppress all console output (silent mode)');
75
-
76
- interface OnboardSetupOptions {
77
- infra: boolean;
78
- infraServices: string[];
79
- envMode: 'default' | 'interactive';
80
- targetDir: string;
81
- basePath: string;
82
- plugins: string[];
83
- enableSsl: boolean;
84
- sslDomain?: string;
85
- sslEmail?: string;
86
- }
87
-
88
- function printStep(step: number, total: number, title: string): void {
89
- printStepHeader(step, total, title);
90
- }
91
-
92
- function shouldAutoEnableNginx(basePath: string): boolean {
93
- return normalizeBasePath(basePath) !== DEFAULT_BASE_PATH;
94
- }
95
-
96
- /**
97
- * Valid infrastructure services
98
- */
99
- const VALID_INFRA_SERVICES = ['mongo', 'kafka', 'kafka-ui', 'redis', 'nginx', 'loki', 'grafana'];
100
-
101
- /**
102
- * Validate infrastructure service names
103
- */
104
- function validateInfraServices(services: string[]): { valid: boolean; invalid: string[] } {
105
- const invalid = services.filter(s => !VALID_INFRA_SERVICES.includes(s));
106
- return { valid: invalid.length === 0, invalid };
107
- }
108
-
109
- /**
110
- * Validate plugin names (must start with @simplens/ or be a valid npm package)
111
- */
112
- function validatePlugins(plugins: string[]): { valid: boolean; invalid: string[] } {
113
- const invalid = plugins.filter(p => {
114
- // Must start with @ or be a valid npm package name
115
- return !p.match(/^(@[\w-]+\/[\w-]+|[\w-]+)$/);
116
- });
117
- return { valid: invalid.length === 0, invalid };
118
- }
119
-
120
- function showSetupSummary(setupOptions: OnboardSetupOptions, targetDir: string, autoNginx: boolean): void {
121
- const basePathLabel = setupOptions.basePath || '(root)';
122
- const infraLabel = setupOptions.infra
123
- ? `enabled (${setupOptions.infraServices.join(', ')})`
124
- : 'disabled';
125
- const pluginsLabel = setupOptions.plugins.length > 0
126
- ? setupOptions.plugins.join(', ')
127
- : 'none';
128
- const sslLabel = setupOptions.enableSsl
129
- ? `enabled (${setupOptions.sslDomain})`
130
- : 'disabled';
131
-
132
- const summaryLines = [
133
- `Target directory : ${targetDir}`,
134
- `Infrastructure : ${infraLabel}`,
135
- `Environment mode : ${setupOptions.envMode}`,
136
- `BASE_PATH : ${basePathLabel}`,
137
- `Plugins : ${pluginsLabel}`,
138
- `SSL (Certbot) : ${sslLabel}`,
139
- `Nginx auto-include : ${autoNginx ? 'enabled (BASE_PATH is non-default)' : 'disabled'}`,
140
- ].join('\n');
141
-
142
- note(summaryLines, 'Setup Summary');
143
- }
144
-
145
- /**
146
- * Prompt for setup options if not provided via CLI args
147
- * In --full mode, all required options must be provided via CLI
148
- */
149
- async function promptSetupOptions(options: any): Promise<OnboardSetupOptions> {
150
- const isFullMode = options.full === true;
151
-
152
- // --- Validate --full mode requirements ---
153
- if (isFullMode) {
154
- const errors: string[] = [];
155
-
156
- // --env is required in full mode
157
- if (!options.env) {
158
- errors.push('--env <mode> is required in --full mode (use \"default\" or \"interactive\")');
159
- } else if (options.env !== 'default' && options.env !== 'interactive') {
160
- errors.push('--env must be either \"default\" or \"interactive\"');
161
- }
162
-
163
- // Validate --base-path if provided
164
- if (options.basePath) {
165
- const validation = validateBasePath(normalizeBasePath(options.basePath));
166
- if (validation !== true) {
167
- errors.push(`Invalid --base-path: ${validation}`);
168
- }
169
- }
170
-
171
- // Validate --infra services if provided
172
- if (options.infra && Array.isArray(options.infra)) {
173
- const { valid, invalid } = validateInfraServices(options.infra);
174
- if (!valid) {
175
- errors.push(
176
- `Invalid infrastructure services: ${invalid.join(', ')}. ` +
177
- `Valid options: ${VALID_INFRA_SERVICES.join(', ')}`
178
- );
179
- }
180
- }
181
-
182
- // Validate --plugin if provided
183
- if (options.plugin && Array.isArray(options.plugin)) {
184
- const { valid, invalid } = validatePlugins(options.plugin);
185
- if (!valid) {
186
- errors.push(`Invalid plugin names: ${invalid.join(', ')}`);
187
- }
188
- }
189
-
190
- if (options.ssl === true) {
191
- if (!options.sslDomain) {
192
- errors.push('--ssl-domain <domain> is required in --full mode when --ssl is enabled');
193
- } else {
194
- const domainValidation = validatePublicDomain(options.sslDomain);
195
- if (domainValidation !== true) {
196
- errors.push(`Invalid --ssl-domain: ${domainValidation}`);
197
- }
198
- }
199
-
200
- if (!options.sslEmail) {
201
- errors.push('--ssl-email <email> is required in --full mode when --ssl is enabled');
202
- } else {
203
- const emailValidation = validateEmailAddress(options.sslEmail);
204
- if (emailValidation !== true) {
205
- errors.push(`Invalid --ssl-email: ${emailValidation}`);
206
- }
207
- }
208
- }
209
-
210
- if (errors.length > 0) {
211
- console.error('\\n❌ Validation errors in --full mode:\\n');
212
- errors.forEach(err => console.error(` • ${err}`));
213
- console.error('\\nRun with --help to see usage examples.\\n');
214
- process.exit(1);
215
- }
216
- }
217
-
218
- // --- BASE_PATH ---
219
- let basePathValue: string;
220
- const cliBasePath = typeof options.basePath === 'string'
221
- ? normalizeBasePath(options.basePath)
222
- : undefined;
223
-
224
- if (cliBasePath !== undefined) {
225
- const validation = validateBasePath(cliBasePath);
226
- if (validation !== true) {
227
- throw new Error(`Invalid --base-path value: ${validation}`);
228
- }
229
- basePathValue = cliBasePath;
230
- } else if (isFullMode) {
231
- basePathValue = DEFAULT_BASE_PATH; // Default to root in full mode
232
- } else {
233
- basePathValue = await promptBasePath(DEFAULT_BASE_PATH);
234
- }
235
-
236
- // --- Infra flag and services ---
237
- let infraValue: boolean;
238
- let infraServices: string[] = [];
239
-
240
- if (Array.isArray(options.infra) && options.infra.length > 0) {
241
- // --infra with services provided
242
- infraValue = true;
243
- infraServices = options.infra;
244
- } else if (options.infra === true) {
245
- // --infra flag without services (backward compatibility - prompt for services)
246
- infraValue = true;
247
- if (isFullMode) {
248
- // In full mode, empty --infra means no services selected (error)
249
- console.error('\\n❌ In --full mode, --infra requires service names.\\n');
250
- console.error('Example: --infra mongo kafka redis\\n');
251
- process.exit(1);
252
- }
253
- // Not in full mode, will prompt later
254
- } else {
255
- // No --infra flag provided
256
- if (isFullMode) {
257
- infraValue = false; // Default to no infrastructure in full mode
258
- } else {
259
- const result = await confirm({
260
- message: 'Do you want to setup infrastructure services (MongoDB, Kafka, Redis, etc.)?',
261
- initialValue: true,
262
- withGuide: true,
263
- });
264
- handleCancel(result);
265
- infraValue = result as boolean;
266
- }
267
- }
268
-
269
- // --- Env mode ---
270
- let envModeValue: 'default' | 'interactive';
271
- if (options.env) {
272
- envModeValue = options.env;
273
- } else if (isFullMode) {
274
- // Already validated above, this shouldn't happen
275
- envModeValue = 'default';
276
- } else {
277
- const result = await select({
278
- message: 'Select environment configuration mode:',
279
- options: [
280
- { value: 'default', label: 'Default', hint: 'use preset values, prompt only for critical' },
281
- { value: 'interactive', label: 'Interactive', hint: 'prompt for all variables' },
282
- ],
283
- initialValue: 'default',
284
- withGuide: true,
285
- });
286
- handleCancel(result);
287
- envModeValue = result as 'default' | 'interactive';
288
- }
289
-
290
- // --- Target directory ---
291
- let targetDirValue: string;
292
- if (options.dir) {
293
- targetDirValue = options.dir;
294
- } else if (isFullMode) {
295
- targetDirValue = process.cwd(); // Default to current directory in full mode
296
- } else {
297
- const result = await text({
298
- message: 'Target directory for setup:',
299
- defaultValue: process.cwd(),
300
- initialValue: process.cwd(),
301
- withGuide: true,
302
- });
303
- handleCancel(result);
304
- targetDirValue = result as string;
305
- }
306
-
307
- // --- Plugins ---
308
- let pluginsValue: string[] = [];
309
- if (Array.isArray(options.plugin) && options.plugin.length > 0) {
310
- pluginsValue = options.plugin;
311
- }
312
- // If not provided and not in full mode, will prompt later in the main workflow
313
-
314
- // --- SSL ---
315
- let enableSslValue = false;
316
- let sslDomainValue: string | undefined;
317
- let sslEmailValue: string | undefined;
318
-
319
- if (options.ssl === true) {
320
- enableSslValue = true;
321
- } else if (!isFullMode) {
322
- const sslConfirm = await confirm({
323
- message: 'Do you want to automatically setup SSL certificate using Certbot?',
324
- initialValue: false,
325
- withGuide: true,
326
- });
327
- handleCancel(sslConfirm);
328
- enableSslValue = sslConfirm as boolean;
329
- }
330
-
331
- if (enableSslValue) {
332
- if (typeof options.sslDomain === 'string') {
333
- const normalizedDomain = options.sslDomain.trim().toLowerCase();
334
- const domainValidation = validatePublicDomain(normalizedDomain);
335
- if (domainValidation !== true) {
336
- throw new Error(`Invalid --ssl-domain value: ${domainValidation}`);
337
- }
338
- sslDomainValue = normalizedDomain;
339
- } else if (!isFullMode) {
340
- const domainAnswer = await text({
341
- message: 'Public domain to secure (example: app.example.com):',
342
- validate: (value: string | undefined) => {
343
- const validation = validatePublicDomain(value ?? '');
344
- return validation === true ? undefined : validation;
345
- },
346
- withGuide: true,
347
- });
348
- handleCancel(domainAnswer);
349
- sslDomainValue = (domainAnswer as string).trim().toLowerCase();
350
- }
351
-
352
- if (typeof options.sslEmail === 'string') {
353
- const normalizedEmail = options.sslEmail.trim();
354
- const emailValidation = validateEmailAddress(normalizedEmail);
355
- if (emailValidation !== true) {
356
- throw new Error(`Invalid --ssl-email value: ${emailValidation}`);
357
- }
358
- sslEmailValue = normalizedEmail;
359
- } else if (!isFullMode) {
360
- const emailAnswer = await text({
361
- message: 'Email for Let\'s Encrypt registration:',
362
- validate: (value: string | undefined) => {
363
- const validation = validateEmailAddress(value ?? '');
364
- return validation === true ? undefined : validation;
365
- },
366
- withGuide: true,
367
- });
368
- handleCancel(emailAnswer);
369
- sslEmailValue = (emailAnswer as string).trim();
370
- }
371
- }
372
-
373
- return {
374
- infra: infraValue,
375
- infraServices: infraServices,
376
- envMode: envModeValue || 'default',
377
- targetDir: targetDirValue || process.cwd(),
378
- basePath: basePathValue,
379
- plugins: pluginsValue,
380
- enableSsl: enableSslValue,
381
- sslDomain: sslDomainValue,
382
- sslEmail: sslEmailValue,
383
- };
384
- }
385
-
386
- /**
387
- * Main onboarding workflow
388
- */
389
- async function main() {
390
- try {
391
- const totalSteps = 6;
392
-
393
- // Parse command line arguments FIRST
394
- program.parse(process.argv);
395
- const options = program.opts();
396
-
397
- // Initialize logger based on CLI flags (before any output)
398
- initLogger({
399
- verbose: options.verbose || false,
400
- debug: options.debug || false,
401
- silent: !options.output, // --no-output sets options.output to false
402
- logFile: options.debug ? path.join(process.cwd(), 'onboard-debug.log') : undefined,
403
- });
404
-
405
- // Display banner (after logger is initialized)
406
- displayBanner();
407
-
408
- // Clack intro
409
- intro('SimpleNS Onboard');
410
-
411
- logDebug('Logger initialized');
412
- logDebug(`CLI options: ${JSON.stringify(options)}`);
413
-
414
- // Prompt for setup options if not provided
415
- const setupOptions = await promptSetupOptions(options);
416
-
417
- // Get target directory
418
- const targetDir = path.resolve(setupOptions.targetDir);
419
- const autoEnableNginx = shouldAutoEnableNginx(setupOptions.basePath);
420
- const nginxRequired = autoEnableNginx || setupOptions.enableSsl;
421
-
422
- logDebug(`Resolved target directory: ${targetDir}`);
423
- showSetupSummary(setupOptions, targetDir, autoEnableNginx);
424
-
425
- // Step 1: Validate prerequisites
426
- log.step('Step 1/6 — Prerequisites Validation');
427
- await validatePrerequisites();
428
-
429
- // Step 2: Infrastructure setup (if --infra flag is provided)
430
- log.step('Step 2/6 — Infrastructure Setup');
431
- let selectedInfraServices: string[] = [];
432
- const shouldSetupInfra = setupOptions.infra || nginxRequired;
433
-
434
- if (shouldSetupInfra) {
435
- // Use pre-provided services from CLI, or prompt for them
436
- if (setupOptions.infra && setupOptions.infraServices.length > 0) {
437
- selectedInfraServices = setupOptions.infraServices;
438
- log.info(`Using infrastructure services: ${selectedInfraServices.join(', ')}`);
439
- } else if (!setupOptions.infra && nginxRequired) {
440
- selectedInfraServices = ['nginx'];
441
- log.info('Nginx is required (BASE_PATH/SSL), so infrastructure compose will be generated with nginx.');
442
- } else {
443
- // Prompt for services (interactive mode)
444
- if (!autoEnableNginx) {
445
- log.info('BASE_PATH is empty, nginx reverse proxy is disabled.');
446
- selectedInfraServices = await promptInfraServicesWithBasePath({
447
- allowNginx: false,
448
- });
449
- } else {
450
- selectedInfraServices = await promptInfraServicesWithBasePath({
451
- allowNginx: true,
452
- defaultNginx: true,
453
- });
454
- }
455
- }
456
-
457
- if (setupOptions.enableSsl && !selectedInfraServices.includes('nginx')) {
458
- selectedInfraServices.push('nginx');
459
- log.info('SSL is enabled, so nginx was added automatically.');
460
- }
461
-
462
- const infraHasNginx = selectedInfraServices.includes('nginx');
463
- await generateInfraCompose(targetDir, selectedInfraServices, {
464
- includeSsl: setupOptions.enableSsl && infraHasNginx,
465
- });
466
- } else {
467
- log.info('Skipping infrastructure setup (use --infra to enable).');
468
- }
469
-
470
- // Step 3: Always write app docker-compose
471
- log.step('Step 3/6 — Application Compose Setup');
472
- await writeAppCompose(targetDir, {
473
- includeNginx: false,
474
- includeSsl: false,
475
- });
476
-
477
- // Step 4: Environment configuration
478
- log.step('Step 4/6 — Environment Configuration');
479
- const envMode = setupOptions.envMode;
480
- const envOverrides = options.full
481
- ? {
482
- CORE_VERSION: options.coreVersion,
483
- DASHBOARD_VERSION: options.dashboardVersion,
484
- }
485
- : undefined;
486
- const envVars = await promptEnvVariables(
487
- envMode,
488
- selectedInfraServices,
489
- setupOptions.basePath,
490
- options.full || false,
491
- envOverrides
492
- );
493
- await generateEnvFile(targetDir, envVars);
494
-
495
- // In full mode, notify user about auto-generated credentials
496
- if (options.full) {
497
- logWarning(
498
- '⚠️ Auto-generated credentials in .env file. ' +
499
- 'Please update NS_API_KEY, AUTH_SECRET, and ADMIN_PASSWORD before deploying to production!'
500
- );
501
- }
502
-
503
- // Generate nginx.conf whenever nginx is active in either compose file
504
- const nginxEnabled = selectedInfraServices.includes('nginx');
505
- if (nginxEnabled) {
506
- await generateNginxConfig(targetDir, setupOptions.basePath, {
507
- enableSsl: setupOptions.enableSsl,
508
- domain: setupOptions.sslDomain,
509
- sslMode: setupOptions.enableSsl ? 'bootstrap' : 'final',
510
- });
511
- }
512
-
513
- // Step 5: Plugin installation
514
- log.step('Step 5/6 — Plugin Installation');
515
- let selectedPlugins: string[] = [];
516
- let pluginCredentialKeys: string[] = [];
517
-
518
- // Use pre-provided plugins from CLI, or prompt for them
519
- if (setupOptions.plugins.length > 0) {
520
- selectedPlugins = setupOptions.plugins;
521
- log.info(`Using plugins: ${selectedPlugins.join(', ')}`);
522
- } else if (!options.full) {
523
- // Only prompt in interactive mode
524
- const availablePlugins = await fetchAvailablePlugins();
525
- selectedPlugins = await promptPluginSelection(availablePlugins);
526
- }
527
-
528
- if (selectedPlugins.length > 0) {
529
- await generatePluginConfig(targetDir, selectedPlugins);
530
-
531
- // Extract and prompt for plugin credentials
532
- const configPath = path.join(targetDir, 'simplens.config.yaml');
533
- const credentialKeys = await parseConfigCredentials(configPath);
534
- pluginCredentialKeys = credentialKeys; // Store for later use
535
-
536
- if (credentialKeys.length > 0) {
537
- if (options.full) {
538
- // In full mode, auto-generate placeholder credentials
539
- const pluginCreds = generateDefaultPluginCredentials(credentialKeys);
540
- await appendPluginEnv(targetDir, pluginCreds);
541
- logWarning(
542
- `⚠️ Auto-generated placeholder plugin credentials. ` +
543
- `Please update these in .env file: ${credentialKeys.join(', ')}`
544
- );
545
- } else {
546
- const pluginCreds = await promptPluginCredentials(credentialKeys);
547
- await appendPluginEnv(targetDir, pluginCreds);
548
- }
549
- }
550
- }
551
-
552
- // Step 6: Service orchestration
553
- log.step('Step 6/6 — Service Orchestration');
554
-
555
- let shouldStart = false;
556
- if (options.full) {
557
- // In full mode, don't auto-start services, just show commands
558
- log.info('In --full mode, services are not auto-started.');
559
- } else {
560
- shouldStart = await promptStartServices();
561
- }
562
-
563
- if (shouldStart) {
564
- // Start infra services first (if --infra was used)
565
- if (selectedInfraServices.length > 0) {
566
- await startInfraServices(targetDir);
567
- await waitForInfraHealth(targetDir);
568
- }
569
-
570
- // Start app services
571
- await startAppServices(targetDir);
572
-
573
- if (setupOptions.enableSsl && setupOptions.sslDomain && setupOptions.sslEmail) {
574
- await setupSslCertificates(targetDir, {
575
- composeFile: 'docker-compose.infra.yaml',
576
- domain: setupOptions.sslDomain,
577
- email: setupOptions.sslEmail,
578
- });
579
-
580
- await generateNginxConfig(targetDir, setupOptions.basePath, {
581
- enableSsl: true,
582
- domain: setupOptions.sslDomain,
583
- sslMode: 'final',
584
- });
585
-
586
- await reloadNginxConfiguration(targetDir, {
587
- composeFile: 'docker-compose.infra.yaml',
588
- });
589
- }
590
-
591
- // Display service status
592
- await displayServiceStatus();
593
- } else {
594
- log.info('Services not started. You can start them later with:');
595
- const commands: string[] = [];
596
- if (selectedInfraServices.length > 0) {
597
- commands.push('docker compose -f docker-compose.infra.yaml up -d');
598
- }
599
- commands.push('docker compose up -d');
600
- if (setupOptions.enableSsl && setupOptions.sslDomain && setupOptions.sslEmail) {
601
- commands.push(...getSslManualCommands({
602
- composeFile: 'docker-compose.infra.yaml',
603
- domain: setupOptions.sslDomain,
604
- email: setupOptions.sslEmail,
605
- }));
606
- }
607
- printCommandHints('Manual startup commands', commands);
608
- }
609
-
610
- // Final success message
611
- logSuccess('SimpleNS onboarding completed successfully.');
612
-
613
- // In full mode, show a comprehensive security warning
614
- if (options.full) {
615
- const credentialWarnings = [
616
- ' • NS_API_KEY - API authentication key',
617
- ' • AUTH_SECRET - Session secret for dashboard',
618
- ' • ADMIN_PASSWORD - Dashboard admin password',
619
- ];
620
-
621
- if (pluginCredentialKeys.length > 0) {
622
- credentialWarnings.push(` • Plugin credentials: ${pluginCredentialKeys.join(', ')}`);
623
- }
624
-
625
- note(
626
- '⚠️ IMPORTANT: Auto-generated credentials were used for non-interactive setup.\n' +
627
- '\n' +
628
- 'Please update the following in your .env file before production use:\n' +
629
- credentialWarnings.join('\n') +
630
- '\n\n' +
631
- 'Default credentials are NOT secure for production environments.',
632
- 'Security Notice'
633
- );
634
- }
635
-
636
- // Display access information
637
- if (nginxEnabled) {
638
- if (setupOptions.basePath) {
639
- note(
640
- `Dashboard : http://localhost${setupOptions.basePath}\nAPI : http://localhost/api/notification/`,
641
- 'Service Access'
642
- );
643
- } else {
644
- note(
645
- 'Dashboard : http://localhost\nAPI : http://localhost/api/notification/',
646
- 'Service Access'
647
- );
648
- }
649
- } else {
650
- note(
651
- 'Dashboard : http://localhost:3002\nAPI : http://localhost:3000',
652
- 'Service Access'
653
- );
654
- }
655
-
656
- // Clack outro
657
- outro('Setup complete — happy notifying! 🚀');
658
-
659
- } catch (error: unknown) {
660
- // Import at top of file
661
- const { formatErrorForUser } = await import('./types/errors.js');
662
- const { getLoggerConfig } = await import('./utils/logger.js');
663
-
664
- // Always log errors to stderr, even in silent mode
665
- if (!getLoggerConfig().silent) {
666
- console.log('\n' + formatErrorForUser(error as Error));
667
- } else {
668
- // In silent mode, write to stderr
669
- console.error(formatErrorForUser(error as Error));
670
- }
671
-
672
- // Log full error to stderr for debugging
673
- if (process.env.DEBUG) {
674
- console.error('\nFull error details:');
675
- console.error(error);
676
- }
677
-
678
- process.exit(1);
679
- }
680
- }
681
-
682
- // Run main function
683
- main();