@simplens/onboard 1.0.0 → 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 +258 -141
  21. package/dist/env-config.js.map +1 -1
  22. package/dist/index.js +341 -71
  23. package/dist/index.js.map +1 -1
  24. package/dist/infra.d.ts +17 -14
  25. package/dist/infra.d.ts.map +1 -1
  26. package/dist/infra.js +265 -176
  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 +3 -2
  37. package/dist/templates.d.ts.map +1 -1
  38. package/dist/templates.js +203 -198
  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 -320
  68. package/src/index.ts +534 -203
  69. package/src/infra.ts +404 -300
  70. package/src/plugins.ts +221 -190
  71. package/src/services.ts +175 -190
  72. package/src/templates.ts +209 -203
  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 -20
package/src/env-config.ts CHANGED
@@ -1,320 +1,455 @@
1
- import inquirer from 'inquirer';
2
- import { readFile, writeFile, appendFile, logInfo, logSuccess, logWarning } from './utils.js';
3
- import path from 'path';
4
- import { CRITICAL_ENV_KEYS, VALIDATION } from './config/constants.js';
5
- import type { EnvVariable } from './types/domain.js';
6
-
7
- /**
8
- * Load and parse .env.example from embedded template
9
- */
10
- export async function loadEnvExample(): Promise<EnvVariable[]> {
11
- // Embedded .env template - always available regardless of installation
12
- const envTemplate = `
13
- # ============================================
14
- # INFRASTRUCTURE HOST CONFIGURATION
15
- # ============================================
16
- INFRA_HOST=host.docker.internal
17
-
18
- # ============================================
19
- # CONNECTION URLS
20
- # ============================================
21
- NODE_ENV=production
22
- # ============================================
23
- # API SERVER
24
- # ============================================
25
- NS_API_KEY=
26
- PORT=3000
27
- MAX_BATCH_REQ_LIMIT=1000
28
-
29
- # ============================================
30
- # DATABASE
31
- # ============================================
32
- MONGO_URI=
33
-
34
- # ============================================
35
- # KAFKA
36
- # ============================================
37
- BROKERS=
38
-
39
- # Kafka Topic Partitions (Core Topics)
40
- DELAYED_PARTITION=1
41
- NOTIFICATION_STATUS_PARTITION=1
42
-
43
- # ============================================
44
- # REDIS
45
- # ============================================
46
- REDIS_URL=
47
-
48
- # ============================================
49
- # PLUGIN SYSTEM
50
- # ============================================
51
- SIMPLENS_CONFIG_PATH=./simplens.config.yaml
52
- PROCESSOR_CHANNEL=all
53
-
54
- # ============================================
55
- # BACKGROUND WORKER
56
- # ============================================
57
- OUTBOX_POLL_INTERVAL_MS=5000
58
- OUTBOX_CLEANUP_INTERVAL_MS=60000
59
- OUTBOX_BATCH_SIZE=100
60
- OUTBOX_RETENTION_MS=300000
61
- OUTBOX_CLAIM_TIMEOUT_MS=30000
62
-
63
- # ============================================
64
- # RETRY & IDEMPOTENCY
65
- # ============================================
66
- IDEMPOTENCY_TTL_SECONDS=86400
67
- MAX_RETRY_COUNT=5
68
- PROCESSING_TTL_SECONDS=120
69
-
70
- # ============================================
71
- # DELAYED NOTIFICATIONS
72
- # ============================================
73
- DELAYED_POLL_INTERVAL_MS=1000
74
- DELAYED_BATCH_SIZE=10
75
- MAX_POLLER_RETRIES=3
76
-
77
- # ============================================
78
- # RECOVERY SERVICE
79
- # ============================================
80
- RECOVERY_POLL_INTERVAL_MS=60000
81
- PROCESSING_STUCK_THRESHOLD_MS=300000
82
- PENDING_STUCK_THRESHOLD_MS=300000
83
- RECOVERY_BATCH_SIZE=50
84
- RECOVERY_CLAIM_TIMEOUT_MS=60000
85
-
86
- # ============================================
87
- # CLEANUP
88
- # ============================================
89
- CLEANUP_RESOLVED_ALERTS_RETENTION_MS=86400000
90
- CLEANUP_PROCESSED_STATUS_OUTBOX_RETENTION_MS=86400000
91
-
92
- # ============================================
93
- # LOGGING
94
- # ============================================
95
- LOKI_URL=
96
- LOG_LEVEL=info
97
- LOG_TO_FILE=true
98
-
99
- # ============================================
100
- # ADMIN DASHBOARD
101
- # ============================================
102
- AUTH_SECRET=
103
- ADMIN_USERNAME=admin
104
- ADMIN_PASSWORD=
105
- AUTH_TRUST_HOST=true
106
- API_BASE_URL=http://api:3000
107
- WEBHOOK_HOST=dashboard
108
- WEBHOOK_PORT=3002
109
- DASHBOARD_PORT=3002
110
- `;
111
-
112
- return parseEnvContent(envTemplate);
113
- }
114
-
115
- /**
116
- * Parse .env content into structured format
117
- */
118
- function parseEnvContent(content: string): EnvVariable[] {
119
- const lines = content.split('\n');
120
- const variables: EnvVariable[] = [];
121
- let currentComment = '';
122
-
123
- for (const line of lines) {
124
- const trimmed = line.trim();
125
-
126
- // Capture comments as descriptions
127
- if (trimmed.startsWith('#') && !trimmed.includes('====')) {
128
- currentComment = trimmed.replace(/^#\s*/, '');
129
- continue;
130
- }
131
-
132
- // Skip empty lines and section dividers
133
- if (!trimmed || trimmed.includes('====')) {
134
- currentComment = '';
135
- continue;
136
- }
137
-
138
- // Parse key=value pairs
139
- const match = trimmed.match(/^([A-Z_]+)=(.*)$/);
140
- if (match) {
141
- const [, key, value] = match;
142
- variables.push({
143
- key,
144
- value: value || '',
145
- description: currentComment || undefined,
146
- required: CRITICAL_ENV_KEYS.includes(key) || !value,
147
- });
148
- currentComment = '';
149
- }
150
- }
151
-
152
- return variables;
153
- }
154
-
155
- /**
156
- * Prompts user for environment variable values based on the selected mode.
157
- *
158
- * @param mode - 'default' prompts only for critical vars, 'interactive' prompts for all
159
- * @param infraServices - List of selected infrastructure service IDs
160
- * @param infraHost - Host for infrastructure services
161
- * @returns Map of environment variable keys to values
162
- *
163
- * @remarks
164
- * Critical variables (always prompted): NS_API_KEY, MONGO_URI, BROKERS, etc.
165
- * Interactive mode prompts for all variables including optional ones.
166
- *
167
- * @example
168
- * ```ts
169
- * const envVars = await promptEnvVariables('default', ['mongo', 'kafka'], 'localhost');
170
- * // Prompts only for critical variables
171
- * ```
172
- */
173
- export async function promptEnvVariables(
174
- mode: 'default' | 'interactive',
175
- infraServices: string[],
176
- infraHost: string
177
- ): Promise<Map<string, string>> {
178
- logInfo('Configuring environment variables...');
179
-
180
- const envVars = await loadEnvExample();
181
- const result = new Map<string, string>();
182
-
183
- // Auto-fill infra connection URLs based on selected services and host
184
- const autoInfraUrls: Record<string, string> = {
185
- MONGO_URI: infraServices.includes('mongo')
186
- ? (infraHost==="host.docker.internal"?`mongodb://mongo:27017/simplens?replicaSet=rs0`:`mongodb://${infraHost}:27017/simplens?replicaSet=rs0`)
187
- : '',
188
- BROKERS: infraServices.includes('kafka')
189
- ? (infraHost==="host.docker.internal"?"kafka:9093":`${infraHost}:9092`)
190
- : '',
191
- REDIS_URL: infraServices.includes('redis')
192
- ? (infraHost==="host.docker.internal"?"redis://redis:6379":`redis://${infraHost}:6379`)
193
- : '',
194
- LOKI_URL: infraServices.includes('loki')
195
- ? (infraHost==="host.docker.internal"?"http://loki:3100":`http://${infraHost}:3100`)
196
- : '',
197
- };
198
-
199
- if (mode === 'default') {
200
- // Use defaults, only prompt for critical values
201
- for (const envVar of envVars) {
202
- // Use auto-filled infra URLs
203
- if (autoInfraUrls[envVar.key]) {
204
- result.set(envVar.key, autoInfraUrls[envVar.key]);
205
- continue;
206
- }
207
-
208
- // Use default value if available
209
- if (envVar.value && !CRITICAL_ENV_KEYS.includes(envVar.key)) {
210
- result.set(envVar.key, envVar.value);
211
- continue;
212
- }
213
-
214
- // Prompt for critical values (only if not auto-filled)
215
- if (CRITICAL_ENV_KEYS.includes(envVar.key)) {
216
- const answer = await inquirer.prompt<{ value: string }>([
217
- {
218
- type: envVar.key.includes('PASSWORD') ? 'password' : 'input',
219
- name: 'value',
220
- message: `${envVar.key}${envVar.description ? ` (${envVar.description})` : ''}:`,
221
- default: getSuggestedValue(envVar.key),
222
- validate: (input: string) => {
223
- if (!input && envVar.required) {
224
- return `${envVar.key} is required`;
225
- }
226
- return true;
227
- },
228
- },
229
- ]);
230
- result.set(envVar.key, answer.value);
231
- }
232
- }
233
- } else {
234
- // Interactive mode: prompt for everything
235
- logInfo('Interactive mode: You will be prompted for each environment variable.');
236
-
237
- for (const envVar of envVars) {
238
- const defaultValue = autoInfraUrls[envVar.key] || envVar.value || getSuggestedValue(envVar.key);
239
-
240
- const answer = await inquirer.prompt<{ value: string }>([
241
- {
242
- type: envVar.key.includes('PASSWORD') ? 'password' : 'input',
243
- name: 'value',
244
- message: `${envVar.key}${envVar.description ? ` (${envVar.description})` : ''}:`,
245
- default: defaultValue,
246
- validate: (input: string) => {
247
- if (!input && envVar.required) {
248
- return `${envVar.key} is required`;
249
- }
250
- return true;
251
- },
252
- },
253
- ]);
254
- result.set(envVar.key, answer.value);
255
- }
256
- }
257
-
258
- logSuccess('Environment variables configured');
259
- return result;
260
- }
261
-
262
- /**
263
- * Get suggested value for specific keys
264
- */
265
- function getSuggestedValue(key: string): string {
266
- if (key === 'NS_API_KEY' || key === 'AUTH_SECRET') {
267
- return `Replace with: openssl rand -base64 32`;
268
- }
269
- if (key === 'NODE_ENV') {
270
- return 'production';
271
- }
272
- if (key === 'ADMIN_USERNAME') {
273
- return 'admin';
274
- }
275
- return '';
276
- }
277
-
278
- /**
279
- * Generate .env file from variables
280
- */
281
- export async function generateEnvFile(
282
- targetDir: string,
283
- envVars: Map<string, string>
284
- ): Promise<void> {
285
- const envPath = path.join(targetDir, '.env');
286
-
287
- let content = '# SimpleNS Environment Configuration\n';
288
- content += '# Generated by @simplens/onboard\n\n';
289
-
290
- for (const [key, value] of envVars.entries()) {
291
- content += `${key}=${value}\n`;
292
- }
293
-
294
- await writeFile(envPath, content);
295
- logSuccess('Generated .env file');
296
- }
297
-
298
- /**
299
- * Append plugin credentials to .env file
300
- */
301
- export async function appendPluginEnv(
302
- targetDir: string,
303
- pluginEnvVars: Map<string, string>
304
- ): Promise<void> {
305
- // Only append if there are actually credentials to add
306
- if (pluginEnvVars.size === 0) {
307
- logInfo('No plugin credentials to add');
308
- return;
309
- }
310
-
311
- const envPath = path.join(targetDir, '.env');
312
-
313
- let content = '\n# Plugin Credentials\n';
314
- for (const [key, value] of pluginEnvVars.entries()) {
315
- content += `${key}=${value}\n`;
316
- }
317
-
318
- await appendFile(envPath, content);
319
- logSuccess('Added plugin credentials to .env');
320
- }
1
+ import { writeFile, appendFile, logInfo, logSuccess, logWarning } from './utils.js';
2
+ import { text, password } from '@clack/prompts';
3
+ import { handleCancel } from './ui.js';
4
+ import path from 'path';
5
+ import crypto from 'crypto';
6
+ import { CRITICAL_ENV_KEYS } from './config/constants.js';
7
+ import type { EnvVariable } from './types/domain.js';
8
+
9
+ export const DEFAULT_BASE_PATH = '';
10
+
11
+ /**
12
+ * Generate a secure random string for credentials
13
+ */
14
+ export function generateSecureRandom(length: number = 32): string {
15
+ return crypto.randomBytes(length).toString('base64').slice(0, length);
16
+ }
17
+
18
+ /**
19
+ * Generate default value for a critical environment variable
20
+ */
21
+ export function generateDefaultValue(key: string): string {
22
+ if (key === 'NS_API_KEY') {
23
+ return `sk_${generateSecureRandom(48)}`;
24
+ }
25
+ if (key === 'AUTH_SECRET') {
26
+ return generateSecureRandom(64);
27
+ }
28
+ if (key === 'ADMIN_PASSWORD') {
29
+ return `Admin${generateSecureRandom(16)}`;
30
+ }
31
+ return '';
32
+ }
33
+
34
+ /**
35
+ * Validate BASE_PATH value.
36
+ * Accepts:
37
+ * - Empty value for root path
38
+ * - Slash-prefixed lowercase segments (e.g. /dashboard, /admin/v1)
39
+ */
40
+ export function validateBasePath(input: string): true | string {
41
+ const value = input.trim();
42
+
43
+ if (!value) {
44
+ return true;
45
+ }
46
+
47
+ if (!value.startsWith('/')) {
48
+ return 'Base path must start with / (example: /dashboard)';
49
+ }
50
+
51
+ if (value.endsWith('/')) {
52
+ return 'Base path must not end with /';
53
+ }
54
+
55
+ if (!/^\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(value)) {
56
+ return 'Use lowercase letters, numbers, hyphens, and "/" separators only';
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Normalize BASE_PATH for consistent downstream use.
64
+ */
65
+ export function normalizeBasePath(input: string): string {
66
+ return input.trim();
67
+ }
68
+
69
+ /**
70
+ * Prompt BASE_PATH once at the beginning of onboarding.
71
+ */
72
+ export async function promptBasePath(defaultValue: string = DEFAULT_BASE_PATH): Promise<string> {
73
+ const result = await text({
74
+ message: 'BASE_PATH for dashboard (leave empty for root, example: /dashboard):',
75
+ placeholder: defaultValue || 'leave empty for root',
76
+ defaultValue,
77
+ validate: (value: string | undefined) => {
78
+ const v = validateBasePath(value ?? '');
79
+ return v === true ? undefined : v;
80
+ },
81
+ withGuide: true,
82
+ });
83
+
84
+ handleCancel(result);
85
+ return normalizeBasePath(result as string);
86
+ }
87
+
88
+ /**
89
+ * Load and parse .env.example from embedded template
90
+ */
91
+ export async function loadEnvExample(): Promise<EnvVariable[]> {
92
+ // Embedded .env template - always available regardless of installation
93
+ const envTemplate = `
94
+ NODE_ENV=production
95
+ # ============================================
96
+ # API SERVER
97
+ # ============================================
98
+ NS_API_KEY=
99
+ PORT=3000
100
+ MAX_BATCH_REQ_LIMIT=1000
101
+
102
+ # ============================================
103
+ # DATABASE
104
+ # ============================================
105
+ MONGO_URI=
106
+
107
+ # ============================================
108
+ # KAFKA
109
+ # ============================================
110
+ BROKERS=
111
+
112
+ # Kafka Topic Partitions (Core Topics)
113
+ DELAYED_PARTITION=1
114
+ NOTIFICATION_STATUS_PARTITION=1
115
+
116
+ # ============================================
117
+ # REDIS
118
+ # ============================================
119
+ REDIS_URL=
120
+
121
+ # ============================================
122
+ # PLUGIN SYSTEM
123
+ # ============================================
124
+ SIMPLENS_CONFIG_PATH=./simplens.config.yaml
125
+ PROCESSOR_CHANNEL=all
126
+
127
+ # ============================================
128
+ # BACKGROUND WORKER
129
+ # ============================================
130
+ OUTBOX_POLL_INTERVAL_MS=5000
131
+ OUTBOX_CLEANUP_INTERVAL_MS=60000
132
+ OUTBOX_BATCH_SIZE=100
133
+ OUTBOX_RETENTION_MS=300000
134
+ OUTBOX_CLAIM_TIMEOUT_MS=30000
135
+
136
+ # ============================================
137
+ # RETRY & IDEMPOTENCY
138
+ # ============================================
139
+ IDEMPOTENCY_TTL_SECONDS=86400
140
+ MAX_RETRY_COUNT=5
141
+ PROCESSING_TTL_SECONDS=120
142
+
143
+ # ============================================
144
+ # DELAYED NOTIFICATIONS
145
+ # ============================================
146
+ DELAYED_POLL_INTERVAL_MS=1000
147
+ DELAYED_BATCH_SIZE=10
148
+ MAX_POLLER_RETRIES=3
149
+
150
+ # ============================================
151
+ # RECOVERY SERVICE
152
+ # ============================================
153
+ RECOVERY_POLL_INTERVAL_MS=60000
154
+ PROCESSING_STUCK_THRESHOLD_MS=300000
155
+ PENDING_STUCK_THRESHOLD_MS=300000
156
+ RECOVERY_BATCH_SIZE=50
157
+ RECOVERY_CLAIM_TIMEOUT_MS=60000
158
+
159
+ # ============================================
160
+ # CLEANUP
161
+ # ============================================
162
+ CLEANUP_RESOLVED_ALERTS_RETENTION_MS=86400000
163
+ CLEANUP_PROCESSED_STATUS_OUTBOX_RETENTION_MS=86400000
164
+
165
+ # ============================================
166
+ # LOGGING
167
+ # ============================================
168
+ LOKI_URL=
169
+ LOG_LEVEL=info
170
+ LOG_TO_FILE=true
171
+
172
+ # ============================================
173
+ # DOCKER IMAGE VERSIONS
174
+ # ============================================
175
+ CORE_VERSION=latest
176
+ DASHBOARD_VERSION=latest
177
+
178
+ # ============================================
179
+ # ADMIN DASHBOARD
180
+ # ============================================
181
+ AUTH_SECRET=
182
+ ADMIN_USERNAME=admin
183
+ ADMIN_PASSWORD=
184
+ AUTH_TRUST_HOST=true
185
+ API_BASE_URL=http://api:3000
186
+ WEBHOOK_HOST=dashboard
187
+ WEBHOOK_PORT=3002
188
+ BASE_PATH=
189
+ DASHBOARD_PORT=3002
190
+ `;
191
+
192
+ return parseEnvContent(envTemplate);
193
+ }
194
+
195
+ /**
196
+ * Parse .env content into structured format
197
+ */
198
+ function parseEnvContent(content: string): EnvVariable[] {
199
+ const lines = content.split('\n');
200
+ const variables: EnvVariable[] = [];
201
+ let currentComment = '';
202
+
203
+ for (const line of lines) {
204
+ const trimmed = line.trim();
205
+
206
+ // Capture comments as descriptions
207
+ if (trimmed.startsWith('#') && !trimmed.includes('====')) {
208
+ currentComment = trimmed.replace(/^#\s*/, '');
209
+ continue;
210
+ }
211
+
212
+ // Skip empty lines and section dividers
213
+ if (!trimmed || trimmed.includes('====')) {
214
+ currentComment = '';
215
+ continue;
216
+ }
217
+
218
+ // Parse key=value pairs
219
+ const match = trimmed.match(/^([A-Z_]+)=(.*)$/);
220
+ if (match) {
221
+ const [, key, value] = match;
222
+ variables.push({
223
+ key,
224
+ value: value || '',
225
+ description: currentComment || undefined,
226
+ required: CRITICAL_ENV_KEYS.includes(key) || !value,
227
+ });
228
+ currentComment = '';
229
+ }
230
+ }
231
+
232
+ return variables;
233
+ }
234
+
235
+ /**
236
+ * Prompts user for environment variable values based on the selected mode.
237
+ *
238
+ * @param mode - 'default' prompts only for critical vars, 'interactive' prompts for all
239
+ * @param infraServices - List of selected infrastructure service IDs
240
+ * @param basePath - BASE_PATH value already collected
241
+ * @param fullMode - If true, auto-generate critical values without prompting
242
+ * @returns Map of environment variable keys to values
243
+ */
244
+ export async function promptEnvVariables(
245
+ mode: 'default' | 'interactive',
246
+ infraServices: string[],
247
+ basePath: string = DEFAULT_BASE_PATH,
248
+ fullMode: boolean = false
249
+ ): Promise<Map<string, string>> {
250
+ logInfo('Configuring environment variables...');
251
+
252
+ const envVars = await loadEnvExample();
253
+ const result = new Map<string, string>();
254
+ const normalizedBasePath = normalizeBasePath(basePath);
255
+ const basePathLabel = normalizedBasePath || '(root)';
256
+ logInfo(`BASE_PATH selected: ${basePathLabel}`);
257
+
258
+ // Auto-fill infra connection URLs based on selected services using Docker service names
259
+ const autoInfraUrls: Record<string, string> = {
260
+ MONGO_URI: infraServices.includes('mongo')
261
+ ? `mongodb://mongo:27017/simplens?replicaSet=rs0`
262
+ : '',
263
+ BROKERS: infraServices.includes('kafka')
264
+ ? 'kafka:9093'
265
+ : '',
266
+ REDIS_URL: infraServices.includes('redis')
267
+ ? 'redis://redis:6379'
268
+ : '',
269
+ LOKI_URL: infraServices.includes('loki')
270
+ ? 'http://loki:3100'
271
+ : '',
272
+ };
273
+
274
+ if (mode === 'default') {
275
+ // Use defaults, only prompt for critical values
276
+ for (const envVar of envVars) {
277
+ // Use auto-filled infra URLs
278
+ if (autoInfraUrls[envVar.key]) {
279
+ result.set(envVar.key, autoInfraUrls[envVar.key]);
280
+ continue;
281
+ }
282
+
283
+ // BASE_PATH is collected upfront in onboarding flow
284
+ if (envVar.key === 'BASE_PATH') {
285
+ result.set(envVar.key, normalizedBasePath);
286
+ continue;
287
+ }
288
+
289
+ // Use default value if available
290
+ if (envVar.value && !CRITICAL_ENV_KEYS.includes(envVar.key)) {
291
+ result.set(envVar.key, envVar.value);
292
+ continue;
293
+ }
294
+
295
+ // Prompt for critical values (only if not auto-filled)
296
+ if (CRITICAL_ENV_KEYS.includes(envVar.key)) {
297
+ if (fullMode) {
298
+ // In full mode, auto-generate critical values
299
+ const defaultValue = generateDefaultValue(envVar.key);
300
+ if (defaultValue) {
301
+ result.set(envVar.key, defaultValue);
302
+ }
303
+ } else {
304
+ // Show changelog info for version variables
305
+ if (envVar.key === 'CORE_VERSION' || envVar.key === 'DASHBOARD_VERSION') {
306
+ logInfo('ℹ️ Visit https://simplens.in/changelog for version information');
307
+ }
308
+
309
+ const promptMessage = `${envVar.key}${envVar.description ? ` (${envVar.description})` : ''}:`;
310
+ const isPasswordField = envVar.key.includes('PASSWORD');
311
+
312
+ let answer: string | symbol;
313
+ if (isPasswordField) {
314
+ answer = await password({
315
+ message: promptMessage,
316
+ validate: (input: string | undefined) => {
317
+ if (!input && envVar.required) {
318
+ return `${envVar.key} is required`;
319
+ }
320
+ return undefined;
321
+ },
322
+ });
323
+ } else {
324
+ answer = await text({
325
+ message: promptMessage,
326
+ placeholder: getSuggestedValue(envVar.key) || undefined,
327
+ defaultValue: getSuggestedValue(envVar.key) || undefined,
328
+ validate: (input: string | undefined) => {
329
+ if (!input && envVar.required) {
330
+ return `${envVar.key} is required`;
331
+ }
332
+ return undefined;
333
+ },
334
+ });
335
+ }
336
+
337
+ handleCancel(answer);
338
+ result.set(envVar.key, answer as string);
339
+ }
340
+ }
341
+ }
342
+ } else {
343
+ // Interactive mode: prompt for everything
344
+ logInfo('Interactive mode: You will be prompted for each environment variable.');
345
+
346
+ for (const envVar of envVars) {
347
+ const defaultValue = autoInfraUrls[envVar.key] || envVar.value || getSuggestedValue(envVar.key);
348
+
349
+ // BASE_PATH is collected upfront in onboarding flow
350
+ if (envVar.key === 'BASE_PATH') {
351
+ result.set(envVar.key, normalizedBasePath);
352
+ continue;
353
+ }
354
+
355
+ // Show changelog info for version variables
356
+ if (envVar.key === 'CORE_VERSION' || envVar.key === 'DASHBOARD_VERSION') {
357
+ logInfo('ℹ️ Visit https://simplens.in/changelog for version information');
358
+ }
359
+
360
+ const promptMessage = `${envVar.key}${envVar.description ? ` (${envVar.description})` : ''}:`;
361
+ const isPasswordField = envVar.key.includes('PASSWORD');
362
+
363
+ let answer: string | symbol;
364
+ if (isPasswordField) {
365
+ answer = await password({
366
+ message: promptMessage,
367
+ validate: (input: string | undefined) => {
368
+ if (!input && envVar.required) {
369
+ return `${envVar.key} is required`;
370
+ }
371
+ return undefined;
372
+ },
373
+ });
374
+ } else {
375
+ answer = await text({
376
+ message: promptMessage,
377
+ placeholder: defaultValue || undefined,
378
+ defaultValue: defaultValue || undefined,
379
+ validate: (input: string | undefined) => {
380
+ if (!input && envVar.required) {
381
+ return `${envVar.key} is required`;
382
+ }
383
+ return undefined;
384
+ },
385
+ });
386
+ }
387
+
388
+ handleCancel(answer);
389
+ result.set(envVar.key, answer as string);
390
+ }
391
+ }
392
+
393
+ logSuccess('Environment variables configured');
394
+ return result;
395
+ }
396
+
397
+ /**
398
+ * Get suggested value for specific keys
399
+ */
400
+ function getSuggestedValue(key: string): string {
401
+ if (key === 'NS_API_KEY' || key === 'AUTH_SECRET') {
402
+ return `Replace with: openssl rand -base64 32`;
403
+ }
404
+ if (key === 'NODE_ENV') {
405
+ return 'production';
406
+ }
407
+ if (key === 'ADMIN_USERNAME') {
408
+ return 'admin';
409
+ }
410
+ return '';
411
+ }
412
+
413
+ /**
414
+ * Generate .env file from variables
415
+ */
416
+ export async function generateEnvFile(
417
+ targetDir: string,
418
+ envVars: Map<string, string>
419
+ ): Promise<void> {
420
+ const envPath = path.join(targetDir, '.env');
421
+
422
+ let content = '# SimpleNS Environment Configuration\n';
423
+ content += '# Generated by @simplens/onboard\n\n';
424
+
425
+ for (const [key, value] of envVars.entries()) {
426
+ content += `${key}=${value}\n`;
427
+ }
428
+
429
+ await writeFile(envPath, content);
430
+ logSuccess('Generated .env file');
431
+ }
432
+
433
+ /**
434
+ * Append plugin credentials to .env file
435
+ */
436
+ export async function appendPluginEnv(
437
+ targetDir: string,
438
+ pluginEnvVars: Map<string, string>
439
+ ): Promise<void> {
440
+ // Only append if there are actually credentials to add
441
+ if (pluginEnvVars.size === 0) {
442
+ logInfo('No plugin credentials to add');
443
+ return;
444
+ }
445
+
446
+ const envPath = path.join(targetDir, '.env');
447
+
448
+ let content = '\n# Plugin Credentials\n';
449
+ for (const [key, value] of pluginEnvVars.entries()) {
450
+ content += `${key}=${value}\n`;
451
+ }
452
+
453
+ await appendFile(envPath, content);
454
+ logSuccess('Added plugin credentials to .env');
455
+ }