@simplens/onboard 1.0.0

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 (94) hide show
  1. package/README.md +214 -0
  2. package/dist/__tests__/errors.test.d.ts +2 -0
  3. package/dist/__tests__/errors.test.d.ts.map +1 -0
  4. package/dist/__tests__/errors.test.js +125 -0
  5. package/dist/__tests__/errors.test.js.map +1 -0
  6. package/dist/__tests__/utils.test.d.ts +2 -0
  7. package/dist/__tests__/utils.test.d.ts.map +1 -0
  8. package/dist/__tests__/utils.test.js +105 -0
  9. package/dist/__tests__/utils.test.js.map +1 -0
  10. package/dist/__tests__/validators.test.d.ts +2 -0
  11. package/dist/__tests__/validators.test.d.ts.map +1 -0
  12. package/dist/__tests__/validators.test.js +148 -0
  13. package/dist/__tests__/validators.test.js.map +1 -0
  14. package/dist/config/constants.d.ts +69 -0
  15. package/dist/config/constants.d.ts.map +1 -0
  16. package/dist/config/constants.js +79 -0
  17. package/dist/config/constants.js.map +1 -0
  18. package/dist/config/index.d.ts +2 -0
  19. package/dist/config/index.d.ts.map +1 -0
  20. package/dist/config/index.js +2 -0
  21. package/dist/config/index.js.map +1 -0
  22. package/dist/env-config.d.ts +33 -0
  23. package/dist/env-config.d.ts.map +1 -0
  24. package/dist/env-config.js +285 -0
  25. package/dist/env-config.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +153 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/infra.d.ts +31 -0
  31. package/dist/infra.d.ts.map +1 -0
  32. package/dist/infra.js +267 -0
  33. package/dist/infra.js.map +1 -0
  34. package/dist/plugins.d.ts +35 -0
  35. package/dist/plugins.d.ts.map +1 -0
  36. package/dist/plugins.js +164 -0
  37. package/dist/plugins.js.map +1 -0
  38. package/dist/services.d.ts +52 -0
  39. package/dist/services.d.ts.map +1 -0
  40. package/dist/services.js +158 -0
  41. package/dist/services.js.map +1 -0
  42. package/dist/templates.d.ts +3 -0
  43. package/dist/templates.d.ts.map +1 -0
  44. package/dist/templates.js +202 -0
  45. package/dist/templates.js.map +1 -0
  46. package/dist/types/domain.d.ts +119 -0
  47. package/dist/types/domain.d.ts.map +1 -0
  48. package/dist/types/domain.js +5 -0
  49. package/dist/types/domain.js.map +1 -0
  50. package/dist/types/errors.d.ts +69 -0
  51. package/dist/types/errors.d.ts.map +1 -0
  52. package/dist/types/errors.js +129 -0
  53. package/dist/types/errors.js.map +1 -0
  54. package/dist/types/index.d.ts +3 -0
  55. package/dist/types/index.d.ts.map +1 -0
  56. package/dist/types/index.js +3 -0
  57. package/dist/types/index.js.map +1 -0
  58. package/dist/utils/index.d.ts +2 -0
  59. package/dist/utils/index.d.ts.map +1 -0
  60. package/dist/utils/index.js +2 -0
  61. package/dist/utils/index.js.map +1 -0
  62. package/dist/utils/logger.d.ts +54 -0
  63. package/dist/utils/logger.d.ts.map +1 -0
  64. package/dist/utils/logger.js +92 -0
  65. package/dist/utils/logger.js.map +1 -0
  66. package/dist/utils.d.ts +32 -0
  67. package/dist/utils.d.ts.map +1 -0
  68. package/dist/utils.js +79 -0
  69. package/dist/utils.js.map +1 -0
  70. package/dist/validators.d.ts +93 -0
  71. package/dist/validators.d.ts.map +1 -0
  72. package/dist/validators.js +180 -0
  73. package/dist/validators.js.map +1 -0
  74. package/package.json +45 -0
  75. package/src/__tests__/errors.test.ts +187 -0
  76. package/src/__tests__/utils.test.ts +142 -0
  77. package/src/__tests__/validators.test.ts +195 -0
  78. package/src/config/constants.ts +86 -0
  79. package/src/config/index.ts +1 -0
  80. package/src/env-config.ts +320 -0
  81. package/src/index.ts +203 -0
  82. package/src/infra.ts +300 -0
  83. package/src/plugins.ts +190 -0
  84. package/src/services.ts +190 -0
  85. package/src/templates.ts +203 -0
  86. package/src/types/domain.ts +127 -0
  87. package/src/types/errors.ts +173 -0
  88. package/src/types/index.ts +2 -0
  89. package/src/utils/index.ts +1 -0
  90. package/src/utils/logger.ts +118 -0
  91. package/src/utils.ts +105 -0
  92. package/src/validators.ts +192 -0
  93. package/tsconfig.json +19 -0
  94. package/vitest.config.ts +20 -0
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ checkDockerInstalled,
4
+ checkDockerRunning,
5
+ detectOS,
6
+ validatePrerequisites,
7
+ validateEnvValue
8
+ } from '../validators.js';
9
+ import {
10
+ DockerNotInstalledError,
11
+ DockerNotRunningError,
12
+ DockerPermissionError
13
+ } from '../types/errors.js';
14
+
15
+ // Mock execa
16
+ vi.mock('execa', () => ({
17
+ execa: vi.fn(),
18
+ }));
19
+
20
+ // Mock utils
21
+ vi.mock('../utils.js', () => ({
22
+ logError: vi.fn(),
23
+ logSuccess: vi.fn(),
24
+ logWarning: vi.fn(),
25
+ }));
26
+
27
+ import { execa } from 'execa';
28
+
29
+ describe('validators', () => {
30
+ beforeEach(() => {
31
+ vi.clearAllMocks();
32
+ });
33
+
34
+ describe('checkDockerInstalled', () => {
35
+ it('should not throw when docker is installed', async () => {
36
+ vi.mocked(execa).mockResolvedValueOnce({
37
+ stdout: 'Docker version 24.0.0',
38
+ stderr: '',
39
+ } as any);
40
+
41
+ await expect(checkDockerInstalled()).resolves.not.toThrow();
42
+ });
43
+
44
+ it('should throw DockerNotInstalledError when docker is not installed', async () => {
45
+ vi.mocked(execa).mockRejectedValueOnce(new Error('Command not found'));
46
+
47
+ await expect(checkDockerInstalled()).rejects.toThrow(DockerNotInstalledError);
48
+ });
49
+ });
50
+
51
+ describe('checkDockerRunning', () => {
52
+ it('should not throw when docker daemon is running', async () => {
53
+ vi.mocked(execa).mockResolvedValueOnce({
54
+ stdout: 'CONTAINER ID IMAGE',
55
+ stderr: '',
56
+ } as any);
57
+
58
+ await expect(checkDockerRunning()).resolves.not.toThrow();
59
+ });
60
+
61
+ it('should throw DockerPermissionError on permission denied', async () => {
62
+ const error = new Error('permission denied while trying to connect');
63
+ vi.mocked(execa).mockRejectedValueOnce(error);
64
+
65
+ await expect(checkDockerRunning()).rejects.toThrow(DockerPermissionError);
66
+ });
67
+
68
+ it('should throw DockerNotRunningError when daemon is not running', async () => {
69
+ const error = new Error('Cannot connect to the Docker daemon');
70
+ vi.mocked(execa).mockRejectedValueOnce(error);
71
+
72
+ await expect(checkDockerRunning()).rejects.toThrow(DockerNotRunningError);
73
+ });
74
+
75
+ it('should throw DockerNotRunningError on generic docker error', async () => {
76
+ const error = new Error('Some other docker error');
77
+ vi.mocked(execa).mockRejectedValueOnce(error);
78
+
79
+ await expect(checkDockerRunning()).rejects.toThrow(DockerNotRunningError);
80
+ });
81
+ });
82
+
83
+ describe('detectOS', () => {
84
+ const originalPlatform = process.platform;
85
+
86
+ afterEach(() => {
87
+ Object.defineProperty(process, 'platform', {
88
+ value: originalPlatform
89
+ });
90
+ });
91
+
92
+ it('should detect Windows', () => {
93
+ Object.defineProperty(process, 'platform', {
94
+ value: 'win32'
95
+ });
96
+ expect(detectOS()).toBe('windows');
97
+ });
98
+
99
+ it('should detect macOS', () => {
100
+ Object.defineProperty(process, 'platform', {
101
+ value: 'darwin'
102
+ });
103
+ expect(detectOS()).toBe('darwin');
104
+ });
105
+
106
+ it('should detect Linux', () => {
107
+ Object.defineProperty(process, 'platform', {
108
+ value: 'linux'
109
+ });
110
+ expect(detectOS()).toBe('linux');
111
+ });
112
+
113
+ it('should default to Linux for unknown platforms', () => {
114
+ Object.defineProperty(process, 'platform', {
115
+ value: 'freebsd'
116
+ });
117
+ expect(detectOS()).toBe('linux');
118
+ });
119
+ });
120
+
121
+ describe('validateEnvValue', () => {
122
+ describe('URL validation', () => {
123
+ it('should reject empty MongoDB URI', () => {
124
+ expect(validateEnvValue('MONGO_URI', '')).toBe(false);
125
+ });
126
+
127
+ it('should reject MongoDB URI without proper format', () => {
128
+ expect(validateEnvValue('MONGO_URI', 'localhost:27017')).toBe(false);
129
+ });
130
+
131
+ it('should accept valid MongoDB URI', () => {
132
+ expect(validateEnvValue('MONGO_URI', 'mongodb://localhost:27017')).toBe(true);
133
+ });
134
+
135
+ it('should reject Redis URL without proper format', () => {
136
+ expect(validateEnvValue('REDIS_URL', 'localhost:6379')).toBe(false);
137
+ });
138
+
139
+ it('should accept valid Redis URL', () => {
140
+ expect(validateEnvValue('REDIS_URL', 'redis://localhost:6379')).toBe(true);
141
+ });
142
+ });
143
+
144
+ describe('Port validation', () => {
145
+ it('should reject negative ports', () => {
146
+ expect(validateEnvValue('PORT', '-1')).toBe(false);
147
+ });
148
+
149
+ it('should reject zero port', () => {
150
+ expect(validateEnvValue('PORT', '0')).toBe(false);
151
+ });
152
+
153
+ it('should reject ports > 65535', () => {
154
+ expect(validateEnvValue('PORT', '65536')).toBe(false);
155
+ });
156
+
157
+ it('should accept valid ports', () => {
158
+ expect(validateEnvValue('PORT', '3000')).toBe(true);
159
+ expect(validateEnvValue('API_PORT', '8080')).toBe(true);
160
+ });
161
+
162
+ it('should reject non-numeric ports', () => {
163
+ expect(validateEnvValue('PORT', 'abc')).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe('Security fields validation', () => {
168
+ it('should reject short API keys', () => {
169
+ expect(validateEnvValue('API_KEY', 'short')).toBe(false);
170
+ });
171
+
172
+ it('should reject short passwords', () => {
173
+ expect(validateEnvValue('PASSWORD', '1234567')).toBe(false);
174
+ });
175
+
176
+ it('should reject short secrets', () => {
177
+ expect(validateEnvValue('AUTH_SECRET', 'abc')).toBe(false);
178
+ });
179
+
180
+ it('should accept API keys with 8+ characters', () => {
181
+ expect(validateEnvValue('API_KEY', 'verylongapikey123')).toBe(true);
182
+ });
183
+
184
+ it('should accept passwords with 8+ characters', () => {
185
+ expect(validateEnvValue('PASSWORD', 'password123')).toBe(true);
186
+ });
187
+ });
188
+
189
+ describe('General validation', () => {
190
+ it('should accept valid non-special values', () => {
191
+ expect(validateEnvValue('SOME_VAR', 'some-value')).toBe(true);
192
+ });
193
+ });
194
+ });
195
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Application configuration constants
3
+ */
4
+
5
+ /**
6
+ * Service port mappings
7
+ */
8
+ export const SERVICE_PORTS = {
9
+ API: 3000,
10
+ DASHBOARD: 3002,
11
+ GRAFANA: 3001,
12
+ KAFKA_UI: 8080,
13
+ MONGO: 27017,
14
+ KAFKA: 9092,
15
+ REDIS: 6379,
16
+ LOKI: 3100,
17
+ } as const;
18
+
19
+ /**
20
+ * Health check configuration
21
+ */
22
+ export const HEALTH_CHECK = {
23
+ MAX_RETRIES: 30,
24
+ RETRY_DELAY_MS: 2000,
25
+ TIMEOUT_MS: 60000,
26
+ } as const;
27
+
28
+ /**
29
+ * Docker command timeouts
30
+ */
31
+ export const DOCKER_TIMEOUTS = {
32
+ START_MS: 30000,
33
+ STOP_MS: 15000,
34
+ BUILD_MS: 300000,
35
+ } as const;
36
+
37
+ /**
38
+ * Critical environment variables that always need user input
39
+ */
40
+ export const CRITICAL_ENV_KEYS = [
41
+ 'NS_API_KEY',
42
+ 'MONGO_URI',
43
+ 'BROKERS',
44
+ 'REDIS_URL',
45
+ 'AUTH_SECRET',
46
+ 'ADMIN_PASSWORD',
47
+ 'LOKI_URL',
48
+ ];
49
+
50
+ /**
51
+ * Validation constraints
52
+ */
53
+ export const VALIDATION = {
54
+ MIN_PASSWORD_LENGTH: 8,
55
+ MIN_API_KEY_LENGTH: 8,
56
+ PORT_MIN: 1,
57
+ PORT_MAX: 65535,
58
+ } as const;
59
+
60
+ /**
61
+ * File paths
62
+ */
63
+ export const FILES = {
64
+ DOCKER_COMPOSE_INFRA: 'docker-compose.infra.yaml',
65
+ DOCKER_COMPOSE_APP: 'docker-compose.yaml',
66
+ ENV_FILE: '.env',
67
+ CONFIG_FILE: 'simplens.config.yaml',
68
+ ERROR_LOG: 'onboard-error.log',
69
+ } as const;
70
+
71
+ /**
72
+ * URL templates for service access
73
+ */
74
+ export function getServiceURL(service: keyof typeof SERVICE_PORTS, host: string = 'localhost'): string {
75
+ return `http://${host}:${SERVICE_PORTS[service]}`;
76
+ }
77
+
78
+ /**
79
+ * Docker compose file paths
80
+ */
81
+ export const DOCKER_COMPOSE_COMMANDS = {
82
+ UP: ['up', '-d'],
83
+ DOWN: ['down'],
84
+ LOGS: ['logs', '-f'],
85
+ PS: ['ps'],
86
+ } as const;
@@ -0,0 +1 @@
1
+ export * from './constants.js';
@@ -0,0 +1,320 @@
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
+ }