@simplens/onboard 1.0.1 → 1.0.2

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