@turboops/cli 1.0.0-dev.584 → 1.0.0-dev.585

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/dist/index.js +530 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command7 } from "commander";
4
+ import { Command as Command8 } from "commander";
5
5
  import chalk8 from "chalk";
6
6
 
7
7
  // src/services/config.ts
@@ -135,9 +135,9 @@ function loadPackageJson() {
135
135
  join(__dirname, "../package.json")
136
136
  // From dist/
137
137
  ];
138
- for (const path3 of paths) {
139
- if (existsSync(path3)) {
140
- return require2(path3);
138
+ for (const path4 of paths) {
139
+ if (existsSync(path4)) {
140
+ return require2(path4);
141
141
  }
142
142
  }
143
143
  return { version: "0.0.0", name: "@turboops/cli" };
@@ -432,7 +432,7 @@ var apiClient = {
432
432
  /**
433
433
  * Make an API request
434
434
  */
435
- async request(method, path3, body) {
435
+ async request(method, path4, body) {
436
436
  const apiUrl = configService.getApiUrl();
437
437
  const token = configService.getToken();
438
438
  if (!token) {
@@ -441,7 +441,7 @@ var apiClient = {
441
441
  status: 401
442
442
  };
443
443
  }
444
- const url = `${apiUrl}${path3}`;
444
+ const url = `${apiUrl}${path4}`;
445
445
  const headers = {
446
446
  "Content-Type": "application/json",
447
447
  Authorization: `Bearer ${token}`
@@ -455,11 +455,29 @@ var apiClient = {
455
455
  fetchOptions.body = JSON.stringify(body);
456
456
  }
457
457
  const response = await fetch(url, fetchOptions);
458
- const data = await response.json().catch(() => null);
458
+ const text = await response.text();
459
+ let data = null;
460
+ try {
461
+ data = text ? JSON.parse(text) : null;
462
+ } catch {
463
+ if (!response.ok) {
464
+ return {
465
+ error: `HTTP ${response.status}: ${text || "No response body"}`,
466
+ status: response.status
467
+ };
468
+ }
469
+ }
459
470
  if (!response.ok) {
460
471
  const errorData = data;
472
+ const errorMessage = errorData?.message || errorData?.error || `HTTP ${response.status}`;
473
+ return {
474
+ error: errorMessage,
475
+ status: response.status
476
+ };
477
+ }
478
+ if (data === null || data === void 0) {
461
479
  return {
462
- error: errorData?.message || `HTTP ${response.status}`,
480
+ error: `Empty response from server (HTTP ${response.status})`,
463
481
  status: response.status
464
482
  };
465
483
  }
@@ -599,10 +617,10 @@ var apiClient = {
599
617
  return this.request("POST", "/deployment/projects/simple", payload);
600
618
  },
601
619
  /**
602
- * Get all customers
620
+ * Get all customers (simplified list for selection)
603
621
  */
604
622
  async getCustomers() {
605
- return this.request("GET", "/customer");
623
+ return this.request("GET", "/customer/simple");
606
624
  },
607
625
  /**
608
626
  * Get environments (stages) for project
@@ -1187,14 +1205,172 @@ function getStatusColor(status) {
1187
1205
 
1188
1206
  // src/commands/init.ts
1189
1207
  import { Command as Command3 } from "commander";
1208
+ import prompts2 from "prompts";
1209
+
1210
+ // src/services/ai-tools.ts
1211
+ import { execSync, spawn } from "child_process";
1190
1212
  import prompts from "prompts";
1213
+ var AI_TOOLS = {
1214
+ claude: {
1215
+ name: "Claude Code",
1216
+ command: "claude",
1217
+ promptFlag: "-p"
1218
+ },
1219
+ codex: {
1220
+ name: "OpenAI Codex",
1221
+ command: "codex",
1222
+ promptFlag: ""
1223
+ }
1224
+ };
1225
+ var aiToolsService = {
1226
+ /**
1227
+ * Check which AI tools are installed locally
1228
+ */
1229
+ getAvailableTools() {
1230
+ const available = [];
1231
+ const whichCommand = process.platform === "win32" ? "where" : "which";
1232
+ for (const [key, config] of Object.entries(AI_TOOLS)) {
1233
+ try {
1234
+ execSync(`${whichCommand} ${config.command}`, { stdio: "ignore" });
1235
+ available.push(key);
1236
+ } catch {
1237
+ }
1238
+ }
1239
+ return available;
1240
+ },
1241
+ /**
1242
+ * Interactive tool selection
1243
+ * Returns null if no tools available or user cancels
1244
+ */
1245
+ async selectTool() {
1246
+ const available = this.getAvailableTools();
1247
+ if (available.length === 0) {
1248
+ logger.error("Kein AI-Tool gefunden. Bitte installieren Sie Claude Code oder OpenAI Codex.");
1249
+ logger.newline();
1250
+ logger.info(" Claude Code: https://claude.ai/code");
1251
+ logger.info(" OpenAI Codex: https://openai.com/codex");
1252
+ return null;
1253
+ }
1254
+ if (available.length === 1) {
1255
+ logger.info(`Verwende ${AI_TOOLS[available[0]].name}`);
1256
+ return available[0];
1257
+ }
1258
+ const { selectedTool } = await prompts({
1259
+ type: "select",
1260
+ name: "selectedTool",
1261
+ message: "AI-Tool ausw\xE4hlen:",
1262
+ choices: available.map((tool) => ({
1263
+ title: AI_TOOLS[tool].name,
1264
+ value: tool
1265
+ }))
1266
+ });
1267
+ return selectedTool || null;
1268
+ },
1269
+ /**
1270
+ * Run AI tool with a prompt in the current directory
1271
+ * The tool runs interactively, inheriting stdio
1272
+ */
1273
+ async runWithPrompt(tool, prompt) {
1274
+ const config = AI_TOOLS[tool];
1275
+ logger.newline();
1276
+ logger.info(`Starte ${config.name}...`);
1277
+ logger.info("Das AI-Tool wird interaktiv ausgef\xFChrt. Folgen Sie den Anweisungen.");
1278
+ logger.newline();
1279
+ return new Promise((resolve2) => {
1280
+ const args = config.promptFlag ? [config.promptFlag, prompt] : [prompt];
1281
+ const child = spawn(config.command, args, {
1282
+ stdio: "inherit",
1283
+ cwd: process.cwd(),
1284
+ shell: process.platform === "win32"
1285
+ });
1286
+ child.on("close", (code) => {
1287
+ logger.newline();
1288
+ resolve2(code === 0);
1289
+ });
1290
+ child.on("error", (err) => {
1291
+ logger.error(`Fehler beim Ausf\xFChren von ${config.name}: ${err.message}`);
1292
+ resolve2(false);
1293
+ });
1294
+ });
1295
+ }
1296
+ };
1297
+
1298
+ // src/utils/detect-project.ts
1299
+ import * as fs2 from "fs/promises";
1300
+ import * as path2 from "path";
1301
+ async function detectProjectConfig() {
1302
+ const cwd = process.cwd();
1303
+ const result = {
1304
+ hasDockerfile: false,
1305
+ hasDockerCompose: false,
1306
+ hasGitLabPipeline: false,
1307
+ hasGitHubPipeline: false,
1308
+ hasTurboOpsInPipeline: false
1309
+ };
1310
+ const dockerfilePaths = [
1311
+ "Dockerfile",
1312
+ "dockerfile",
1313
+ "docker/Dockerfile",
1314
+ "api/Dockerfile",
1315
+ "app/Dockerfile",
1316
+ "projects/api/Dockerfile",
1317
+ "projects/app/Dockerfile"
1318
+ ];
1319
+ for (const dockerPath of dockerfilePaths) {
1320
+ try {
1321
+ await fs2.access(path2.join(cwd, dockerPath));
1322
+ result.hasDockerfile = true;
1323
+ result.dockerfilePath = dockerPath;
1324
+ break;
1325
+ } catch {
1326
+ }
1327
+ }
1328
+ const composePaths = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
1329
+ for (const composePath of composePaths) {
1330
+ try {
1331
+ await fs2.access(path2.join(cwd, composePath));
1332
+ result.hasDockerCompose = true;
1333
+ break;
1334
+ } catch {
1335
+ }
1336
+ }
1337
+ const gitlabPath = path2.join(cwd, ".gitlab-ci.yml");
1338
+ try {
1339
+ await fs2.access(gitlabPath);
1340
+ result.hasGitLabPipeline = true;
1341
+ result.pipelinePath = ".gitlab-ci.yml";
1342
+ const content = await fs2.readFile(gitlabPath, "utf-8");
1343
+ result.hasTurboOpsInPipeline = content.includes("turbo deploy") || content.includes("@turboops/cli");
1344
+ } catch {
1345
+ }
1346
+ const githubPath = path2.join(cwd, ".github", "workflows");
1347
+ try {
1348
+ const files = await fs2.readdir(githubPath);
1349
+ const ymlFiles = files.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
1350
+ if (ymlFiles.length > 0) {
1351
+ result.hasGitHubPipeline = true;
1352
+ result.pipelinePath = `.github/workflows/${ymlFiles[0]}`;
1353
+ for (const file of ymlFiles) {
1354
+ const content = await fs2.readFile(path2.join(githubPath, file), "utf-8");
1355
+ if (content.includes("turbo deploy") || content.includes("@turboops/cli")) {
1356
+ result.hasTurboOpsInPipeline = true;
1357
+ break;
1358
+ }
1359
+ }
1360
+ }
1361
+ } catch {
1362
+ }
1363
+ return result;
1364
+ }
1365
+
1366
+ // src/commands/init.ts
1191
1367
  import chalk4 from "chalk";
1192
1368
  var initCommand = new Command3("init").description("Initialize TurboOps project in current directory").action(async () => {
1193
1369
  logger.header("TurboOps Project Initialization");
1194
1370
  if (!configService.isAuthenticated()) {
1195
1371
  logger.warning("Nicht authentifiziert. Bitte melden Sie sich zuerst an.");
1196
1372
  logger.newline();
1197
- const { shouldLogin } = await prompts({
1373
+ const { shouldLogin } = await prompts2({
1198
1374
  initial: true,
1199
1375
  message: "M\xF6chten Sie sich jetzt anmelden?",
1200
1376
  name: "shouldLogin",
@@ -1218,7 +1394,7 @@ var initCommand = new Command3("init").description("Initialize TurboOps project
1218
1394
  logger.success(`Angemeldet als ${result.user?.email || "Unknown"}`);
1219
1395
  }
1220
1396
  logger.newline();
1221
- const { projectSlug } = await prompts({
1397
+ const { projectSlug } = await prompts2({
1222
1398
  hint: "Der Slug Ihres TurboOps-Projekts",
1223
1399
  message: "Projekt-Slug:",
1224
1400
  name: "projectSlug",
@@ -1240,7 +1416,7 @@ var initCommand = new Command3("init").description("Initialize TurboOps project
1240
1416
  }
1241
1417
  logger.newline();
1242
1418
  logger.warning(`Projekt "${projectSlug}" nicht gefunden.`);
1243
- const { shouldCreate } = await prompts({
1419
+ const { shouldCreate } = await prompts2({
1244
1420
  initial: true,
1245
1421
  message: "M\xF6chten Sie ein neues Projekt anlegen?",
1246
1422
  name: "shouldCreate",
@@ -1258,7 +1434,7 @@ async function setupProject(project) {
1258
1434
  const { data: environments } = await apiClient.getEnvironments(project.id);
1259
1435
  if (!environments || environments.length === 0) {
1260
1436
  logger.newline();
1261
- const { shouldCreateStage } = await prompts({
1437
+ const { shouldCreateStage } = await prompts2({
1262
1438
  initial: true,
1263
1439
  message: "Das Projekt hat noch keine Stages. M\xF6chten Sie jetzt eine erstellen?",
1264
1440
  name: "shouldCreateStage",
@@ -1268,18 +1444,18 @@ async function setupProject(project) {
1268
1444
  await createFirstStage(project.id, project.slug);
1269
1445
  }
1270
1446
  }
1271
- const fs3 = await import("fs/promises");
1272
- const path3 = await import("path");
1273
- const gitlabCiPath = path3.join(process.cwd(), ".gitlab-ci.yml");
1447
+ const fs4 = await import("fs/promises");
1448
+ const path4 = await import("path");
1449
+ const gitlabCiPath = path4.join(process.cwd(), ".gitlab-ci.yml");
1274
1450
  let hasGitLabPipeline = false;
1275
1451
  try {
1276
- await fs3.access(gitlabCiPath);
1452
+ await fs4.access(gitlabCiPath);
1277
1453
  hasGitLabPipeline = true;
1278
1454
  } catch {
1279
1455
  }
1280
1456
  if (!hasGitLabPipeline) {
1281
1457
  logger.newline();
1282
- const { shouldCreatePipeline } = await prompts({
1458
+ const { shouldCreatePipeline } = await prompts2({
1283
1459
  initial: false,
1284
1460
  message: "M\xF6chten Sie eine CI/CD Pipeline anlegen?",
1285
1461
  name: "shouldCreatePipeline",
@@ -1289,6 +1465,7 @@ async function setupProject(project) {
1289
1465
  await createPipeline(project.id);
1290
1466
  }
1291
1467
  }
1468
+ await offerAiAssistance(project.slug);
1292
1469
  await showFinalSummary(project);
1293
1470
  }
1294
1471
  async function createNewProject(slug) {
@@ -1330,7 +1507,7 @@ async function createNewProject(slug) {
1330
1507
  type: "text"
1331
1508
  }
1332
1509
  );
1333
- const projectDetails = await prompts(promptQuestions);
1510
+ const projectDetails = await prompts2(promptQuestions);
1334
1511
  if (!projectDetails.name) {
1335
1512
  logger.warning("Projekterstellung abgebrochen");
1336
1513
  addJsonData({ initialized: false, reason: "cancelled" });
@@ -1358,7 +1535,7 @@ async function createNewProject(slug) {
1358
1535
  logger.success(`Projekt "${newProject.name}" wurde erstellt!`);
1359
1536
  configService.setProject(newProject.slug);
1360
1537
  logger.newline();
1361
- const { shouldCreateStage } = await prompts({
1538
+ const { shouldCreateStage } = await prompts2({
1362
1539
  initial: true,
1363
1540
  message: "M\xF6chten Sie jetzt die erste Stage anlegen?",
1364
1541
  name: "shouldCreateStage",
@@ -1368,7 +1545,7 @@ async function createNewProject(slug) {
1368
1545
  await createFirstStage(newProject.id, newProject.slug);
1369
1546
  }
1370
1547
  logger.newline();
1371
- const { shouldCreatePipeline } = await prompts({
1548
+ const { shouldCreatePipeline } = await prompts2({
1372
1549
  initial: true,
1373
1550
  message: "M\xF6chten Sie eine CI/CD Pipeline anlegen?",
1374
1551
  name: "shouldCreatePipeline",
@@ -1437,7 +1614,7 @@ async function createFirstStage(projectId, projectSlug) {
1437
1614
  type: "select"
1438
1615
  });
1439
1616
  }
1440
- const stageDetails = await prompts(stageQuestions);
1617
+ const stageDetails = await prompts2(stageQuestions);
1441
1618
  if (!stageDetails.name || !stageDetails.slug) {
1442
1619
  logger.warning("Stage-Erstellung abgebrochen");
1443
1620
  return;
@@ -1475,7 +1652,7 @@ async function createPipeline(projectId) {
1475
1652
  type: "select"
1476
1653
  }
1477
1654
  ];
1478
- const pipelineDetails = await prompts(pipelineQuestions);
1655
+ const pipelineDetails = await prompts2(pipelineQuestions);
1479
1656
  if (!pipelineDetails.pipelineType) {
1480
1657
  logger.warning("Pipeline-Erstellung abgebrochen");
1481
1658
  return;
@@ -1488,18 +1665,18 @@ async function createPipeline(projectId) {
1488
1665
  logger.error(`Pipeline konnte nicht generiert werden: ${error || "Unbekannter Fehler"}`);
1489
1666
  return;
1490
1667
  }
1491
- const fs3 = await import("fs/promises");
1492
- const path3 = await import("path");
1668
+ const fs4 = await import("fs/promises");
1669
+ const path4 = await import("path");
1493
1670
  let filePath;
1494
1671
  if (pipelineDetails.pipelineType === "github") {
1495
- const workflowsDir = path3.join(process.cwd(), ".github", "workflows");
1496
- await fs3.mkdir(workflowsDir, { recursive: true });
1497
- filePath = path3.join(workflowsDir, "deploy.yml");
1672
+ const workflowsDir = path4.join(process.cwd(), ".github", "workflows");
1673
+ await fs4.mkdir(workflowsDir, { recursive: true });
1674
+ filePath = path4.join(workflowsDir, "deploy.yml");
1498
1675
  } else {
1499
- filePath = path3.join(process.cwd(), ".gitlab-ci.yml");
1676
+ filePath = path4.join(process.cwd(), ".gitlab-ci.yml");
1500
1677
  }
1501
1678
  try {
1502
- await fs3.writeFile(filePath, pipeline.content, "utf-8");
1679
+ await fs4.writeFile(filePath, pipeline.content, "utf-8");
1503
1680
  logger.success(`${pipeline.filename} wurde erstellt!`);
1504
1681
  logger.newline();
1505
1682
  const { data: secrets } = await apiClient.getPipelineSecrets(projectId, pipelineDetails.pipelineType);
@@ -1557,6 +1734,114 @@ async function showFinalSummary(project) {
1557
1734
  "F\xFChren Sie `turbo logs <stage>` aus, um Logs anzuzeigen"
1558
1735
  ]);
1559
1736
  }
1737
+ async function offerAiAssistance(projectSlug) {
1738
+ const detection = await detectProjectConfig();
1739
+ if (!detection.hasDockerCompose) {
1740
+ logger.newline();
1741
+ logger.warning("Keine docker-compose.yml gefunden.");
1742
+ logger.info("TurboOps Deployment ben\xF6tigt eine docker-compose.yml auf Root-Ebene.");
1743
+ const { shouldCreateDocker } = await prompts2({
1744
+ type: "confirm",
1745
+ name: "shouldCreateDocker",
1746
+ message: "Docker-Setup mit AI erstellen lassen?",
1747
+ initial: true
1748
+ });
1749
+ if (shouldCreateDocker) {
1750
+ await createDockerSetupWithAI();
1751
+ }
1752
+ }
1753
+ if ((detection.hasGitLabPipeline || detection.hasGitHubPipeline) && !detection.hasTurboOpsInPipeline) {
1754
+ logger.newline();
1755
+ const pipelineType = detection.hasGitLabPipeline ? "GitLab" : "GitHub";
1756
+ const { shouldIntegrate } = await prompts2({
1757
+ type: "confirm",
1758
+ name: "shouldIntegrate",
1759
+ message: `${pipelineType} Pipeline gefunden ohne TurboOps. Mit AI integrieren lassen?`,
1760
+ initial: true
1761
+ });
1762
+ if (shouldIntegrate) {
1763
+ await integratePipelineWithAI(detection, projectSlug);
1764
+ }
1765
+ }
1766
+ }
1767
+ async function createDockerSetupWithAI() {
1768
+ const tool = await aiToolsService.selectTool();
1769
+ if (!tool) return;
1770
+ const prompt = `Analysiere dieses Projekt und erstelle ein vollst\xE4ndiges Docker-Setup f\xFCr Production Deployment.
1771
+
1772
+ **Wichtig: TurboOps ben\xF6tigt eine docker-compose.yml auf Root-Ebene!**
1773
+
1774
+ Anforderungen:
1775
+ 1. Erstelle eine "docker-compose.yml" im Projekt-Root
1776
+ - Services f\xFCr alle Komponenten (z.B. api, app, db wenn n\xF6tig)
1777
+ - Verwende build-Direktive mit Pfad zu den Dockerfiles
1778
+ - Health-Checks f\xFCr alle Services
1779
+ - Production-geeignete Konfiguration
1780
+
1781
+ 2. Erstelle Dockerfiles in den entsprechenden Unterordnern
1782
+ - Multi-stage builds f\xFCr kleine Images
1783
+ - F\xFCr jeden Service ein eigenes Dockerfile (z.B. ./api/Dockerfile, ./app/Dockerfile)
1784
+ - Optimiert f\xFCr Production
1785
+
1786
+ 3. Erstelle eine ".dockerignore" auf Root-Ebene
1787
+ - node_modules, .git, etc. ausschlie\xDFen
1788
+
1789
+ 4. Falls Monorepo: Beachte die Projekt-Struktur
1790
+ - Pr\xFCfe ob projects/, packages/, apps/ Ordner existieren
1791
+
1792
+ Beispiel docker-compose.yml Struktur:
1793
+ \`\`\`yaml
1794
+ version: '3.8'
1795
+ services:
1796
+ api:
1797
+ build:
1798
+ context: .
1799
+ dockerfile: ./api/Dockerfile
1800
+ ports:
1801
+ - "3000:3000"
1802
+ healthcheck:
1803
+ test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
1804
+ interval: 30s
1805
+ timeout: 10s
1806
+ retries: 3
1807
+ \`\`\`
1808
+
1809
+ Erstelle alle notwendigen Dateien.`;
1810
+ const success = await aiToolsService.runWithPrompt(tool, prompt);
1811
+ if (success) {
1812
+ logger.success("Docker-Setup wurde erstellt!");
1813
+ }
1814
+ }
1815
+ async function integratePipelineWithAI(detection, projectSlug) {
1816
+ const tool = await aiToolsService.selectTool();
1817
+ if (!tool) return;
1818
+ const pipelineType = detection.hasGitLabPipeline ? "GitLab CI" : "GitHub Actions";
1819
+ const pipelineFile = detection.pipelinePath;
1820
+ const prompt = `Integriere TurboOps Deployment in die bestehende ${pipelineType} Pipeline.
1821
+
1822
+ Projekt-Slug: ${projectSlug}
1823
+ Pipeline-Datei: ${pipelineFile}
1824
+
1825
+ Anforderungen:
1826
+ 1. Die bestehende Pipeline-Struktur beibehalten
1827
+ 2. Einen neuen Deploy-Job/Step hinzuf\xFCgen der nach dem Build l\xE4uft
1828
+ 3. TurboOps CLI installieren: npm install -g @turboops/cli
1829
+ 4. Token setzen: turbo config set token \${TURBOOPS_TOKEN}
1830
+ 5. Deploy ausf\xFChren: turbo deploy <environment> --image <image-tag> --wait
1831
+
1832
+ Die erste Stage braucht --image, weitere Stages k\xF6nnen ohne --image deployen (Promotion).
1833
+
1834
+ F\xFCge am Ende einen Kommentar hinzu welche Secrets ben\xF6tigt werden:
1835
+ - TURBOOPS_TOKEN: TurboOps Projekt-Token
1836
+
1837
+ Modifiziere die Datei "${pipelineFile}" entsprechend.`;
1838
+ const success = await aiToolsService.runWithPrompt(tool, prompt);
1839
+ if (success) {
1840
+ logger.success("Pipeline wurde mit AI aktualisiert!");
1841
+ logger.newline();
1842
+ logger.info("Vergessen Sie nicht, das CI/CD Secret TURBOOPS_TOKEN zu konfigurieren.");
1843
+ }
1844
+ }
1560
1845
 
1561
1846
  // src/commands/logs.ts
1562
1847
  import { Command as Command4 } from "commander";
@@ -1825,17 +2110,33 @@ function getStatusColor2(status) {
1825
2110
 
1826
2111
  // src/commands/pipeline.ts
1827
2112
  import { Command as Command6 } from "commander";
1828
- import prompts2 from "prompts";
2113
+ import prompts3 from "prompts";
1829
2114
  import chalk7 from "chalk";
1830
- import * as fs2 from "fs/promises";
1831
- import * as path2 from "path";
2115
+ import * as fs3 from "fs/promises";
2116
+ import * as path3 from "path";
1832
2117
  var pipelineCommand = new Command6("pipeline").description("Manage CI/CD pipeline configuration");
1833
2118
  pipelineCommand.command("generate").description("Generate CI/CD pipeline configuration").option("-t, --type <type>", "Pipeline type (gitlab, github)").option("-f, --force", "Overwrite existing pipeline file").option("-o, --output <path>", "Custom output path").action(async (options) => {
1834
2119
  const { project } = await getCommandContext();
1835
2120
  logger.header("CI/CD Pipeline generieren");
2121
+ const detection = await detectProjectConfig();
2122
+ if (!detection.hasDockerCompose) {
2123
+ logger.warning("Keine docker-compose.yml gefunden.");
2124
+ logger.info("TurboOps Deployment ben\xF6tigt eine docker-compose.yml.");
2125
+ logger.newline();
2126
+ const { shouldCreateDocker } = await prompts3({
2127
+ type: "confirm",
2128
+ name: "shouldCreateDocker",
2129
+ message: "Docker-Setup mit AI erstellen?",
2130
+ initial: true
2131
+ });
2132
+ if (shouldCreateDocker) {
2133
+ await createDockerSetupWithAI2();
2134
+ logger.newline();
2135
+ }
2136
+ }
1836
2137
  let pipelineType = options.type;
1837
2138
  if (!pipelineType) {
1838
- const { selectedType } = await prompts2({
2139
+ const { selectedType } = await prompts3({
1839
2140
  choices: [
1840
2141
  { title: "GitLab CI/CD", value: "gitlab" },
1841
2142
  { title: "GitHub Actions", value: "github" }
@@ -1858,25 +2159,49 @@ pipelineCommand.command("generate").description("Generate CI/CD pipeline configu
1858
2159
  }
1859
2160
  let outputPath;
1860
2161
  if (options.output) {
1861
- outputPath = path2.resolve(options.output);
2162
+ outputPath = path3.resolve(options.output);
1862
2163
  } else if (pipelineType === "github") {
1863
- outputPath = path2.join(process.cwd(), ".github", "workflows", "deploy.yml");
2164
+ outputPath = path3.join(process.cwd(), ".github", "workflows", "deploy.yml");
1864
2165
  } else {
1865
- outputPath = path2.join(process.cwd(), ".gitlab-ci.yml");
2166
+ outputPath = path3.join(process.cwd(), ".gitlab-ci.yml");
1866
2167
  }
1867
2168
  if (!options.force) {
1868
2169
  try {
1869
- await fs2.access(outputPath);
1870
- const { shouldOverwrite } = await prompts2({
1871
- initial: false,
1872
- message: `${path2.basename(outputPath)} existiert bereits. \xDCberschreiben?`,
1873
- name: "shouldOverwrite",
1874
- type: "confirm"
1875
- });
1876
- if (!shouldOverwrite) {
1877
- logger.info("Pipeline-Generierung abgebrochen");
1878
- addJsonData({ generated: false, reason: "file_exists" });
1879
- return;
2170
+ await fs3.access(outputPath);
2171
+ const content = await fs3.readFile(outputPath, "utf-8");
2172
+ const hasTurboOps = content.includes("turbo deploy") || content.includes("@turboops/cli");
2173
+ if (!hasTurboOps) {
2174
+ const { action } = await prompts3({
2175
+ type: "select",
2176
+ name: "action",
2177
+ message: `${path3.basename(outputPath)} existiert bereits ohne TurboOps.`,
2178
+ choices: [
2179
+ { title: "Mit AI integrieren (empfohlen)", value: "ai-integrate" },
2180
+ { title: "\xDCberschreiben", value: "overwrite" },
2181
+ { title: "Abbrechen", value: "cancel" }
2182
+ ]
2183
+ });
2184
+ if (action === "cancel" || !action) {
2185
+ logger.info("Pipeline-Generierung abgebrochen");
2186
+ addJsonData({ generated: false, reason: "cancelled" });
2187
+ return;
2188
+ }
2189
+ if (action === "ai-integrate") {
2190
+ await integratePipelineWithAI2(pipelineType, project.slug, outputPath);
2191
+ return;
2192
+ }
2193
+ } else {
2194
+ const { shouldOverwrite } = await prompts3({
2195
+ initial: false,
2196
+ message: `${path3.basename(outputPath)} existiert bereits mit TurboOps. \xDCberschreiben?`,
2197
+ name: "shouldOverwrite",
2198
+ type: "confirm"
2199
+ });
2200
+ if (!shouldOverwrite) {
2201
+ logger.info("Pipeline-Generierung abgebrochen");
2202
+ addJsonData({ generated: false, reason: "file_exists" });
2203
+ return;
2204
+ }
1880
2205
  }
1881
2206
  } catch {
1882
2207
  }
@@ -1890,13 +2215,13 @@ pipelineCommand.command("generate").description("Generate CI/CD pipeline configu
1890
2215
  addJsonData({ error: error || "Unknown error", generated: false });
1891
2216
  process.exit(13 /* API_ERROR */);
1892
2217
  }
1893
- const outputDir = path2.dirname(outputPath);
1894
- await fs2.mkdir(outputDir, { recursive: true });
2218
+ const outputDir = path3.dirname(outputPath);
2219
+ await fs3.mkdir(outputDir, { recursive: true });
1895
2220
  try {
1896
- await fs2.writeFile(outputPath, pipeline.content, "utf-8");
1897
- logger.success(`${path2.relative(process.cwd(), outputPath)} wurde erstellt!`);
2221
+ await fs3.writeFile(outputPath, pipeline.content, "utf-8");
2222
+ logger.success(`${path3.relative(process.cwd(), outputPath)} wurde erstellt!`);
1898
2223
  addJsonData({
1899
- filename: path2.relative(process.cwd(), outputPath),
2224
+ filename: path3.relative(process.cwd(), outputPath),
1900
2225
  generated: true,
1901
2226
  project: project.slug,
1902
2227
  type: pipelineType
@@ -1954,11 +2279,160 @@ async function showSecrets(projectId, pipelineType) {
1954
2279
  logger.info("Projekt-Token k\xF6nnen Sie in der TurboOps Web-UI erstellen:");
1955
2280
  logger.info("Projekt \u2192 Settings \u2192 Tokens \u2192 Neuen Token erstellen");
1956
2281
  }
2282
+ async function createDockerSetupWithAI2() {
2283
+ const tool = await aiToolsService.selectTool();
2284
+ if (!tool) return;
2285
+ const prompt = `Analysiere dieses Projekt und erstelle ein vollst\xE4ndiges Docker-Setup f\xFCr Production Deployment.
2286
+
2287
+ **Wichtig: TurboOps ben\xF6tigt eine docker-compose.yml auf Root-Ebene!**
2288
+
2289
+ Anforderungen:
2290
+ 1. Erstelle eine "docker-compose.yml" im Projekt-Root
2291
+ - Services f\xFCr alle Komponenten (z.B. api, app, db wenn n\xF6tig)
2292
+ - Verwende build-Direktive mit Pfad zu den Dockerfiles
2293
+ - Health-Checks f\xFCr alle Services
2294
+ - Production-geeignete Konfiguration
2295
+
2296
+ 2. Erstelle Dockerfiles in den entsprechenden Unterordnern
2297
+ - Multi-stage builds f\xFCr kleine Images
2298
+ - F\xFCr jeden Service ein eigenes Dockerfile (z.B. ./api/Dockerfile, ./app/Dockerfile)
2299
+ - Optimiert f\xFCr Production
2300
+
2301
+ 3. Erstelle eine ".dockerignore" auf Root-Ebene
2302
+ - node_modules, .git, etc. ausschlie\xDFen
2303
+
2304
+ 4. Falls Monorepo: Beachte die Projekt-Struktur
2305
+ - Pr\xFCfe ob projects/, packages/, apps/ Ordner existieren
2306
+
2307
+ Erstelle alle notwendigen Dateien.`;
2308
+ const success = await aiToolsService.runWithPrompt(tool, prompt);
2309
+ if (success) {
2310
+ logger.success("Docker-Setup wurde erstellt!");
2311
+ }
2312
+ }
2313
+ async function integratePipelineWithAI2(pipelineType, projectSlug, pipelinePath) {
2314
+ const tool = await aiToolsService.selectTool();
2315
+ if (!tool) {
2316
+ addJsonData({ generated: false, reason: "no_ai_tool" });
2317
+ return;
2318
+ }
2319
+ const typeName = pipelineType === "gitlab" ? "GitLab CI" : "GitHub Actions";
2320
+ const prompt = `Integriere TurboOps Deployment in die bestehende ${typeName} Pipeline.
2321
+
2322
+ Projekt-Slug: ${projectSlug}
2323
+ Pipeline-Datei: ${pipelinePath}
2324
+
2325
+ Anforderungen:
2326
+ 1. Die bestehende Pipeline-Struktur beibehalten
2327
+ 2. Einen neuen Deploy-Job/Step hinzuf\xFCgen der nach dem Build l\xE4uft
2328
+ 3. TurboOps CLI installieren: npm install -g @turboops/cli
2329
+ 4. Token setzen: turbo config set token \${TURBOOPS_TOKEN}
2330
+ 5. Deploy ausf\xFChren: turbo deploy <environment> --image <image-tag> --wait
2331
+
2332
+ Die erste Stage braucht --image, weitere Stages k\xF6nnen ohne --image deployen (Auto-Promotion).
2333
+
2334
+ Secrets die ben\xF6tigt werden:
2335
+ - TURBOOPS_TOKEN: TurboOps Projekt-Token
2336
+
2337
+ Modifiziere die Datei "${pipelinePath}" entsprechend.`;
2338
+ const success = await aiToolsService.runWithPrompt(tool, prompt);
2339
+ if (success) {
2340
+ logger.success("Pipeline wurde mit AI aktualisiert!");
2341
+ logger.newline();
2342
+ logger.info("Vergessen Sie nicht, das CI/CD Secret TURBOOPS_TOKEN zu konfigurieren.");
2343
+ addJsonData({ generated: true, method: "ai-integration" });
2344
+ } else {
2345
+ addJsonData({ generated: false, reason: "ai_failed" });
2346
+ }
2347
+ }
2348
+
2349
+ // src/commands/docker.ts
2350
+ import { Command as Command7 } from "commander";
2351
+ import prompts4 from "prompts";
2352
+ var DOCKER_SETUP_PROMPT = `Analysiere dieses Projekt und erstelle ein vollst\xE4ndiges Docker-Setup f\xFCr Production Deployment.
2353
+
2354
+ **Wichtig: TurboOps ben\xF6tigt eine docker-compose.yml auf Root-Ebene!**
2355
+
2356
+ Anforderungen:
2357
+ 1. Erstelle eine "docker-compose.yml" im Projekt-Root
2358
+ - Services f\xFCr alle Komponenten (z.B. api, app, db wenn n\xF6tig)
2359
+ - Verwende build-Direktive mit Pfad zu den Dockerfiles
2360
+ - Health-Checks f\xFCr alle Services
2361
+ - Production-geeignete Konfiguration
2362
+
2363
+ 2. Erstelle Dockerfiles in den entsprechenden Unterordnern
2364
+ - Multi-stage builds f\xFCr kleine Images
2365
+ - F\xFCr jeden Service ein eigenes Dockerfile (z.B. ./api/Dockerfile, ./app/Dockerfile)
2366
+ - Optimiert f\xFCr Production
2367
+
2368
+ 3. Erstelle eine ".dockerignore" auf Root-Ebene
2369
+ - node_modules, .git, etc. ausschlie\xDFen
2370
+
2371
+ 4. Falls Monorepo: Beachte die Projekt-Struktur
2372
+ - Pr\xFCfe ob projects/, packages/, apps/ Ordner existieren
2373
+
2374
+ Beispiel docker-compose.yml Struktur:
2375
+ \`\`\`yaml
2376
+ version: '3.8'
2377
+ services:
2378
+ api:
2379
+ build:
2380
+ context: .
2381
+ dockerfile: ./api/Dockerfile
2382
+ ports:
2383
+ - "3000:3000"
2384
+ healthcheck:
2385
+ test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
2386
+ interval: 30s
2387
+ timeout: 10s
2388
+ retries: 3
2389
+ \`\`\`
2390
+
2391
+ Erstelle alle notwendigen Dateien.`;
2392
+ var dockerCommand = new Command7("docker").description("Manage Docker configuration");
2393
+ dockerCommand.command("generate").description("Docker-Setup (docker-compose + Dockerfiles) mit AI erstellen").option("-f, --force", "Bestehende Dateien \xFCberschreiben").action(async (options) => {
2394
+ logger.header("Docker-Setup generieren");
2395
+ const detection = await detectProjectConfig();
2396
+ if (detection.hasDockerCompose && !options.force) {
2397
+ logger.warning("docker-compose.yml existiert bereits.");
2398
+ const { shouldOverwrite } = await prompts4({
2399
+ type: "confirm",
2400
+ name: "shouldOverwrite",
2401
+ message: "M\xF6chten Sie das bestehende Setup \xFCberschreiben?",
2402
+ initial: false
2403
+ });
2404
+ if (!shouldOverwrite) {
2405
+ logger.info("Docker-Setup Generierung abgebrochen");
2406
+ addJsonData({ generated: false, reason: "file_exists" });
2407
+ return;
2408
+ }
2409
+ }
2410
+ const tool = await aiToolsService.selectTool();
2411
+ if (!tool) {
2412
+ addJsonData({ generated: false, reason: "no_ai_tool" });
2413
+ return;
2414
+ }
2415
+ const success = await aiToolsService.runWithPrompt(tool, DOCKER_SETUP_PROMPT);
2416
+ if (success) {
2417
+ logger.success("Docker-Setup wurde erstellt!");
2418
+ logger.newline();
2419
+ logger.info("N\xE4chste Schritte:");
2420
+ logger.list([
2421
+ "Pr\xFCfen Sie die erstellten Dateien",
2422
+ "Passen Sie Umgebungsvariablen an",
2423
+ "Testen Sie mit: docker compose build && docker compose up"
2424
+ ]);
2425
+ addJsonData({ generated: true });
2426
+ } else {
2427
+ logger.warning("Docker-Setup Generierung wurde abgebrochen oder ist fehlgeschlagen.");
2428
+ addJsonData({ generated: false, reason: "ai_failed" });
2429
+ }
2430
+ });
1957
2431
 
1958
2432
  // src/index.ts
1959
2433
  var VERSION = getCurrentVersion();
1960
2434
  var shouldCheckUpdate = true;
1961
- var program = new Command7();
2435
+ var program = new Command8();
1962
2436
  program.name("turbo").description("TurboCLI - Command line interface for TurboOps deployments").version(VERSION, "-v, --version", "Show version number").option("--project <slug>", "Override project slug").option("--token <token>", "Override API token").option("--json", "Output as JSON").option("--quiet", "Only show errors").option("--verbose", "Show debug output").option("--no-update-check", "Skip version check");
1963
2437
  program.hook("preAction", (thisCommand) => {
1964
2438
  const opts = thisCommand.opts();
@@ -1997,6 +2471,7 @@ program.addCommand(configCommand);
1997
2471
  program.addCommand(logsCommand);
1998
2472
  program.addCommand(deployCommand);
1999
2473
  program.addCommand(pipelineCommand);
2474
+ program.addCommand(dockerCommand);
2000
2475
  program.command("self-update").description("Update TurboCLI to the latest version").action(async () => {
2001
2476
  logger.info("Checking for updates...");
2002
2477
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turboops/cli",
3
- "version": "1.0.0-dev.584",
3
+ "version": "1.0.0-dev.585",
4
4
  "description": "TurboCLI - Command line interface for TurboOps deployments",
5
5
  "author": "lenne.tech GmbH",
6
6
  "license": "MIT",