@alcedocore/cli 0.0.1-rc.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 (70) hide show
  1. package/LICENSE.md +102 -0
  2. package/dist/commands/add-endpoint.js +104 -0
  3. package/dist/commands/add-migration.js +80 -0
  4. package/dist/commands/add-nav-item.js +72 -0
  5. package/dist/commands/add-page.js +64 -0
  6. package/dist/commands/build-frontend.js +126 -0
  7. package/dist/commands/compile-pages.js +120 -0
  8. package/dist/commands/connect.js +122 -0
  9. package/dist/commands/deploy.js +74 -0
  10. package/dist/commands/dev.js +13 -0
  11. package/dist/commands/init-core.js +900 -0
  12. package/dist/commands/init.js +124 -0
  13. package/dist/commands/init.test.js +84 -0
  14. package/dist/commands/migrate.js +22 -0
  15. package/dist/commands/proxy.js +157 -0
  16. package/dist/commands/publish.js +81 -0
  17. package/dist/commands/replay.js +142 -0
  18. package/dist/commands/serve-frontend.js +92 -0
  19. package/dist/config.js +110 -0
  20. package/dist/config.test.js +48 -0
  21. package/dist/index.js +81 -0
  22. package/dist/integration/cli-api.test.js +116 -0
  23. package/dist/utils/ejs-renderer.js +46 -0
  24. package/dist/utils/ejs-renderer.test.js +93 -0
  25. package/dist/utils/formatting.js +65 -0
  26. package/dist/utils/generateTimestamp.js +17 -0
  27. package/dist/utils/logger.js +33 -0
  28. package/dist/utils/validation.js +15 -0
  29. package/package.json +41 -0
  30. package/src/commands/add-endpoint.ts +157 -0
  31. package/src/commands/add-migration.ts +102 -0
  32. package/src/commands/add-nav-item.ts +106 -0
  33. package/src/commands/add-page.ts +100 -0
  34. package/src/commands/build-frontend.ts +148 -0
  35. package/src/commands/connect.ts +98 -0
  36. package/src/commands/deploy.ts +85 -0
  37. package/src/commands/dev.ts +12 -0
  38. package/src/commands/init-core.ts +1019 -0
  39. package/src/commands/init.test.ts +92 -0
  40. package/src/commands/init.ts +171 -0
  41. package/src/commands/migrate.ts +20 -0
  42. package/src/commands/proxy.ts +206 -0
  43. package/src/commands/publish.ts +106 -0
  44. package/src/commands/serve-frontend.ts +103 -0
  45. package/src/config.test.ts +50 -0
  46. package/src/config.ts +125 -0
  47. package/src/index.ts +100 -0
  48. package/src/integration/cli-api.test.ts +143 -0
  49. package/src/utils/ejs-renderer.ts +55 -0
  50. package/src/utils/formatting.ts +62 -0
  51. package/src/utils/generateTimestamp.ts +16 -0
  52. package/src/utils/logger.ts +27 -0
  53. package/src/utils/validation.ts +13 -0
  54. package/templates/endpoint/handler.js.ejs +23 -0
  55. package/templates/endpoint/handler.py.ejs +23 -0
  56. package/templates/migration/down.sql.ejs +6 -0
  57. package/templates/migration/up.sql.ejs +11 -0
  58. package/templates/page/page.vue.ejs +63 -0
  59. package/templates/plugin/Dockerfile.ejs +13 -0
  60. package/templates/plugin/Dockerfile.node.ejs +14 -0
  61. package/templates/plugin/README.md.ejs +19 -0
  62. package/templates/plugin/gitignore.ejs +6 -0
  63. package/templates/plugin/manifest.json.ejs +18 -0
  64. package/templates/plugin/migrations/.gitkeep +0 -0
  65. package/templates/plugin/pages/.gitkeep +0 -0
  66. package/templates/plugin/public/.gitkeep +0 -0
  67. package/templates/plugin/server.js.ejs +27 -0
  68. package/templates/plugin/server.py.ejs +32 -0
  69. package/tsconfig.json +16 -0
  70. package/vitest.config.ts +14 -0
@@ -0,0 +1,900 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.initCoreCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const node_fs_1 = __importDefault(require("node:fs"));
43
+ const node_crypto_1 = __importDefault(require("node:crypto"));
44
+ const node_child_process_1 = require("node:child_process");
45
+ const detect_port_1 = __importDefault(require("detect-port"));
46
+ const check_disk_space_1 = __importDefault(require("check-disk-space"));
47
+ const logger_1 = require("../utils/logger");
48
+ exports.initCoreCommand = new commander_1.Command("init-core")
49
+ .description("Generate configuration for running Alcedo Core (Docker Compose or Kubernetes)")
50
+ .option("-o, --output-dir <path>", "Output directory (default: ./alcedo)")
51
+ .option("-P, --platform <type>", "Platform: docker or k8s (default: docker, prompts if interactive)")
52
+ .option("-e, --admin-email <email>", "Admin email (skips prompt if provided)")
53
+ .option("-p, --admin-password <password>", "Admin password (skips prompt if provided)")
54
+ .option("--port <port>", "Core HTTP port", parseInt)
55
+ .option("--non-interactive", "Skip prompts, use defaults", false)
56
+ .action(async (options) => {
57
+ const targetDir = node_path_1.default.resolve(options.outputDir || "./alcedo");
58
+ if (node_fs_1.default.existsSync(targetDir) &&
59
+ node_fs_1.default.readdirSync(targetDir).length > 0) {
60
+ (0, logger_1.error)(`Directory already exists and is not empty: ${targetDir}`);
61
+ process.exit(1);
62
+ }
63
+ const defaults = {
64
+ platform: options.platform || "docker",
65
+ adminEmail: options.adminEmail || "admin@alcedo.dev",
66
+ adminPassword: options.adminPassword || generatePassword(),
67
+ dbPassword: generatePassword(),
68
+ corePort: options.port || 8080,
69
+ swarmEnabled: false,
70
+ coreImageTag: "alcedocore/core:0.1.0",
71
+ postgresVersion: "16",
72
+ redisPassword: "",
73
+ sessionTtl: 86400,
74
+ rateLimitRequests: 200,
75
+ rateLimitWindow: 60,
76
+ corsOrigins: "*",
77
+ enableCors: true,
78
+ enableRateLimit: true,
79
+ corePublicUrl: "",
80
+ registryPort: 5000,
81
+ systemPluginsUrl: "https://cspm.alcedocore.nl",
82
+ localRegistryUrl: "localhost:5000",
83
+ pluginNetwork: "alcedo_plugins",
84
+ registryEncryptionKey: generateRegistryKey(),
85
+ };
86
+ // Pre-flight checks (Docker only)
87
+ if (defaults.platform === "docker" &&
88
+ !(await preflightCheck(defaults))) {
89
+ (0, logger_1.error)("Pre-flight checks failed. Please fix the issues above and try again.");
90
+ process.exit(1);
91
+ }
92
+ try {
93
+ const config = options.nonInteractive
94
+ ? defaults
95
+ : await prompt(defaults);
96
+ await writeFiles(targetDir, config);
97
+ }
98
+ catch (err) {
99
+ (0, logger_1.error)(`Failed to generate configuration: ${err.message}`);
100
+ process.exit(1);
101
+ }
102
+ });
103
+ function generatePassword(length = 24) {
104
+ return node_crypto_1.default.randomBytes(length).toString("base64url").slice(0, length);
105
+ }
106
+ function generateRegistryKey() {
107
+ return node_crypto_1.default.randomBytes(32).toString("base64");
108
+ }
109
+ function checkDocker() {
110
+ try {
111
+ (0, node_child_process_1.execSync)("docker info --format '{{.ServerVersion}}'", {
112
+ stdio: "pipe",
113
+ timeout: 5000,
114
+ });
115
+ return true;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ async function checkPort(port) {
122
+ try {
123
+ const actual = await (0, detect_port_1.default)(port);
124
+ return actual === port; // if detectPort returns same port, it's free
125
+ }
126
+ catch {
127
+ return true; // on error, assume free
128
+ }
129
+ }
130
+ async function checkDiskSpaceFree(minGB = 1) {
131
+ try {
132
+ const disk = await (0, check_disk_space_1.default)("/");
133
+ const freeGB = Math.floor(disk.free / 1024 / 1024 / 1024);
134
+ return { ok: freeGB >= minGB, freeGB };
135
+ }
136
+ catch {
137
+ return { ok: true, freeGB: 99 };
138
+ }
139
+ }
140
+ async function preflightCheck(config) {
141
+ let ok = true;
142
+ if (!checkDocker()) {
143
+ (0, logger_1.error)("Docker is not installed or the daemon is not running.");
144
+ (0, logger_1.info)(" Install Docker: https://docs.docker.com/engine/install/");
145
+ ok = false;
146
+ }
147
+ const disk = await checkDiskSpaceFree();
148
+ if (!disk.ok) {
149
+ (0, logger_1.warn)(`Low disk space: only ${disk.freeGB}GB available (minimum 1GB recommended).`);
150
+ }
151
+ if (!(await checkPort(config.corePort))) {
152
+ (0, logger_1.warn)(`Port ${config.corePort} is already in use. The core may fail to start.`);
153
+ }
154
+ if (!(await checkPort(config.registryPort))) {
155
+ (0, logger_1.warn)(`Port ${config.registryPort} is already in use. The registry may fail to start.`);
156
+ }
157
+ return ok;
158
+ }
159
+ function detectSwarmState() {
160
+ try {
161
+ const out = (0, node_child_process_1.execSync)("docker info --format '{{.Swarm.LocalNodeState}}'", { encoding: "utf-8", timeout: 5000 }).trim();
162
+ if (out === "active")
163
+ return "active";
164
+ if (out === "inactive")
165
+ return "inactive";
166
+ return "disabled";
167
+ }
168
+ catch {
169
+ return "disabled";
170
+ }
171
+ }
172
+ async function prompt(config) {
173
+ const inquirer = (await Promise.resolve().then(() => __importStar(require("inquirer")))).default;
174
+ const DEFAULT_PLUGINS_URL = "https://cspm.alcedocore.nl";
175
+ // Platform selection
176
+ const platformAnswer = await inquirer.prompt([
177
+ {
178
+ type: "list",
179
+ name: "platform",
180
+ message: "Which platform?",
181
+ choices: [
182
+ {
183
+ name: "Docker Compose (local dev / single host)",
184
+ value: "docker",
185
+ },
186
+ { name: "Kubernetes (K8s cluster)", value: "k8s" },
187
+ ],
188
+ default: config.platform,
189
+ },
190
+ ]);
191
+ config.platform = platformAnswer.platform;
192
+ if (config.platform === "docker") {
193
+ // Detect Swarm state automatically
194
+ const swarmState = detectSwarmState();
195
+ if (swarmState === "active") {
196
+ config.swarmEnabled = true;
197
+ (0, logger_1.info)("Docker Swarm is active — Swarm support enabled.");
198
+ }
199
+ else if (swarmState === "inactive") {
200
+ const initSwarm = await inquirer.prompt([
201
+ {
202
+ type: "confirm",
203
+ name: "confirm",
204
+ message: "Docker Swarm is not initialized. Initialize it now?",
205
+ default: false,
206
+ },
207
+ ]);
208
+ if (initSwarm.confirm) {
209
+ try {
210
+ (0, node_child_process_1.execSync)("docker swarm init", {
211
+ stdio: "ignore",
212
+ timeout: 10000,
213
+ });
214
+ config.swarmEnabled = true;
215
+ (0, logger_1.info)("Docker Swarm initialized.");
216
+ }
217
+ catch {
218
+ (0, logger_1.info)("Failed to initialize Swarm — proceeding without Swarm support.");
219
+ config.swarmEnabled = false;
220
+ }
221
+ }
222
+ else {
223
+ config.swarmEnabled = false;
224
+ }
225
+ }
226
+ else {
227
+ (0, logger_1.info)("Docker Swarm is not available on this host — Swarm support disabled.");
228
+ config.swarmEnabled = false;
229
+ }
230
+ }
231
+ const basic = await inquirer.prompt([
232
+ {
233
+ type: "input",
234
+ name: "adminEmail",
235
+ message: "Admin email:",
236
+ default: config.adminEmail,
237
+ validate: (v) => v.includes("@") || "Please enter a valid email",
238
+ },
239
+ {
240
+ type: "password",
241
+ name: "adminPassword",
242
+ message: "Admin password:",
243
+ default: config.adminPassword,
244
+ validate: (v) => v.length >= 8 || "Password must be at least 8 characters",
245
+ },
246
+ {
247
+ type: "number",
248
+ name: "corePort",
249
+ message: "Core HTTP port:",
250
+ default: config.corePort,
251
+ },
252
+ {
253
+ type: "input",
254
+ name: "corePublicUrl",
255
+ message: "Core public URL (leave empty to use http://localhost:<port>):",
256
+ default: config.corePublicUrl,
257
+ },
258
+ {
259
+ type: "confirm",
260
+ name: "useCustomPluginsUrl",
261
+ message: "Do you want to use a custom system plugins manifest URL?",
262
+ default: false,
263
+ },
264
+ {
265
+ type: "confirm",
266
+ name: "advanced",
267
+ message: "Configure advanced options?",
268
+ default: false,
269
+ },
270
+ ]);
271
+ config.adminEmail = basic.adminEmail;
272
+ config.adminPassword = basic.adminPassword;
273
+ config.corePort = basic.corePort;
274
+ config.corePublicUrl = basic.corePublicUrl;
275
+ if (basic.useCustomPluginsUrl) {
276
+ const custom = await inquirer.prompt([
277
+ {
278
+ type: "input",
279
+ name: "systemPluginsUrl",
280
+ message: "Custom system plugins manifest URL:",
281
+ default: config.systemPluginsUrl,
282
+ },
283
+ ]);
284
+ config.systemPluginsUrl = custom.systemPluginsUrl;
285
+ }
286
+ else {
287
+ config.systemPluginsUrl = DEFAULT_PLUGINS_URL;
288
+ }
289
+ if (basic.advanced) {
290
+ const advanced = await inquirer.prompt([
291
+ {
292
+ type: "input",
293
+ name: "dbPassword",
294
+ message: "PostgreSQL password:",
295
+ default: config.dbPassword,
296
+ },
297
+ {
298
+ type: "input",
299
+ name: "redisPassword",
300
+ message: "Redis password (leave empty for none):",
301
+ default: config.redisPassword,
302
+ },
303
+ {
304
+ type: "input",
305
+ name: "coreImageTag",
306
+ message: "Core Docker image tag:",
307
+ default: config.coreImageTag,
308
+ },
309
+ {
310
+ type: "input",
311
+ name: "postgresVersion",
312
+ message: "PostgreSQL version:",
313
+ default: config.postgresVersion,
314
+ },
315
+ {
316
+ type: "number",
317
+ name: "sessionTtl",
318
+ message: "Session TTL (seconds):",
319
+ default: config.sessionTtl,
320
+ },
321
+ {
322
+ type: "confirm",
323
+ name: "enableCors",
324
+ message: "Enable CORS?",
325
+ default: config.enableCors,
326
+ },
327
+ {
328
+ type: "input",
329
+ name: "corsOrigins",
330
+ message: "Allowed CORS origins (comma-separated or *):",
331
+ default: config.corsOrigins,
332
+ when: (a) => a.enableCors,
333
+ },
334
+ {
335
+ type: "confirm",
336
+ name: "enableRateLimit",
337
+ message: "Enable rate limiting?",
338
+ default: config.enableRateLimit,
339
+ },
340
+ {
341
+ type: "number",
342
+ name: "rateLimitRequests",
343
+ message: "Max requests per window:",
344
+ default: config.rateLimitRequests,
345
+ when: (a) => a.enableRateLimit,
346
+ },
347
+ {
348
+ type: "number",
349
+ name: "rateLimitWindow",
350
+ message: "Rate limit window (seconds):",
351
+ default: config.rateLimitWindow,
352
+ when: (a) => a.enableRateLimit,
353
+ },
354
+ {
355
+ type: "input",
356
+ name: "pluginNetwork",
357
+ message: "Docker network name for plugin containers:",
358
+ default: config.pluginNetwork,
359
+ },
360
+ {
361
+ type: "number",
362
+ name: "registryPort",
363
+ message: "Docker registry port:",
364
+ default: config.registryPort,
365
+ },
366
+ {
367
+ type: "input",
368
+ name: "localRegistryUrl",
369
+ message: "Local registry URL (for plugin image storage):",
370
+ default: config.localRegistryUrl,
371
+ },
372
+ ]);
373
+ config.dbPassword = advanced.dbPassword;
374
+ config.redisPassword = advanced.redisPassword;
375
+ config.coreImageTag = advanced.coreImageTag;
376
+ config.postgresVersion = advanced.postgresVersion;
377
+ config.sessionTtl = advanced.sessionTtl;
378
+ config.enableCors = advanced.enableCors;
379
+ config.corsOrigins = advanced.corsOrigins;
380
+ config.enableRateLimit = advanced.enableRateLimit;
381
+ config.rateLimitRequests = advanced.rateLimitRequests;
382
+ config.rateLimitWindow = advanced.rateLimitWindow;
383
+ config.pluginNetwork = advanced.pluginNetwork;
384
+ config.registryPort = advanced.registryPort;
385
+ config.localRegistryUrl = advanced.localRegistryUrl;
386
+ }
387
+ return config;
388
+ }
389
+ function generateCompose(config) {
390
+ const securityVars = `
391
+ SECURITY_COOKIE_SECURE: "false"
392
+ SECURITY_COOKIE_SAMESITE: "lax"
393
+ SECURITY_COOKIE_HTTP_ONLY: "true"
394
+ SECURITY_CORS_ENABLED: "${config.enableCors}"
395
+ SECURITY_CORS_ORIGINS: "${config.corsOrigins}"
396
+ SECURITY_RATE_LIMIT_ENABLED: "${config.enableRateLimit}"
397
+ RATE_LIMIT_AUTH_REQUESTS: "${config.rateLimitRequests}"
398
+ RATE_LIMIT_AUTH_WINDOW: "${config.rateLimitWindow}"
399
+ RATE_LIMIT_API_REQUESTS: "${config.rateLimitRequests}"
400
+ RATE_LIMIT_API_WINDOW: "${config.rateLimitWindow}"
401
+ SECURITY_PASSWORD_MIN_LENGTH: "8"
402
+ SECURITY_SESSION_FIXATION: "true"`;
403
+ return `services:
404
+ nginx:
405
+ image: nginx:alpine
406
+ ports:
407
+ - "${config.corePort}:${config.corePort}"
408
+ volumes:
409
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
410
+ depends_on:
411
+ - core
412
+ core:
413
+ image: ${config.coreImageTag}
414
+ depends_on:
415
+ postgres:
416
+ condition: service_healthy
417
+ redis:
418
+ condition: service_healthy
419
+ environment:
420
+ DATABASE_URL: postgres://postgres:${config.dbPassword}@postgres:5432/alcedo
421
+ REDIS_URL: redis://:${config.redisPassword || ""}@redis:6379
422
+ CORE_PORT: "8080"
423
+ DEV_MODE: "false"
424
+ RUST_LOG: info
425
+ ADMIN_EMAIL: "${config.adminEmail}"
426
+ ADMIN_PASSWORD: "${config.adminPassword}"
427
+ PLUGIN_NETWORK: ${config.pluginNetwork}
428
+ PLUGINS_DIR: /var/lib/plugin-public
429
+ PLUGINS_DIR: /plugins
430
+ CORE_PUBLIC_URL: "${config.corePublicUrl || `http://localhost:${config.corePort}`}"
431
+ SYSTEM_PLUGINS_URL: "${config.systemPluginsUrl}"
432
+ LOCAL_REGISTRY_URL: "${config.localRegistryUrl}"
433
+ SESSION_TTL_SECONDS: "${config.sessionTtl}"
434
+ REGISTRY_ENCRYPTION_KEY: "${config.registryEncryptionKey}"
435
+ COLLECTION_LOG_FLUSH_MS: "100"${securityVars}
436
+ networks:
437
+ - default
438
+ - ${config.pluginNetwork}
439
+ extra_hosts:
440
+ - "host.docker.internal:host-gateway"
441
+ volumes:
442
+ - /var/run/docker.sock:/var/run/docker.sock
443
+ - plugin-public:/var/lib/plugin-public
444
+ - ./plugins:/plugins
445
+ - ./plugins:/app/plugins
446
+
447
+ registry:
448
+ image: registry:2
449
+ ports:
450
+ - "${config.registryPort}:5000"
451
+ volumes:
452
+ - registry-data:/var/lib/registry
453
+ healthcheck:
454
+ test: ["CMD", "wget", "--spider", "http://localhost:5000/"]
455
+ interval: 10s
456
+ timeout: 5s
457
+ retries: 3
458
+
459
+ postgres:
460
+ image: postgres:${config.postgresVersion}
461
+ environment:
462
+ POSTGRES_USER: postgres
463
+ POSTGRES_PASSWORD: ${config.dbPassword}
464
+ POSTGRES_DB: alcedo
465
+ healthcheck:
466
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
467
+ interval: 5s
468
+ timeout: 5s
469
+ retries: 5
470
+ volumes:
471
+ - postgres-data:/var/lib/postgresql/data
472
+
473
+ redis:
474
+ image: redis:7-alpine
475
+ restart: unless-stopped
476
+ command: ${config.redisPassword ? `["redis-server", "--requirepass", "${config.redisPassword}"]` : '["redis-server"]'}
477
+ healthcheck:
478
+ test: ["CMD", "redis-cli", "ping"]
479
+ interval: 5s
480
+ timeout: 3s
481
+ retries: 5
482
+ volumes:
483
+ - redis-data:/data
484
+
485
+ networks:
486
+ default:
487
+ ${config.pluginNetwork}:
488
+ driver: bridge
489
+
490
+ volumes:
491
+ postgres-data:
492
+ redis-data:
493
+ registry-data:
494
+ plugin-public:
495
+ `;
496
+ }
497
+ function generateNginxConf(config) {
498
+ return `upstream core_backend {
499
+ server core:8080;
500
+ }
501
+
502
+ server {
503
+ listen ${config.corePort};
504
+ server_name _;
505
+ absolute_redirect off;
506
+
507
+ location / {
508
+ proxy_pass http://core_backend;
509
+ proxy_set_header Host \$host;
510
+ proxy_set_header X-Real-IP \$remote_addr;
511
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
512
+ proxy_set_header X-Forwarded-Proto \$scheme;
513
+ proxy_read_timeout 120s;
514
+ proxy_send_timeout 120s;
515
+ }
516
+ }
517
+ `;
518
+ }
519
+ function generateEnv(config) {
520
+ return `# Alcedo Core — Environment Configuration
521
+ # Generated by \`alcedo init-core\`
522
+
523
+ # Admin credentials (used for first-run bootstrap)
524
+ ADMIN_EMAIL=${config.adminEmail}
525
+ ADMIN_PASSWORD=${config.adminPassword}
526
+
527
+ # Database
528
+ POSTGRES_PASSWORD=${config.dbPassword}
529
+ DATABASE_URL=postgres://postgres:${config.dbPassword}@postgres:5432/alcedo
530
+
531
+ # Redis
532
+ ${config.redisPassword ? `REDIS_PASSWORD=${config.redisPassword}\nREDIS_URL=redis://:${config.redisPassword}@redis:6379` : "REDIS_URL=redis://redis:6379"}
533
+
534
+ # Core
535
+ CORE_PORT=${config.corePort}
536
+ CORE_PUBLIC_URL=${config.corePublicUrl || `http://localhost:${config.corePort}`}
537
+ SYSTEM_PLUGINS_URL=${config.systemPluginsUrl}
538
+ LOCAL_REGISTRY_URL=${config.localRegistryUrl}
539
+ SESSION_TTL_SECONDS=${config.sessionTtl}
540
+ SECURITY_CORS_ENABLED=${config.enableCors}
541
+ SECURITY_CORS_ORIGINS=${config.corsOrigins}
542
+ SECURITY_RATE_LIMIT_ENABLED=${config.enableRateLimit}
543
+ RATE_LIMIT_AUTH_REQUESTS=${config.rateLimitRequests}
544
+ RATE_LIMIT_AUTH_WINDOW=${config.rateLimitWindow}
545
+ RATE_LIMIT_API_REQUESTS=${config.rateLimitRequests}
546
+ RATE_LIMIT_API_WINDOW=${config.rateLimitWindow}
547
+
548
+ # Docker Registry
549
+ REGISTRY_PORT=${config.registryPort}
550
+ REGISTRY_ENCRYPTION_KEY=${config.registryEncryptionKey}
551
+ `;
552
+ }
553
+ function generateK8sManifests(config) {
554
+ const namespace = config.pluginNetwork || "alcedo-core";
555
+ const dbName = "plugin_core";
556
+ const dbUrlBase = `postgres://postgres:${config.dbPassword}@postgres:5432/${dbName}`;
557
+ return {
558
+ "00-namespace.yaml": `apiVersion: v1
559
+ kind: Namespace
560
+ metadata:
561
+ name: ${namespace}
562
+ `,
563
+ "02-configmap.yaml": `apiVersion: v1
564
+ kind: ConfigMap
565
+ metadata:
566
+ name: core-config
567
+ namespace: ${namespace}
568
+ data:
569
+ CORE_PORT: "${config.corePort}"
570
+ DATABASE_URL: "${dbUrlBase}"
571
+ REDIS_URL: "redis://redis:6379"
572
+ DEV_MODE: "false"
573
+ RUST_LOG: "info"
574
+ PLUGIN_NETWORK: "${namespace}"
575
+ CORE_PUBLIC_URL: "${config.corePublicUrl || `http://localhost:${config.corePort}`}"
576
+ SYSTEM_PLUGINS_URL: "${config.systemPluginsUrl}"
577
+ LOCAL_REGISTRY_URL: "${config.localRegistryUrl}"
578
+ SESSION_TTL_SECONDS: "${config.sessionTtl}"
579
+ COLLECTION_LOG_FLUSH_MS: "100"
580
+ RATE_LIMIT_AUTH_REQUESTS: "${config.rateLimitRequests}"
581
+ RATE_LIMIT_AUTH_WINDOW: "${config.rateLimitWindow}"
582
+ RATE_LIMIT_API_REQUESTS: "${config.rateLimitRequests}"
583
+ RATE_LIMIT_API_WINDOW: "${config.rateLimitWindow}"
584
+ SECURITY_COOKIE_SECURE: "false"
585
+ SECURITY_COOKIE_SAMESITE: "lax"
586
+ SECURITY_COOKIE_HTTP_ONLY: "true"
587
+ SECURITY_SESSION_FIXATION: "true"
588
+ SECURITY_PASSWORD_MIN_LENGTH: "8"
589
+ PLUGINS_DIR: "/plugins"
590
+ PLUGINS_DIR: "/var/lib/plugin-public"
591
+ `,
592
+ "03-secrets.yaml": `apiVersion: v1
593
+ kind: Secret
594
+ metadata:
595
+ name: core-secrets
596
+ namespace: ${namespace}
597
+ type: Opaque
598
+ stringData:
599
+ REGISTRY_ENCRYPTION_KEY: "${config.registryEncryptionKey}"
600
+ POSTGRES_PASSWORD: "${config.dbPassword}"
601
+ ADMIN_EMAIL: "${config.adminEmail}"
602
+ ADMIN_PASSWORD: "${config.adminPassword}"
603
+ `,
604
+ "04-postgres.yaml": `apiVersion: apps/v1
605
+ kind: StatefulSet
606
+ metadata:
607
+ name: postgres
608
+ namespace: ${namespace}
609
+ spec:
610
+ serviceName: postgres
611
+ replicas: 1
612
+ selector:
613
+ matchLabels:
614
+ app: postgres
615
+ volumeClaimTemplates:
616
+ - metadata:
617
+ name: postgres-data
618
+ spec:
619
+ accessModes: ["ReadWriteOnce"]
620
+ resources:
621
+ requests:
622
+ storage: 10Gi
623
+ storageClassName: local-path
624
+ template:
625
+ metadata:
626
+ labels:
627
+ app: postgres
628
+ spec:
629
+ containers:
630
+ - name: postgres
631
+ image: postgres:${config.postgresVersion}
632
+ env:
633
+ - name: POSTGRES_USER
634
+ value: postgres
635
+ - name: POSTGRES_PASSWORD
636
+ valueFrom:
637
+ secretKeyRef:
638
+ name: core-secrets
639
+ key: POSTGRES_PASSWORD
640
+ - name: POSTGRES_DB
641
+ value: ${dbName}
642
+ ports:
643
+ - containerPort: 5432
644
+ volumeMounts:
645
+ - name: postgres-data
646
+ mountPath: /var/lib/postgresql/data
647
+ subPath: postgres
648
+ readinessProbe:
649
+ exec:
650
+ command: ["pg_isready", "-U", "postgres"]
651
+ initialDelaySeconds: 5
652
+ periodSeconds: 5
653
+ ---
654
+ apiVersion: v1
655
+ kind: Service
656
+ metadata:
657
+ name: postgres
658
+ namespace: ${namespace}
659
+ spec:
660
+ selector:
661
+ app: postgres
662
+ ports:
663
+ - port: 5432
664
+ targetPort: 5432
665
+ `,
666
+ "05-redis.yaml": `apiVersion: apps/v1
667
+ kind: Deployment
668
+ metadata:
669
+ name: redis
670
+ namespace: ${namespace}
671
+ spec:
672
+ replicas: 1
673
+ selector:
674
+ matchLabels:
675
+ app: redis
676
+ template:
677
+ metadata:
678
+ labels:
679
+ app: redis
680
+ spec:
681
+ containers:
682
+ - name: redis
683
+ image: redis:7-alpine
684
+ ports:
685
+ - containerPort: 6379
686
+ readinessProbe:
687
+ exec:
688
+ command: ["redis-cli", "ping"]
689
+ initialDelaySeconds: 3
690
+ periodSeconds: 5
691
+ ---
692
+ apiVersion: v1
693
+ kind: Service
694
+ metadata:
695
+ name: redis
696
+ namespace: ${namespace}
697
+ spec:
698
+ selector:
699
+ app: redis
700
+ ports:
701
+ - port: 6379
702
+ targetPort: 6379
703
+ `,
704
+ "06-core-deployment.yaml": `apiVersion: apps/v1
705
+ kind: Deployment
706
+ metadata:
707
+ name: alcedo-core
708
+ namespace: ${namespace}
709
+ labels:
710
+ app.kubernetes.io/name: alcedo-core
711
+ app.kubernetes.io/managed-by: alcedo-core
712
+ spec:
713
+ replicas: 1
714
+ selector:
715
+ matchLabels:
716
+ app: alcedo-core
717
+ template:
718
+ metadata:
719
+ labels:
720
+ app: alcedo-core
721
+ app.kubernetes.io/name: alcedo-core
722
+ app.kubernetes.io/managed-by: alcedo-core
723
+ spec:
724
+ serviceAccountName: alcedo-core
725
+ securityContext:
726
+ fsGroup: 1001
727
+ initContainers:
728
+ - name: seed-plugins
729
+ image: ${config.coreImageTag}
730
+ imagePullPolicy: Always
731
+ command: ["sh", "-c", "cp -r /plugins/* /data/ 2>/dev/null; chmod -R 755 /data/"]
732
+ securityContext:
733
+ runAsUser: 0
734
+ volumeMounts:
735
+ - name: plugins
736
+ mountPath: /data
737
+ - name: plugin-data
738
+ mountPath: /var/lib/plugin-public
739
+ containers:
740
+ - name: core
741
+ image: ${config.coreImageTag}
742
+ imagePullPolicy: Always
743
+ ports:
744
+ - containerPort: 8080
745
+ name: http
746
+ envFrom:
747
+ - configMapRef:
748
+ name: core-config
749
+ - secretRef:
750
+ name: core-secrets
751
+ env:
752
+ - name: POD_NAMESPACE
753
+ valueFrom:
754
+ fieldRef:
755
+ fieldPath: metadata.namespace
756
+ - name: POD_NAME
757
+ valueFrom:
758
+ fieldRef:
759
+ fieldPath: metadata.name
760
+ volumeMounts:
761
+ - name: plugins
762
+ mountPath: /plugins
763
+ - name: plugin-data
764
+ mountPath: /var/lib/plugin-public
765
+ readinessProbe:
766
+ httpGet:
767
+ path: /health
768
+ port: 8080
769
+ initialDelaySeconds: 5
770
+ periodSeconds: 10
771
+ livenessProbe:
772
+ httpGet:
773
+ path: /health
774
+ port: 8080
775
+ initialDelaySeconds: 15
776
+ periodSeconds: 20
777
+ resources:
778
+ requests:
779
+ cpu: 100m
780
+ memory: 256Mi
781
+ limits:
782
+ cpu: 500m
783
+ memory: 512Mi
784
+ volumes:
785
+ - name: plugins
786
+ emptyDir: {}
787
+ - name: plugin-data
788
+ persistentVolumeClaim:
789
+ claimName: plugin-data
790
+ `,
791
+ "07-core-service.yaml": `apiVersion: v1
792
+ kind: Service
793
+ metadata:
794
+ name: alcedo-core
795
+ namespace: ${namespace}
796
+ labels:
797
+ app.kubernetes.io/name: alcedo-core
798
+ spec:
799
+ selector:
800
+ app: alcedo-core
801
+ ports:
802
+ - port: 8080
803
+ targetPort: 8080
804
+ name: http
805
+ type: ClusterIP
806
+ `,
807
+ "08-plugin-data-pvc.yaml": `apiVersion: v1
808
+ kind: PersistentVolumeClaim
809
+ metadata:
810
+ name: plugin-data
811
+ namespace: ${namespace}
812
+ labels:
813
+ app.kubernetes.io/name: alcedo-core
814
+ app.kubernetes.io/managed-by: alcedo-core
815
+ spec:
816
+ accessModes:
817
+ - ReadWriteOnce
818
+ resources:
819
+ requests:
820
+ storage: 1Gi
821
+ `,
822
+ };
823
+ }
824
+ async function writeFiles(targetDir, config) {
825
+ const spinner = (0, logger_1.createSpinner)("Generating Alcedo Core configuration");
826
+ try {
827
+ node_fs_1.default.mkdirSync(targetDir, { recursive: true });
828
+ if (config.platform === "k8s") {
829
+ const k8sDir = node_path_1.default.join(targetDir, "k8s");
830
+ node_fs_1.default.mkdirSync(k8sDir, { recursive: true });
831
+ const manifests = generateK8sManifests(config);
832
+ for (const [filename, content] of Object.entries(manifests)) {
833
+ node_fs_1.default.writeFileSync(node_path_1.default.join(k8sDir, filename), content, "utf-8");
834
+ }
835
+ // Create plugins directory
836
+ const pluginsDir = node_path_1.default.join(targetDir, "plugins");
837
+ node_fs_1.default.mkdirSync(pluginsDir, { recursive: true });
838
+ spinner.succeed();
839
+ (0, logger_1.success)(`Alcedo Core K8s configuration generated: ${targetDir}`);
840
+ (0, logger_1.info)("");
841
+ (0, logger_1.info)("To deploy Alcedo Core on Kubernetes:");
842
+ (0, logger_1.info)(` cd ${node_path_1.default.relative(process.cwd(), targetDir)}`);
843
+ (0, logger_1.info)(" kubectl apply -f k8s/");
844
+ (0, logger_1.info)("");
845
+ (0, logger_1.info)("The admin UI will be available after port-forwarding:");
846
+ (0, logger_1.info)(" kubectl port-forward -n alcedo-core svc/alcedo-core 8080:8080");
847
+ (0, logger_1.info)(` ${config.corePublicUrl || `http://localhost:${config.corePort}`}/admin`);
848
+ (0, logger_1.info)(` Email: ${config.adminEmail}`);
849
+ (0, logger_1.info)("");
850
+ (0, logger_1.info)("Static plugins directory:");
851
+ (0, logger_1.info)(` ${node_path_1.default.join(targetDir, "plugins")}/`);
852
+ (0, logger_1.info)(" Place static plugin folders (with manifest.json) here.");
853
+ }
854
+ else {
855
+ // docker-compose.yml
856
+ const compose = generateCompose(config);
857
+ node_fs_1.default.writeFileSync(node_path_1.default.join(targetDir, "docker-compose.yml"), compose, "utf-8");
858
+ // .env
859
+ const env = generateEnv(config);
860
+ node_fs_1.default.writeFileSync(node_path_1.default.join(targetDir, ".env"), env, "utf-8");
861
+ const nginx = generateNginxConf(config);
862
+ node_fs_1.default.writeFileSync(node_path_1.default.join(targetDir, "nginx.conf"), nginx, "utf-8");
863
+ // Create plugins directory for static system plugins (e.g. admin UI)
864
+ const pluginsDir = node_path_1.default.join(targetDir, "plugins");
865
+ node_fs_1.default.mkdirSync(pluginsDir, { recursive: true });
866
+ // Create a README so users know they can place static plugins here
867
+ node_fs_1.default.writeFileSync(node_path_1.default.join(pluginsDir, "README.md"), "# Static Plugins\n\nPlace static plugin folders here (each with a manifest.json).\n" +
868
+ "The core will load them automatically on startup.\n", "utf-8");
869
+ spinner.succeed();
870
+ (0, logger_1.success)(`Alcedo Core configuration generated: ${targetDir}`);
871
+ (0, logger_1.info)("");
872
+ (0, logger_1.info)("To start Alcedo Core:");
873
+ (0, logger_1.info)(` cd ${node_path_1.default.relative(process.cwd(), targetDir)}`);
874
+ (0, logger_1.info)(" docker compose up -d");
875
+ (0, logger_1.info)("");
876
+ (0, logger_1.info)("The admin UI will be available at:");
877
+ (0, logger_1.info)(` ${config.corePublicUrl || `http://localhost:${config.corePort}`}/admin`);
878
+ (0, logger_1.info)(` Email: ${config.adminEmail}`);
879
+ (0, logger_1.info)("");
880
+ (0, logger_1.info)("Local Docker Registry:");
881
+ (0, logger_1.info)(` http://localhost:${config.registryPort}`);
882
+ (0, logger_1.info)("");
883
+ (0, logger_1.info)("Static plugins directory:");
884
+ (0, logger_1.info)(` ${node_path_1.default.join(targetDir, "plugins")}/`);
885
+ (0, logger_1.info)(" Place static plugin folders (with manifest.json) here.");
886
+ (0, logger_1.info)("System plugins will be auto-deployed from:");
887
+ (0, logger_1.info)(` ${config.systemPluginsUrl}`);
888
+ (0, logger_1.info)("");
889
+ (0, logger_1.info)("To deploy a plugin:");
890
+ (0, logger_1.info)(` curl -X POST http://localhost:${config.corePort}/api/plugins/deploy \\`);
891
+ (0, logger_1.info)(' -H "Content-Type: application/json" \\');
892
+ (0, logger_1.info)(' -d \'{"slug": "my-plugin", "image": "localhost:5000/my-plugin:latest"\'}');
893
+ }
894
+ }
895
+ catch (err) {
896
+ spinner.fail();
897
+ throw err;
898
+ }
899
+ }
900
+ //# sourceMappingURL=init-core.js.map