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