@kodus/cli 0.0.1

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 (2) hide show
  1. package/index.js +538 -0
  2. package/package.json +33 -0
package/index.js ADDED
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import inquirer from "inquirer";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import fs from "fs-extra";
8
+ import path from "path";
9
+ import { execSync } from "child_process";
10
+ import { fileURLToPath } from "url";
11
+ import { dirname } from "path";
12
+ import crypto from "crypto";
13
+ import fetch from "node-fetch";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ // Helper functions
19
+ const generateSecretKey = () => crypto.randomBytes(32).toString("base64");
20
+ const generateDbPassword = () =>
21
+ crypto
22
+ .randomBytes(16)
23
+ .toString("base64")
24
+ .replace(/[^a-zA-Z0-9]/g, "")
25
+ .slice(0, 16);
26
+
27
+ const backupEnv = () => {
28
+ if (fs.existsSync(".env")) {
29
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
30
+ fs.copySync(".env", `.env.backup.${timestamp}`);
31
+ return true;
32
+ }
33
+ return false;
34
+ };
35
+
36
+ const restoreEnv = (backupFile) => {
37
+ if (fs.existsSync(backupFile)) {
38
+ fs.copySync(backupFile, ".env");
39
+ return true;
40
+ }
41
+ return false;
42
+ };
43
+
44
+ const getLatestVersion = () => {
45
+ try {
46
+ const result = execSync("git describe --tags --abbrev=0", { stdio: "pipe" })
47
+ .toString()
48
+ .trim();
49
+ return result;
50
+ } catch (error) {
51
+ Y;
52
+ return null;
53
+ }
54
+ };
55
+
56
+ const getCurrentVersion = () => {
57
+ try {
58
+ const result = execSync("git rev-parse --abbrev-ref HEAD", {
59
+ stdio: "pipe",
60
+ })
61
+ .toString()
62
+ .trim();
63
+ return result;
64
+ } catch (error) {
65
+ return null;
66
+ }
67
+ };
68
+
69
+ // CLI version
70
+ program.version("1.0.0");
71
+
72
+ // Install command
73
+ program
74
+ .command("install")
75
+ .description("Install and configure Kodus")
76
+ .action(async () => {
77
+ try {
78
+ // Check prerequisites
79
+ console.log(chalk.blue("\nšŸ” Checking prerequisites..."));
80
+ const spinner = ora("Checking Docker installation").start();
81
+
82
+ try {
83
+ execSync("docker --version", { stdio: "ignore" });
84
+ spinner.succeed("Docker is installed");
85
+ } catch (error) {
86
+ spinner.fail("Docker is not installed");
87
+ console.error(
88
+ chalk.red(
89
+ "\nPlease install Docker first: https://docs.docker.com/get-docker/"
90
+ )
91
+ );
92
+ process.exit(1);
93
+ }
94
+
95
+ // Environment type selection
96
+ const { envType } = await inquirer.prompt([
97
+ {
98
+ type: "list",
99
+ name: "envType",
100
+ message: "What type of environment are you setting up?",
101
+ choices: [
102
+ { name: "Local (localhost)", value: "local" },
103
+ { name: "External (with public URL)", value: "external" },
104
+ ],
105
+ default: "local",
106
+ },
107
+ ]);
108
+
109
+ let baseUrl = "http://localhost:3000";
110
+ if (envType === "external") {
111
+ const { url } = await inquirer.prompt([
112
+ {
113
+ type: "input",
114
+ name: "url",
115
+ message: "Enter your public URL (e.g., https://kodus.yourdomain.com):",
116
+ validate: (input) => {
117
+ try {
118
+ const url = new URL(input);
119
+ if (!url.protocol.startsWith('http')) {
120
+ return "URL must start with http:// or https://";
121
+ }
122
+ if (!url.hostname.includes('.')) {
123
+ return "URL must include a valid domain name";
124
+ }
125
+ return true;
126
+ } catch (e) {
127
+ return "Please enter a valid URL (e.g., https://kodus.yourdomain.com)";
128
+ }
129
+ },
130
+ },
131
+ ]);
132
+ baseUrl = url;
133
+ }
134
+
135
+ // Git service selection
136
+ const { gitService } = await inquirer.prompt([
137
+ {
138
+ type: "list",
139
+ name: "gitService",
140
+ message: "Which Git service will you use?",
141
+ choices: [
142
+ { name: "GitHub", value: "github" },
143
+ { name: "GitLab", value: "gitlab" },
144
+ { name: "Bitbucket", value: "bitbucket" },
145
+ ],
146
+ },
147
+ ]);
148
+
149
+ // Git service configuration
150
+ let gitConfig = {};
151
+
152
+ if (envType === "local") {
153
+ console.log(chalk.yellow("\nāš ļø IMPORTANT: If you're using a cloud Git service (GitHub, GitLab, Bitbucket), you'll need to configure the webhook manually."));
154
+ console.log(chalk.yellow("For local or self-hosted Git instances, no additional configuration is needed."));
155
+ console.log(chalk.yellow("The webhook URL will be:"));
156
+ console.log(chalk.white(`${baseUrl}/api/webhook/${gitService}`));
157
+ }
158
+
159
+ // Configure webhook URL based on the selected Git service
160
+ if (gitService === "github") {
161
+ gitConfig = {
162
+ API_GITHUB_CODE_MANAGEMENT_WEBHOOK: `${baseUrl}/api/webhook/${gitService}`,
163
+ };
164
+ } else if (gitService === "gitlab") {
165
+ gitConfig = {
166
+ API_GITLAB_CODE_MANAGEMENT_WEBHOOK: `${baseUrl}/api/webhook/${gitService}`,
167
+ };
168
+ } else if (gitService === "bitbucket") {
169
+ gitConfig = {
170
+ GLOBAL_BITBUCKET_CODE_MANAGEMENT_WEBHOOK: `${baseUrl}/api/webhook/${gitService}`,
171
+ };
172
+ }
173
+
174
+ // Database configuration
175
+ const { useDefaultDb } = await inquirer.prompt([
176
+ {
177
+ type: "confirm",
178
+ name: "useDefaultDb",
179
+ message: "Would you like to use default database configurations?",
180
+ default: true,
181
+ },
182
+ ]);
183
+
184
+ // LLM API Keys configuration
185
+ console.log(chalk.blue("\nšŸ”‘ Configuring LLM API Keys..."));
186
+ const llmKeys = await inquirer.prompt([
187
+ {
188
+ type: "input",
189
+ name: "openAiKey",
190
+ message: "Enter your OpenAI API key (optional):",
191
+ default: "",
192
+ },
193
+ {
194
+ type: "input",
195
+ name: "googleAiKey",
196
+ message: "Enter your Google AI API key (optional):",
197
+ default: "",
198
+ },
199
+ {
200
+ type: "input",
201
+ name: "anthropicKey",
202
+ message: "Enter your Anthropic API key (optional):",
203
+ default: "",
204
+ },
205
+ {
206
+ type: "input",
207
+ name: "fireworksKey",
208
+ message: "Enter your Fireworks API key (optional):",
209
+ default: "",
210
+ },
211
+ {
212
+ type: "input",
213
+ name: "novitaKey",
214
+ message: "Enter your Novita AI API key (optional):",
215
+ default: "",
216
+ },
217
+ {
218
+ type: "input",
219
+ name: "vertexKey",
220
+ message: "Enter your Vertex AI API key (optional):",
221
+ default: "",
222
+ },
223
+ ]);
224
+
225
+ // Start installation
226
+ console.log(chalk.blue("\nšŸš€ Starting installation..."));
227
+
228
+ // Generate .env content
229
+ const envContent = {
230
+ // Base configuration
231
+ WEB_NODE_ENV: "development",
232
+ WEB_HOSTNAME_API: "localhost",
233
+ WEB_PORT_API: "3001",
234
+ WEB_PORT: "3000",
235
+ WEB_NEXTAUTH_URL: "http://localhost:3000",
236
+ WEB_NEXTAUTH_SECRET: generateSecretKey(),
237
+ WEB_JWT_SECRET_KEY: generateSecretKey(),
238
+
239
+ API_NODE_ENV: "development",
240
+ API_LOG_LEVEL: "error",
241
+ API_LOG_PRETTY: "true",
242
+ API_HOST: "0.0.0.0",
243
+ API_PORT: "3001",
244
+ API_RATE_MAX_REQUEST: "100",
245
+ API_RATE_INTERVAL: "1000",
246
+ API_JWT_EXPIRES_IN: "365d",
247
+ API_JWT_SECRET: generateSecretKey(),
248
+ API_JWT_REFRESHSECRET: generateSecretKey(),
249
+ API_JWT_REFRESH_EXPIRES_IN: "7d",
250
+
251
+ GLOBAL_API_CONTAINER_NAME: "kodus-orchestrator-prod",
252
+
253
+ // Database
254
+ API_DATABASE_ENV: "development",
255
+ API_PG_DB_USERNAME: "kodusdev",
256
+ API_PG_DB_PASSWORD: generateDbPassword(),
257
+ API_PG_DB_DATABASE: "kodus_db",
258
+ API_PG_DB_HOST: "db_kodus_postgres",
259
+ API_PG_DB_PORT: "5432",
260
+
261
+ API_MG_DB_USERNAME: "kodusdev",
262
+ API_MG_DB_PASSWORD: generateDbPassword(),
263
+ API_MG_DB_DATABASE: "kodus_db",
264
+ API_MG_DB_HOST: "db_kodus_mongodb",
265
+ API_MG_DB_PORT: "27017",
266
+ API_MG_DB_PRODUCTION_CONFIG: "",
267
+
268
+ // LLM API Keys
269
+ API_OPEN_AI_APIKEY: llmKeys.openAiKey,
270
+ API_GOOGLE_AI_API_KEY: llmKeys.googleAiKey,
271
+ API_ANTHROPIC_API_KEY: llmKeys.anthropicKey,
272
+ API_FIREWORKS_API_KEY: llmKeys.fireworksKey,
273
+ API_NOVITA_AI_API_KEY: llmKeys.novitaKey,
274
+ API_VERTEX_AI_API_KEY: llmKeys.vertexKey,
275
+
276
+ // LLM Models
277
+ API_LLM_MODEL_CHATGPT_3_5_TURBO: "gpt-4o-mini",
278
+ API_LLM_MODEL_CHATGPT_3_5_TURBO_16K: "gpt-4o-mini",
279
+ API_LLM_MODEL_CHATGPT_4: "gpt-4o-mini",
280
+ API_LLM_MODEL_CHATGPT_4_TURBO: "gpt-4o-mini",
281
+ API_LLM_MODEL_CHATGPT_4_ALL: "gpt-4o",
282
+ API_LLM_MODEL_CHATGPT_4_ALL_MINI: "gpt-4o-mini",
283
+ API_LLM_MODEL_CLAUDE_3_5_SONNET: "claude-3-5-sonnet-20241022",
284
+ API_LLM_MODEL_CLAUDE_3_5_SONNET_20241022: "claude-3-5-sonnet-20241022",
285
+ API_LLM_MODEL_GEMINI_1_5_PRO: "gpt-4o-mini",
286
+ API_LLM_MODEL_GEMINI_1_5_PRO_EXP: "gpt-4o-mini",
287
+
288
+ // Git service configuration
289
+ ...gitConfig,
290
+
291
+ // Ports
292
+ WEB_PORT: "3000",
293
+ API_PORT: "3001",
294
+
295
+ // RabbitMQ
296
+ RABBITMQ_DEFAULT_USER: "kodus",
297
+ RABBITMQ_DEFAULT_PASS: generateDbPassword(),
298
+
299
+ // Monitoring
300
+ GRAFANA_ADMIN_PASSWORD: generateDbPassword(),
301
+ };
302
+
303
+ // Create .env file
304
+ const envSpinner = ora("Creating .env file").start();
305
+ const envContentString = Object.entries(envContent)
306
+ .map(([key, value]) => `${key}=${value}`)
307
+ .join("\n");
308
+ fs.writeFileSync(".env", envContentString);
309
+ envSpinner.succeed("Created .env file");
310
+
311
+ // Start containers
312
+ const dockerSpinner = ora("Starting containers").start();
313
+ try {
314
+ execSync("docker-compose up -d", { stdio: "pipe" });
315
+ dockerSpinner.succeed("Containers started");
316
+ } catch (error) {
317
+ dockerSpinner.fail("Failed to start containers");
318
+ console.error(chalk.red("\nError details:"));
319
+ console.error(chalk.white(error.stdout?.toString() || error.message));
320
+ process.exit(1);
321
+ }
322
+
323
+ const maxAttempts = 30; // 5 minutes with 10 second intervals
324
+
325
+ // Wait for PostgreSQL to be ready
326
+ const pgSpinner = ora("Waiting for PostgreSQL to be ready").start();
327
+ let isPgReady = false;
328
+ let pgAttempts = 0;
329
+
330
+ while (!isPgReady && pgAttempts < maxAttempts) {
331
+ try {
332
+ const logs = execSync("docker-compose logs db_kodus_postgres", { stdio: "pipe" }).toString();
333
+ if (logs.includes("database system is ready to accept connections")) {
334
+ isPgReady = true;
335
+ pgSpinner.succeed("PostgreSQL is ready");
336
+ } else {
337
+ pgAttempts++;
338
+ await new Promise(resolve => setTimeout(resolve, 10000));
339
+ }
340
+ } catch (error) {
341
+ pgAttempts++;
342
+ await new Promise(resolve => setTimeout(resolve, 10000));
343
+ }
344
+ }
345
+
346
+ if (!isPgReady) {
347
+ pgSpinner.fail("PostgreSQL failed to start");
348
+ console.error(chalk.red("\nError: PostgreSQL failed to start properly"));
349
+ console.error(chalk.yellow("\nTroubleshooting steps:"));
350
+ console.error(chalk.white("1. Check PostgreSQL logs: docker-compose logs db_kodus_postgres"));
351
+ console.error(chalk.white("2. Verify PostgreSQL container is running: docker-compose ps db_kodus_postgres"));
352
+ process.exit(1);
353
+ }
354
+
355
+ // Wait for MongoDB to be ready
356
+ const mongoSpinner = ora("Waiting for MongoDB to be ready").start();
357
+ let isMongoReady = false;
358
+ let mongoAttempts = 0;
359
+
360
+ while (!isMongoReady && mongoAttempts < maxAttempts) {
361
+ try {
362
+ const logs = execSync("docker-compose logs db_kodus_mongodb", { stdio: "pipe" }).toString();
363
+ if (logs.includes("Waiting for connections") || logs.includes("Connection accepted")) {
364
+ isMongoReady = true;
365
+ mongoSpinner.succeed("MongoDB is ready");
366
+ } else {
367
+ mongoAttempts++;
368
+ await new Promise(resolve => setTimeout(resolve, 10000));
369
+ }
370
+ } catch (error) {
371
+ mongoAttempts++;
372
+ await new Promise(resolve => setTimeout(resolve, 10000));
373
+ }
374
+ }
375
+
376
+ if (!isMongoReady) {
377
+ mongoSpinner.fail("MongoDB failed to start");
378
+ console.error(chalk.red("\nError: MongoDB failed to start properly"));
379
+ console.error(chalk.yellow("\nTroubleshooting steps:"));
380
+ console.error(chalk.white("1. Check MongoDB logs: docker-compose logs db_kodus_mongodb"));
381
+ console.error(chalk.white("2. Verify MongoDB container is running: docker-compose ps db_kodus_mongodb"));
382
+ process.exit(1);
383
+ }
384
+
385
+ // Now wait for orchestrator to be ready
386
+ const healthSpinner = ora("Waiting for orchestrator to be ready").start();
387
+ let isOrchestratorReady = false;
388
+ let attempts = 0;
389
+
390
+ const criticalErrors = [
391
+ "password authentication failed",
392
+ "Unable to connect to the database",
393
+ "FATAL:",
394
+ "MongoServerError:",
395
+ "connection refused"
396
+ ];
397
+
398
+ while (!isOrchestratorReady && attempts < maxAttempts) {
399
+ try {
400
+ // Check if container is running
401
+ const containerStatus = execSync("docker-compose ps orchestrator --format json", { stdio: "pipe" }).toString();
402
+ const status = JSON.parse(containerStatus)[0];
403
+
404
+ if (status.State === "running") {
405
+ // Check logs for any errors
406
+ const logs = execSync("docker-compose logs --tail=50 orchestrator", { stdio: "pipe" }).toString();
407
+
408
+ // Check for critical errors that should stop the process
409
+ const foundCriticalError = criticalErrors.find(error => logs.includes(error));
410
+ if (foundCriticalError) {
411
+ healthSpinner.fail("Critical error detected");
412
+ console.error(chalk.red("\nError: Database connection failed"));
413
+ console.error(chalk.yellow("\nTroubleshooting steps:"));
414
+ console.error(chalk.white("1. Check if the database passwords in .env match the ones in your database"));
415
+ console.error(chalk.white("2. Verify if the database containers are running: docker-compose ps"));
416
+ console.error(chalk.white("3. Check database logs: docker-compose logs db_kodus_postgres db_kodus_mongodb"));
417
+ console.error(chalk.white("\nFull error log:"));
418
+ console.error(chalk.white(logs));
419
+ process.exit(1);
420
+ }
421
+
422
+ // If no critical errors and we see successful connection messages, proceed
423
+ if (logs.includes("Database connection established") || logs.includes("Connected to MongoDB")) {
424
+ isOrchestratorReady = true;
425
+ healthSpinner.succeed("Orchestrator is ready");
426
+ } else {
427
+ attempts++;
428
+ await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
429
+ }
430
+ } else {
431
+ attempts++;
432
+ await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
433
+ }
434
+ } catch (error) {
435
+ attempts++;
436
+ await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
437
+ }
438
+ }
439
+
440
+ if (!isOrchestratorReady) {
441
+ healthSpinner.fail("Orchestrator failed to start");
442
+ console.error(chalk.red("\nError: The orchestrator service failed to start properly."));
443
+ console.error(chalk.yellow("\nTroubleshooting steps:"));
444
+ console.error(chalk.white("1. Check the orchestrator logs: docker-compose logs orchestrator"));
445
+ console.error(chalk.white("2. Verify all required services are running: docker-compose ps"));
446
+ console.error(chalk.white("3. Try restarting the services: docker-compose restart"));
447
+ process.exit(1);
448
+ }
449
+
450
+ // Setup database
451
+ const dbSpinner = ora("Setting up database").start();
452
+ try {
453
+ execSync("./scripts/setup-db.sh", { stdio: "pipe" });
454
+ dbSpinner.succeed("Database setup completed");
455
+ } catch (error) {
456
+ dbSpinner.fail("Failed to setup database");
457
+ console.error(chalk.red("\nError details:"));
458
+ console.error(chalk.white(error.stdout?.toString() || error.message));
459
+ process.exit(1);
460
+ }
461
+
462
+ // Verify database connection by checking orchestrator logs
463
+ const dbCheckSpinner = ora("Verifying database connection").start();
464
+ try {
465
+ const logs = execSync("docker-compose logs --tail=50 orchestrator", { stdio: "pipe" }).toString();
466
+
467
+ // Look for database connection errors in logs
468
+ if (logs.includes("database connection error") || logs.includes("Database connection failed")) {
469
+ throw new Error("Database connection errors found in logs");
470
+ }
471
+
472
+ dbCheckSpinner.succeed("Database connection verified");
473
+ } catch (error) {
474
+ dbCheckSpinner.fail("Failed to verify database connection");
475
+ console.error(chalk.red("\nError: Could not verify database connection."));
476
+ console.error(chalk.yellow("Please check the logs with: docker-compose logs orchestrator"));
477
+ process.exit(1);
478
+ }
479
+
480
+ // Installation complete
481
+ console.log(chalk.green("\n✨ Installation completed successfully!"));
482
+
483
+ console.log(chalk.blue("\nšŸ“ Installation Summary:"));
484
+ console.log(
485
+ chalk.white(
486
+ ` - Environment: ${envType === "local" ? "Local" : "External"}`
487
+ )
488
+ );
489
+ if (envType === "external") {
490
+ console.log(chalk.white(` - Base URL: ${baseUrl}`));
491
+ }
492
+ console.log(
493
+ chalk.white(
494
+ ` - Git Service: ${
495
+ gitService.charAt(0).toUpperCase() + gitService.slice(1)
496
+ }`
497
+ )
498
+ );
499
+ console.log(
500
+ chalk.white(
501
+ ` - Database: ${
502
+ useDefaultDb ? "Default configuration" : "Custom configuration"
503
+ }`
504
+ )
505
+ );
506
+
507
+ console.log(chalk.blue("\nšŸ”— Access URLs:"));
508
+ console.log(chalk.white(` - Web Interface: ${baseUrl}`));
509
+ console.log(chalk.white(" - Grafana Dashboard: http://localhost:3001"));
510
+ console.log(
511
+ chalk.white(" - RabbitMQ Management: http://localhost:15672")
512
+ );
513
+
514
+ console.log(chalk.blue("\nšŸ“š Next Steps:"));
515
+ console.log(chalk.white(" 1. Access the web interface"));
516
+ console.log(chalk.white(" 2. Set up your first user"));
517
+ console.log(chalk.white(" 3. Start using Kodus!"));
518
+
519
+ const { startServices } = await inquirer.prompt([
520
+ {
521
+ type: "confirm",
522
+ name: "startServices",
523
+ message: "Would you like to start the services now?",
524
+ default: true,
525
+ },
526
+ ]);
527
+
528
+ if (startServices) {
529
+ execSync("docker-compose up -d", { stdio: "ignore" });
530
+ console.log(chalk.green("\nServices started successfully!"));
531
+ }
532
+ } catch (error) {
533
+ console.error(chalk.red("\nāŒ Installation failed:"), error.message);
534
+ process.exit(1);
535
+ }
536
+ });
537
+
538
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@kodus/cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI tool for Kodus installation and management",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "preferGlobal": true,
8
+ "bin": {
9
+ "kodus": "./index.js"
10
+ },
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": ["kodus", "cli", "installation", "deployment"],
15
+ "author": "Kodus",
16
+ "license": "ISC",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/kodus/kodus-cli.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/kodus/kodus-cli/issues"
23
+ },
24
+ "homepage": "https://github.com/kodus/kodus-cli#readme",
25
+ "dependencies": {
26
+ "chalk": "^4.1.2",
27
+ "commander": "^11.1.0",
28
+ "dotenv": "^16.4.1",
29
+ "fs-extra": "^11.2.0",
30
+ "inquirer": "^8.2.6",
31
+ "ora": "^5.4.1"
32
+ }
33
+ }