@simplens/onboard 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/infra.ts DELETED
@@ -1,514 +0,0 @@
1
- import {
2
- APP_COMPOSE_TEMPLATE,
3
- APP_NGINX_SERVICE_TEMPLATE,
4
- APP_NGINX_SSL_SERVICE_TEMPLATE,
5
- APP_CERTBOT_SERVICES_TEMPLATE,
6
- INFRA_CERTBOT_SERVICES_TEMPLATE,
7
- INFRA_CERTBOT_VOLUMES,
8
- } from './templates.js';
9
- import { writeFile, logInfo, logSuccess } from './utils.js';
10
- import { multiselect } from '@clack/prompts';
11
- import { handleCancel, spinner } from './ui.js';
12
- import path from 'path';
13
- import type { InfraService } from './types/domain.js';
14
-
15
- const INFRA_SERVICES: InfraService[] = [
16
- { name: 'MongoDB (Database)', value: 'mongo', checked: true },
17
- { name: 'Kafka (Message Queue)', value: 'kafka', checked: true },
18
- { name: 'Kafka UI (Dashboard)', value: 'kafka-ui', checked: false },
19
- { name: 'Redis (Cache)', value: 'redis', checked: true },
20
- { name: 'Nginx (Reverse Proxy)', value: 'nginx', checked: false },
21
- { name: 'Loki (Log Aggregation)', value: 'loki', checked: false },
22
- { name: 'Grafana (Observability Dashboard)', value: 'grafana', checked: false },
23
- ];
24
-
25
- /**
26
- * Prompts user to select which infrastructure services to deploy.
27
- *
28
- * @returns Array of selected service IDs (e.g., ['mongo', 'kafka', 'redis'])
29
- * @throws Error if no services are selected
30
- */
31
- export async function promptInfraServices(): Promise<string[]> {
32
- return promptInfraServicesWithBasePath({ allowNginx: true });
33
- }
34
-
35
- /**
36
- * Prompts infrastructure services with optional nginx availability.
37
- * If nginx is disabled, it is removed from choices and from result safety-check.
38
- */
39
- export async function promptInfraServicesWithBasePath(options: {
40
- allowNginx: boolean;
41
- defaultNginx?: boolean;
42
- }): Promise<string[]> {
43
- const choices = options.allowNginx
44
- ? INFRA_SERVICES.map(service => {
45
- if (service.value === 'nginx') {
46
- return { ...service, checked: options.defaultNginx === true };
47
- }
48
- return service;
49
- })
50
- : INFRA_SERVICES.filter(service => service.value !== 'nginx');
51
-
52
- const message = options.allowNginx
53
- ? 'Select infrastructure services to run (Space to select, Enter to confirm):'
54
- : 'Select infrastructure services to run (Space to select, Enter to confirm) — nginx disabled:';
55
-
56
- const selected = await multiselect({
57
- message,
58
- options: choices.map(s => ({
59
- value: s.value,
60
- label: s.name,
61
- hint: s.checked ? 'recommended' : undefined,
62
- })),
63
- initialValues: choices.filter(s => s.checked).map(s => s.value),
64
- required: true,
65
- withGuide: true,
66
- });
67
-
68
- handleCancel(selected);
69
- const result = selected as string[];
70
-
71
- if (options.allowNginx) {
72
- return result;
73
- }
74
-
75
- return result.filter(service => service !== 'nginx');
76
- }
77
-
78
-
79
-
80
- /**
81
- * Service chunk definitions - each service as a complete block
82
- */
83
- const SERVICE_CHUNKS: Record<string, string> = {
84
- 'mongo': ` mongo:
85
- image: mongo:7.0
86
- container_name: mongo
87
- command: [ "--replSet", "rs0", "--bind_ip_all", "--port", "27017" ]
88
- ports:
89
- - 27017:27017
90
- healthcheck:
91
- test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongo:27017'}]}) }" | mongosh --port 27017 --quiet
92
- interval: 5s
93
- timeout: 30s
94
- start_period: 0s
95
- start_interval: 1s
96
- retries: 30
97
- volumes:
98
- - "mongo_data:/data/db"
99
- - "mongo_config:/data/configdb"`,
100
-
101
- 'kafka': ` kafka:
102
- image: apache/kafka-native
103
- container_name: kafka
104
- ports:
105
- - "9092:9092"
106
- environment:
107
- # Configure listeners for both docker and host communication
108
- KAFKA_LISTENERS: CONTROLLER://localhost:9091,HOST://0.0.0.0:9092,DOCKER://0.0.0.0:9093
109
- KAFKA_ADVERTISED_LISTENERS: HOST://kafka:9092,DOCKER://kafka:9093
110
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,DOCKER:PLAINTEXT,HOST:PLAINTEXT
111
-
112
- # Settings required for KRaft mode
113
- KAFKA_NODE_ID: 1
114
- KAFKA_PROCESS_ROLES: broker,controller
115
- KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
116
- KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9091
117
-
118
- # Listener to use for broker-to-broker communication
119
- KAFKA_INTER_BROKER_LISTENER_NAME: DOCKER
120
-
121
- # Required for a single node cluster
122
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
123
-
124
- # Disable auto-topic creation - API server will create topics with correct partitions
125
- KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"
126
- volumes:
127
- - "kafka_data:/var/lib/kafka/data"`,
128
-
129
- 'kafka-ui': ` kafka-ui:
130
- image: kafbat/kafka-ui:main
131
- container_name: kafka-ui
132
- ports:
133
- - 8080:8080
134
- environment:
135
- DYNAMIC_CONFIG_ENABLED: "true"
136
- KAFKA_CLUSTERS_0_NAME: local
137
- KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9093
138
- depends_on:
139
- - kafka`,
140
-
141
- 'redis': ` redis:
142
- image: redis:7-alpine
143
- container_name: redis
144
- ports:
145
- - "6379:6379"
146
- command: redis-server --appendonly yes
147
- volumes:
148
- - "redis_data:/data"
149
- healthcheck:
150
- test: [ "CMD", "redis-cli", "ping" ]
151
- interval: 5s
152
- timeout: 3s
153
- retries: 5`,
154
-
155
- 'loki': ` loki:
156
- image: grafana/loki:2.9.0
157
- container_name: loki
158
- ports:
159
- - "3100:3100"
160
- command: -config.file=/etc/loki/local-config.yaml
161
- volumes:
162
- - "loki_data:/loki"
163
- healthcheck:
164
- test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1" ]
165
- interval: 10s
166
- timeout: 5s
167
- retries: 5`,
168
-
169
- 'grafana': ` grafana:
170
- image: grafana/grafana:10.2.0
171
- container_name: grafana
172
- ports:
173
- - "3001:3000"
174
- environment:
175
- - GF_PATHS_PROVISIONING=/etc/grafana/provisioning
176
- - GF_AUTH_ANONYMOUS_ENABLED=true
177
- - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
178
- - GF_SECURITY_ADMIN_PASSWORD=admin
179
- volumes:
180
- - "grafana_data:/var/lib/grafana"
181
- depends_on:
182
- loki:
183
- condition: service_healthy`,
184
-
185
- 'nginx': ` nginx:
186
- image: nginx:alpine
187
- container_name: nginx
188
- ports:
189
- - "80:80"
190
- volumes:
191
- - "./nginx.conf:/etc/nginx/conf.d/default.conf:ro"
192
- restart: unless-stopped`,
193
- };
194
-
195
- const NGINX_INFRA_SSL_SERVICE_CHUNK = ` nginx:
196
- image: nginx:alpine
197
- container_name: nginx
198
- ports:
199
- - "80:80"
200
- - "443:443"
201
- volumes:
202
- - "./nginx.conf:/etc/nginx/conf.d/default.conf:ro"
203
- - certbot-etc:/etc/letsencrypt
204
- - certbot-www:/var/www/certbot
205
- restart: unless-stopped`;
206
-
207
- /**
208
- * Service-to-volumes mapping
209
- */
210
- const SERVICE_VOLUMES: Record<string, string[]> = {
211
- 'mongo': ['mongo_data', 'mongo_config'],
212
- 'kafka': ['kafka_data'],
213
- 'kafka-ui': [],
214
- 'redis': ['redis_data'],
215
- 'nginx': [],
216
- 'loki': ['loki_data'],
217
- 'grafana': ['grafana_data'],
218
- };
219
-
220
- /**
221
- * Build docker-compose content from selected services
222
- */
223
- function buildInfraCompose(
224
- selectedServices: string[],
225
- options: { includeSsl?: boolean } = {}
226
- ): string {
227
- // Header
228
- const header = `# ============================================
229
- # SimpleNS Infrastructure Services
230
- # All services use Docker service names for container-to-container communication.
231
- # This ensures cross-platform compatibility (Windows, Linux, macOS).
232
- # ============================================
233
-
234
- services:
235
- # ============================================
236
- # Infrastructure Services
237
- # ============================================`;
238
-
239
- // Assemble selected service chunks
240
- const serviceBlocks: string[] = [];
241
- for (const service of selectedServices) {
242
- if (SERVICE_CHUNKS[service]) {
243
- if (service === 'nginx' && options.includeSsl === true) {
244
- serviceBlocks.push(NGINX_INFRA_SSL_SERVICE_CHUNK);
245
- } else {
246
- serviceBlocks.push(SERVICE_CHUNKS[service]);
247
- }
248
- }
249
- }
250
-
251
- if (options.includeSsl === true) {
252
- serviceBlocks.push(INFRA_CERTBOT_SERVICES_TEMPLATE);
253
- }
254
-
255
- // Collect volumes for selected services
256
- const volumeSet = new Set<string>();
257
- for (const service of selectedServices) {
258
- const volumes = SERVICE_VOLUMES[service] || [];
259
- volumes.forEach(v => volumeSet.add(v));
260
- }
261
-
262
- if (options.includeSsl === true) {
263
- for (const volumeName of INFRA_CERTBOT_VOLUMES) {
264
- volumeSet.add(volumeName);
265
- }
266
- }
267
-
268
- // Build volumes section
269
- const volumeLines: string[] = ['', 'volumes:'];
270
- for (const volume of Array.from(volumeSet).sort()) {
271
- volumeLines.push(` ${volume}:`);
272
- }
273
-
274
- // Build networks section with custom default network name
275
- const networkLines: string[] = ['', 'networks:', ' default:', ' name: simplens'];
276
-
277
- // Combine all parts
278
- return [
279
- header,
280
- serviceBlocks.join('\n\n'),
281
- volumeLines.join('\n'),
282
- networkLines.join('\n'),
283
- ].join('\n');
284
- }
285
-
286
-
287
-
288
- /**
289
- * Generate and write docker-compose.infra.yaml
290
- */
291
- export async function generateInfraCompose(
292
- targetDir: string,
293
- selectedServices: string[],
294
- options: { includeSsl?: boolean } = {}
295
- ): Promise<void> {
296
- const s = spinner();
297
- s.start('Generating docker-compose.infra.yaml...');
298
-
299
- // Build compose content from service chunks
300
- const infraContent = buildInfraCompose(selectedServices, options);
301
-
302
- // Write infrastructure compose file
303
- const infraPath = path.join(targetDir, 'docker-compose.infra.yaml');
304
- await writeFile(infraPath, infraContent);
305
- s.stop('Generated docker-compose.infra.yaml');
306
- }
307
-
308
- /**
309
- * Build app docker-compose content.
310
- * Optionally inject nginx reverse-proxy service before the volumes section.
311
- */
312
- export function buildAppComposeContent(
313
- includeNginx: boolean,
314
- options: { includeSsl?: boolean } = {}
315
- ): string {
316
- let content = APP_COMPOSE_TEMPLATE;
317
- const includeSsl = options.includeSsl === true;
318
- const shouldIncludeNginx = includeNginx || includeSsl;
319
- const marker = '\nvolumes:';
320
-
321
- if (!content.includes(marker)) {
322
- return content;
323
- }
324
-
325
- if (shouldIncludeNginx) {
326
- const nginxBlock = includeSsl
327
- ? APP_NGINX_SSL_SERVICE_TEMPLATE
328
- : APP_NGINX_SERVICE_TEMPLATE;
329
- content = content.replace(marker, `\n${nginxBlock}\n${marker}`);
330
- }
331
-
332
- if (includeSsl) {
333
- content = content.replace(marker, `\n${APP_CERTBOT_SERVICES_TEMPLATE}\n${marker}`);
334
- content = content.replace(
335
- '\nvolumes:\n plugin-data:',
336
- '\nvolumes:\n certbot-etc:\n certbot-www:\n plugin-data:'
337
- );
338
- }
339
-
340
- return content;
341
- }
342
-
343
- /**
344
- * Write app docker-compose.yaml
345
- */
346
- export async function writeAppCompose(
347
- targetDir: string,
348
- options: { includeNginx?: boolean; includeSsl?: boolean } = {}
349
- ): Promise<void> {
350
- const s = spinner();
351
- s.start('Generating docker-compose.yaml...');
352
- const appPath = path.join(targetDir, 'docker-compose.yaml');
353
- const appContent = buildAppComposeContent(options.includeNginx === true, {
354
- includeSsl: options.includeSsl === true,
355
- });
356
- await writeFile(appPath, appContent);
357
- s.stop('Generated docker-compose.yaml');
358
- }
359
-
360
- /**
361
- * Generate nginx.conf based on basePath configuration
362
- */
363
- export async function generateNginxConfig(
364
- targetDir: string,
365
- basePath: string,
366
- options: { enableSsl?: boolean; domain?: string; sslMode?: 'bootstrap' | 'final' } = {}
367
- ): Promise<void> {
368
- const s = spinner();
369
- s.start('Generating nginx.conf...');
370
-
371
- // Normalize basePath (remove leading/trailing slashes for template)
372
- const normalizedPath = basePath.trim().replace(/^\/|\/$/g, '');
373
- const hasBasePath = normalizedPath.length > 0;
374
- const enableSsl = options.enableSsl === true;
375
- const domain = options.domain?.trim() || 'localhost';
376
- const sslMode = options.sslMode ?? 'final';
377
-
378
- const proxyRoutes = `
379
- location /api {
380
- proxy_pass http://api:3000;
381
- proxy_http_version 1.1;
382
-
383
- proxy_set_header Host $host;
384
- proxy_set_header X-Real-IP $remote_addr;
385
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
386
- proxy_set_header X-Forwarded-Proto $scheme;
387
- }
388
-
389
- location = /runtime-config.js {
390
- proxy_pass http://dashboard:3002/runtime-config.js;
391
- proxy_http_version 1.1;
392
-
393
- proxy_set_header Host $host;
394
- proxy_set_header X-Real-IP $remote_addr;
395
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
396
- proxy_set_header X-Forwarded-Proto $scheme;
397
-
398
- # optional: prevent caching if config is dynamic
399
- add_header Cache-Control "no-store";
400
- }
401
-
402
- location ^~ /_next/ {
403
- proxy_pass http://dashboard:3002/_next/;
404
- proxy_http_version 1.1;
405
-
406
- proxy_set_header Host $host;
407
- proxy_set_header X-Real-IP $remote_addr;
408
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
409
- proxy_set_header X-Forwarded-Proto $scheme;
410
-
411
- expires 1y;
412
- add_header Cache-Control "public, immutable";
413
- }
414
- ${hasBasePath ? `
415
- location ^~ /${normalizedPath}/_next/ {
416
- proxy_pass http://dashboard:3002/_next/;
417
- proxy_http_version 1.1;
418
-
419
- proxy_set_header Host $host;
420
- proxy_set_header X-Real-IP $remote_addr;
421
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
422
- proxy_set_header X-Forwarded-Proto $scheme;
423
-
424
- expires 1y;
425
- add_header Cache-Control "public, immutable";
426
- }
427
- ` : ''}
428
- location ~* \\.(png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
429
- proxy_pass http://dashboard:3002;
430
- proxy_http_version 1.1;
431
-
432
- proxy_set_header Host $host;
433
- proxy_set_header X-Real-IP $remote_addr;
434
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
435
- proxy_set_header X-Forwarded-Proto $scheme;
436
-
437
- expires 1y;
438
- add_header Cache-Control "public, max-age=31536000";
439
- }
440
- ${hasBasePath ? `
441
- location ^~ /${normalizedPath} {
442
- proxy_pass http://dashboard:3002;
443
- proxy_http_version 1.1;
444
-
445
- proxy_set_header Host $host;
446
- proxy_set_header X-Real-IP $remote_addr;
447
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
448
- proxy_set_header X-Forwarded-Proto $scheme;
449
- }
450
- ` : `
451
- location / {
452
- proxy_pass http://dashboard:3002;
453
- proxy_http_version 1.1;
454
-
455
- proxy_set_header Host $host;
456
- proxy_set_header X-Real-IP $remote_addr;
457
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
458
- proxy_set_header X-Forwarded-Proto $scheme;
459
- }
460
- `}
461
- `;
462
-
463
- const rootRedirectPath = hasBasePath ? `/${normalizedPath}` : '/';
464
-
465
- const nginxTemplate = enableSsl
466
- ? sslMode === 'bootstrap'
467
- ? `server {
468
- listen 80;
469
- server_name ${domain};
470
-
471
- location /.well-known/acme-challenge/ {
472
- root /var/www/certbot;
473
- }
474
- ${proxyRoutes}
475
- }
476
- `
477
- : `server {
478
- listen 80;
479
- server_name ${domain};
480
-
481
- location /.well-known/acme-challenge/ {
482
- root /var/www/certbot;
483
- }
484
-
485
- location / {
486
- return 301 https://$host$request_uri;
487
- }
488
- }
489
-
490
- server {
491
- listen 443 ssl;
492
- server_name ${domain};
493
-
494
- ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
495
- ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
496
- ssl_protocols TLSv1.2 TLSv1.3;
497
- ssl_prefer_server_ciphers on;
498
- location = / {
499
- return 302 ${rootRedirectPath};
500
- }
501
- ${proxyRoutes}
502
- }
503
- `
504
- : `server {
505
- listen 80;
506
- server_name localhost;${proxyRoutes}
507
- }
508
- `;
509
-
510
- const nginxPath = path.join(targetDir, 'nginx.conf');
511
- await writeFile(nginxPath, nginxTemplate);
512
- const sslLabel = enableSsl ? `, SSL domain: ${domain}` : '';
513
- s.stop(`Generated nginx.conf${hasBasePath ? ` with base path: /${normalizedPath}` : ' (root path)'}${sslLabel}`);
514
- }