@turboops/cli 1.0.0-dev.588 → 1.0.0-dev.590

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 +564 -381
  2. package/package.json +5 -2
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 Command8 } from "commander";
4
+ import { Command as Command7 } from "commander";
5
5
  import chalk8 from "chalk";
6
6
 
7
7
  // src/services/config.ts
@@ -194,13 +194,16 @@ function getPackageName() {
194
194
  }
195
195
 
196
196
  // src/services/config.ts
197
+ var BUILD_ENV = true ? "dev" : void 0;
197
198
  var API_URLS = {
198
- production: "https://api.turbo-ops.de",
199
- dev: "https://api.dev.turbo-ops.de"
199
+ local: "http://localhost:3000",
200
+ dev: "https://api.dev.turbo-ops.de",
201
+ prod: "https://api.turbo-ops.de"
200
202
  };
201
203
  var APP_URLS = {
202
- production: "https://turbo-ops.de",
203
- dev: "https://dev.turbo-ops.de"
204
+ local: "http://localhost:3001",
205
+ dev: "https://dev.turbo-ops.de",
206
+ prod: "https://turbo-ops.de"
204
207
  };
205
208
  var LOCAL_CONFIG_FILE = ".turboops.json";
206
209
  var globalDefaults = {
@@ -258,10 +261,16 @@ function writeLocalConfig(config) {
258
261
  return false;
259
262
  }
260
263
  }
261
- function getApiUrl() {
264
+ function getVersionBasedEnvironment() {
262
265
  const version = getCurrentVersion();
263
266
  const isDevVersion = version.includes("-dev");
264
- return isDevVersion ? API_URLS.dev : API_URLS.production;
267
+ return isDevVersion ? "dev" : "prod";
268
+ }
269
+ function getEnvironment() {
270
+ return BUILD_ENV || getVersionBasedEnvironment();
271
+ }
272
+ function getApiUrl() {
273
+ return API_URLS[getEnvironment()];
265
274
  }
266
275
  var configService = {
267
276
  /**
@@ -275,12 +284,16 @@ var configService = {
275
284
  },
276
285
  /**
277
286
  * Get App URL (frontend)
278
- * Returns the appropriate frontend URL based on CLI version
287
+ * Returns the appropriate frontend URL based on environment
279
288
  */
280
289
  getAppUrl() {
281
- const version = getCurrentVersion();
282
- const isDevVersion = version.includes("-dev");
283
- return isDevVersion ? APP_URLS.dev : APP_URLS.production;
290
+ return APP_URLS[getEnvironment()];
291
+ },
292
+ /**
293
+ * Get current environment
294
+ */
295
+ getEnvironment() {
296
+ return getEnvironment();
284
297
  },
285
298
  /**
286
299
  * Get authentication token (global - user session)
@@ -408,8 +421,10 @@ var configService = {
408
421
  const isProjectToken2 = this.isProjectToken();
409
422
  const isCliToken = this.isCliSessionToken();
410
423
  const localConfigPath = this.getLocalConfigPath();
424
+ const env = getEnvironment();
411
425
  logger.header("Configuration");
412
426
  logger.table({
427
+ Environment: env,
413
428
  "API URL": data.apiUrl,
414
429
  Token: data.token || "Not set",
415
430
  "Token Type": isProjectToken2 ? "Project Token (CI/CD)" : isCliToken ? "CLI Session Token" : data.token ? "User Token" : "N/A",
@@ -655,6 +670,12 @@ var apiClient = {
655
670
  async getDeploymentServers() {
656
671
  return this.request("GET", "/server?deploymentReady=true");
657
672
  },
673
+ /**
674
+ * Update project's detected configuration (partial update)
675
+ */
676
+ async updateProjectConfig(projectId, config) {
677
+ return this.request("PATCH", `/deployment/projects/${projectId}/config`, config);
678
+ },
658
679
  /**
659
680
  * Generate CI/CD pipeline configuration
660
681
  */
@@ -1213,16 +1234,18 @@ import prompts2 from "prompts";
1213
1234
  // src/services/ai-tools.ts
1214
1235
  import { execSync, spawn } from "child_process";
1215
1236
  import prompts from "prompts";
1237
+ import ora2 from "ora";
1238
+ import chalk4 from "chalk";
1216
1239
  var AI_TOOLS = {
1217
1240
  claude: {
1218
1241
  name: "Claude Code",
1219
1242
  command: "claude",
1220
- promptFlag: "-p"
1243
+ args: ["-p", "--dangerously-skip-permissions", "--verbose", "--output-format", "stream-json"]
1221
1244
  },
1222
1245
  codex: {
1223
1246
  name: "OpenAI Codex",
1224
1247
  command: "codex",
1225
- promptFlag: ""
1248
+ args: []
1226
1249
  }
1227
1250
  };
1228
1251
  var aiToolsService = {
@@ -1271,32 +1294,171 @@ var aiToolsService = {
1271
1294
  },
1272
1295
  /**
1273
1296
  * Run AI tool with a prompt in the current directory
1274
- * The tool runs interactively, inheriting stdio
1297
+ * Runs non-interactively with real-time output streaming
1275
1298
  */
1276
- async runWithPrompt(tool, prompt) {
1299
+ async runWithPrompt(tool, prompt, verbose = false) {
1277
1300
  const config = AI_TOOLS[tool];
1278
1301
  logger.newline();
1279
- logger.info(`Starte ${config.name}...`);
1280
- logger.info("Das AI-Tool wird interaktiv ausgef\xFChrt. Folgen Sie den Anweisungen.");
1281
- logger.newline();
1302
+ const args = [...config.args, prompt];
1303
+ if (verbose) {
1304
+ console.log(chalk4.cyan("[DEBUG] === AI Tool Debug Mode ==="));
1305
+ console.log(chalk4.dim(`[DEBUG] Tool: ${config.name}`));
1306
+ console.log(chalk4.dim(`[DEBUG] Command: ${config.command}`));
1307
+ console.log(chalk4.dim(`[DEBUG] Args: ${config.args.join(" ")} + prompt (${prompt.length} chars)`));
1308
+ console.log(chalk4.dim(`[DEBUG] CWD: ${process.cwd()}`));
1309
+ console.log(chalk4.dim(`[DEBUG] Prompt preview: ${prompt.substring(0, 100)}...`));
1310
+ console.log("");
1311
+ }
1312
+ const spinner = ora2({
1313
+ text: `${config.name} arbeitet...`,
1314
+ color: "cyan"
1315
+ }).start();
1282
1316
  return new Promise((resolve2) => {
1283
- const args = config.promptFlag ? [config.promptFlag, prompt] : [prompt];
1317
+ if (verbose) {
1318
+ spinner.stop();
1319
+ console.log(chalk4.dim("[DEBUG] Spawning child process..."));
1320
+ spinner.start();
1321
+ }
1284
1322
  const child = spawn(config.command, args, {
1285
- stdio: "inherit",
1286
1323
  cwd: process.cwd(),
1287
- shell: process.platform === "win32"
1324
+ shell: process.platform === "win32",
1325
+ stdio: ["pipe", "pipe", "pipe"],
1326
+ // stdin must be piped and closed for non-interactive mode
1327
+ env: {
1328
+ ...process.env,
1329
+ FORCE_COLOR: "1"
1330
+ }
1331
+ });
1332
+ child.stdin?.end();
1333
+ if (verbose) {
1334
+ spinner.stop();
1335
+ console.log(chalk4.dim(`[DEBUG] Child process spawned, PID: ${child.pid}`));
1336
+ console.log(chalk4.dim(`[DEBUG] Waiting for output...`));
1337
+ spinner.start();
1338
+ }
1339
+ let filesModified = [];
1340
+ let rawOutput = "";
1341
+ child.stdout?.on("data", (data) => {
1342
+ const text = data.toString();
1343
+ rawOutput += text;
1344
+ if (verbose) {
1345
+ spinner.stop();
1346
+ console.log(chalk4.dim(`[STDOUT] ${text.substring(0, 500)}`));
1347
+ spinner.start();
1348
+ }
1349
+ for (const line of text.split("\n")) {
1350
+ if (!line.trim()) continue;
1351
+ try {
1352
+ const event = JSON.parse(line);
1353
+ if (verbose) {
1354
+ spinner.stop();
1355
+ console.log(chalk4.dim(`[EVENT] type=${event.type}`));
1356
+ spinner.start();
1357
+ }
1358
+ switch (event.type) {
1359
+ case "assistant":
1360
+ if (event.message?.content) {
1361
+ for (const block of event.message.content) {
1362
+ if (block.type === "text" && block.text) {
1363
+ spinner.text = truncateText(block.text, 60);
1364
+ }
1365
+ if (block.type === "tool_use") {
1366
+ const action = formatToolAction(block.name, block.input);
1367
+ spinner.text = action;
1368
+ if (verbose) {
1369
+ spinner.stop();
1370
+ console.log(chalk4.dim(`[TOOL] ${block.name}: ${JSON.stringify(block.input).substring(0, 200)}`));
1371
+ spinner.start();
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ break;
1377
+ case "result":
1378
+ if (event.tool_name === "Write" || event.tool_name === "Edit") {
1379
+ const filePath = event.input?.file_path || event.input?.path;
1380
+ if (filePath && !filesModified.includes(filePath)) {
1381
+ filesModified.push(filePath);
1382
+ }
1383
+ }
1384
+ break;
1385
+ }
1386
+ } catch {
1387
+ if (line.trim()) {
1388
+ spinner.text = truncateText(line, 60);
1389
+ }
1390
+ }
1391
+ }
1392
+ });
1393
+ child.stderr?.on("data", (data) => {
1394
+ const text = data.toString().trim();
1395
+ if (verbose) {
1396
+ spinner.stop();
1397
+ console.log(chalk4.yellow(`[STDERR] ${text}`));
1398
+ spinner.start();
1399
+ }
1400
+ if (text && !text.includes("ExperimentalWarning")) {
1401
+ spinner.warn(text);
1402
+ spinner.start();
1403
+ }
1288
1404
  });
1289
1405
  child.on("close", (code) => {
1406
+ if (verbose) {
1407
+ spinner.stop();
1408
+ console.log(chalk4.dim(`[DEBUG] Process exited with code: ${code}`));
1409
+ console.log(chalk4.dim(`[DEBUG] Total output length: ${rawOutput.length} chars`));
1410
+ if (rawOutput.length === 0) {
1411
+ console.log(chalk4.yellow(`[DEBUG] No output received from ${config.name}!`));
1412
+ }
1413
+ }
1414
+ if (code === 0) {
1415
+ spinner.succeed(`${config.name} abgeschlossen`);
1416
+ if (filesModified.length > 0) {
1417
+ logger.newline();
1418
+ logger.info("Ge\xE4nderte Dateien:");
1419
+ for (const file of filesModified) {
1420
+ console.log(` ${chalk4.green("\u2713")} ${file}`);
1421
+ }
1422
+ }
1423
+ } else {
1424
+ spinner.fail(`${config.name} fehlgeschlagen (Exit Code: ${code})`);
1425
+ }
1290
1426
  logger.newline();
1291
1427
  resolve2(code === 0);
1292
1428
  });
1293
1429
  child.on("error", (err) => {
1294
- logger.error(`Fehler beim Ausf\xFChren von ${config.name}: ${err.message}`);
1430
+ if (verbose) {
1431
+ console.log(chalk4.red(`[ERROR] Spawn error: ${err.message}`));
1432
+ }
1433
+ spinner.fail(`Fehler beim Ausf\xFChren von ${config.name}: ${err.message}`);
1295
1434
  resolve2(false);
1296
1435
  });
1297
1436
  });
1298
1437
  }
1299
1438
  };
1439
+ function truncateText(text, maxLength) {
1440
+ const cleaned = text.replace(/\n/g, " ").trim();
1441
+ if (cleaned.length <= maxLength) return cleaned;
1442
+ return cleaned.slice(0, maxLength - 3) + "...";
1443
+ }
1444
+ function formatToolAction(toolName, input) {
1445
+ switch (toolName) {
1446
+ case "Read":
1447
+ return `Lese ${input.file_path || "Datei"}...`;
1448
+ case "Write":
1449
+ return `Schreibe ${input.file_path || "Datei"}...`;
1450
+ case "Edit":
1451
+ return `Bearbeite ${input.file_path || "Datei"}...`;
1452
+ case "Bash":
1453
+ return `F\xFChre Befehl aus...`;
1454
+ case "Glob":
1455
+ return `Suche Dateien...`;
1456
+ case "Grep":
1457
+ return `Durchsuche Code...`;
1458
+ default:
1459
+ return `${toolName}...`;
1460
+ }
1461
+ }
1300
1462
 
1301
1463
  // src/utils/detect-project.ts
1302
1464
  import * as fs2 from "fs/promises";
@@ -1304,30 +1466,11 @@ import * as path2 from "path";
1304
1466
  async function detectProjectConfig() {
1305
1467
  const cwd = process.cwd();
1306
1468
  const result = {
1307
- hasDockerfile: false,
1308
1469
  hasDockerCompose: false,
1309
1470
  hasGitLabPipeline: false,
1310
1471
  hasGitHubPipeline: false,
1311
1472
  hasTurboOpsInPipeline: false
1312
1473
  };
1313
- const dockerfilePaths = [
1314
- "Dockerfile",
1315
- "dockerfile",
1316
- "docker/Dockerfile",
1317
- "api/Dockerfile",
1318
- "app/Dockerfile",
1319
- "projects/api/Dockerfile",
1320
- "projects/app/Dockerfile"
1321
- ];
1322
- for (const dockerPath of dockerfilePaths) {
1323
- try {
1324
- await fs2.access(path2.join(cwd, dockerPath));
1325
- result.hasDockerfile = true;
1326
- result.dockerfilePath = dockerPath;
1327
- break;
1328
- } catch {
1329
- }
1330
- }
1331
1474
  const composePaths = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
1332
1475
  for (const composePath of composePaths) {
1333
1476
  try {
@@ -1367,8 +1510,12 @@ async function detectProjectConfig() {
1367
1510
  }
1368
1511
 
1369
1512
  // src/commands/init.ts
1370
- import chalk4 from "chalk";
1513
+ import chalk5 from "chalk";
1371
1514
  var initCommand = new Command3("init").description("Initialize TurboOps project in current directory").action(async () => {
1515
+ const verbose = process.env.DEBUG === "true";
1516
+ if (verbose) {
1517
+ console.log("[DEBUG] Verbose mode enabled");
1518
+ }
1372
1519
  logger.header("TurboOps Project Initialization");
1373
1520
  if (!configService.isAuthenticated()) {
1374
1521
  logger.warning("Nicht authentifiziert. Bitte melden Sie sich zuerst an.");
@@ -1414,7 +1561,7 @@ var initCommand = new Command3("init").description("Initialize TurboOps project
1414
1561
  () => apiClient.getProject(projectSlug)
1415
1562
  );
1416
1563
  if (project) {
1417
- await setupProject(project);
1564
+ await setupProject(project, verbose);
1418
1565
  return;
1419
1566
  }
1420
1567
  logger.newline();
@@ -1430,48 +1577,27 @@ var initCommand = new Command3("init").description("Initialize TurboOps project
1430
1577
  addJsonData({ initialized: false, reason: "project_not_found" });
1431
1578
  return;
1432
1579
  }
1433
- await createNewProject(projectSlug);
1580
+ await createNewProject(projectSlug, verbose);
1434
1581
  });
1435
- async function setupProject(project) {
1582
+ async function setupProject(project, verbose = false) {
1436
1583
  configService.setProject(project.slug);
1437
1584
  const { data: environments } = await apiClient.getEnvironments(project.id);
1438
1585
  if (!environments || environments.length === 0) {
1439
1586
  logger.newline();
1440
1587
  const { shouldCreateStage } = await prompts2({
1441
1588
  initial: true,
1442
- message: "Das Projekt hat noch keine Stages. M\xF6chten Sie jetzt eine erstellen?",
1589
+ message: "Das Projekt hat noch keine Stages. Standard-Stages erstellen (dev, test, prod)?",
1443
1590
  name: "shouldCreateStage",
1444
1591
  type: "confirm"
1445
1592
  });
1446
1593
  if (shouldCreateStage) {
1447
- await createFirstStage(project.id, project.slug);
1594
+ await createDefaultStages(project.id, project.slug);
1448
1595
  }
1449
1596
  }
1450
- const fs4 = await import("fs/promises");
1451
- const path4 = await import("path");
1452
- const gitlabCiPath = path4.join(process.cwd(), ".gitlab-ci.yml");
1453
- let hasGitLabPipeline = false;
1454
- try {
1455
- await fs4.access(gitlabCiPath);
1456
- hasGitLabPipeline = true;
1457
- } catch {
1458
- }
1459
- if (!hasGitLabPipeline) {
1460
- logger.newline();
1461
- const { shouldCreatePipeline } = await prompts2({
1462
- initial: false,
1463
- message: "M\xF6chten Sie eine CI/CD Pipeline anlegen?",
1464
- name: "shouldCreatePipeline",
1465
- type: "confirm"
1466
- });
1467
- if (shouldCreatePipeline) {
1468
- await createPipeline(project.id);
1469
- }
1470
- }
1471
- await offerAiAssistance(project.slug);
1597
+ await offerAiAssistance(project.slug, project.id, verbose);
1472
1598
  await showFinalSummary(project);
1473
1599
  }
1474
- async function createNewProject(slug) {
1600
+ async function createNewProject(slug, verbose = false) {
1475
1601
  logger.newline();
1476
1602
  logger.header("Neues Projekt erstellen");
1477
1603
  const { data: customers } = await withSpinner(
@@ -1545,105 +1671,76 @@ async function createNewProject(slug) {
1545
1671
  logger.newline();
1546
1672
  const { shouldCreateStage } = await prompts2({
1547
1673
  initial: true,
1548
- message: "M\xF6chten Sie jetzt die erste Stage anlegen?",
1674
+ message: "Standard-Stages erstellen (dev, test, prod)?",
1549
1675
  name: "shouldCreateStage",
1550
1676
  type: "confirm"
1551
1677
  });
1552
1678
  if (shouldCreateStage) {
1553
- await createFirstStage(newProject.id, newProject.slug);
1554
- }
1555
- logger.newline();
1556
- const { shouldCreatePipeline } = await prompts2({
1557
- initial: true,
1558
- message: "M\xF6chten Sie eine CI/CD Pipeline anlegen?",
1559
- name: "shouldCreatePipeline",
1560
- type: "confirm"
1561
- });
1562
- if (shouldCreatePipeline) {
1563
- await createPipeline(newProject.id);
1679
+ await createDefaultStages(newProject.id, newProject.slug);
1564
1680
  }
1681
+ await offerAiAssistance(newProject.slug, newProject.id, verbose);
1565
1682
  await showFinalSummary(newProject);
1566
1683
  }
1567
- async function createFirstStage(projectId, projectSlug) {
1684
+ var DEFAULT_STAGES = [
1685
+ {
1686
+ name: "Development",
1687
+ slug: "dev",
1688
+ type: "development",
1689
+ branch: "dev",
1690
+ stageOrder: 0
1691
+ },
1692
+ {
1693
+ name: "Test",
1694
+ slug: "test",
1695
+ type: "staging",
1696
+ branch: "test",
1697
+ stageOrder: 1
1698
+ },
1699
+ {
1700
+ name: "Production",
1701
+ slug: "prod",
1702
+ type: "production",
1703
+ branch: "main",
1704
+ stageOrder: 2
1705
+ }
1706
+ ];
1707
+ async function createDefaultStages(projectId, projectSlug) {
1568
1708
  logger.newline();
1569
- logger.header("Erste Stage erstellen");
1570
- const { data: servers } = await withSpinner(
1571
- "Lade verf\xFCgbare Server...",
1572
- () => apiClient.getDeploymentServers()
1573
- );
1574
- const stageTypes = [
1575
- { title: "Development", value: "development" },
1576
- { title: "Staging", value: "staging" },
1577
- { title: "Production", value: "production" }
1578
- ];
1579
- const stageQuestions = [
1580
- {
1581
- choices: stageTypes,
1582
- initial: 0,
1583
- message: "Stage-Typ:",
1584
- name: "type",
1585
- type: "select"
1586
- },
1587
- {
1588
- initial: (prev) => prev === "production" ? "Production" : prev === "staging" ? "Staging" : "Development",
1589
- message: "Stage-Name:",
1590
- name: "name",
1591
- type: "text",
1592
- validate: (value) => value.length > 0 || "Name ist erforderlich"
1593
- },
1594
- {
1595
- initial: (_prev, values) => values.type || "",
1596
- message: "Stage-Slug:",
1597
- name: "slug",
1598
- type: "text",
1599
- validate: (value) => value.length > 0 || "Slug ist erforderlich"
1600
- },
1601
- {
1602
- initial: (_prev, values) => `${values.slug || ""}.${projectSlug}.example.com`,
1603
- message: "Domain:",
1604
- name: "domain",
1605
- type: "text"
1606
- },
1607
- {
1608
- initial: (_prev, values) => values.type === "production" ? "main" : values.type === "staging" ? "staging" : "develop",
1609
- message: "Branch:",
1610
- name: "branch",
1611
- type: "text"
1612
- }
1613
- ];
1614
- if (servers && servers.length > 0) {
1615
- stageQuestions.push({
1616
- choices: [
1617
- { title: "(Sp\xE4ter ausw\xE4hlen)", value: "" },
1618
- ...servers.map((s) => ({ title: `${s.name} (${s.host})`, value: s.id }))
1619
- ],
1620
- message: "Server (optional):",
1621
- name: "server",
1622
- type: "select"
1623
- });
1624
- }
1625
- const stageDetails = await prompts2(stageQuestions);
1626
- if (!stageDetails.name || !stageDetails.slug) {
1709
+ logger.header("Standard-Stages erstellen");
1710
+ const { baseDomain } = await prompts2({
1711
+ type: "text",
1712
+ name: "baseDomain",
1713
+ message: "Basis-Domain (z.B. example.com):",
1714
+ initial: "example.com"
1715
+ });
1716
+ if (!baseDomain) {
1627
1717
  logger.warning("Stage-Erstellung abgebrochen");
1628
1718
  return;
1629
1719
  }
1630
- const { data: newStage, error: stageError } = await withSpinner(
1631
- "Erstelle Stage...",
1632
- () => apiClient.createStage({
1633
- project: projectId,
1634
- name: stageDetails.name,
1635
- slug: stageDetails.slug,
1636
- type: stageDetails.type,
1637
- domain: stageDetails.domain || void 0,
1638
- server: stageDetails.server || void 0,
1639
- branch: stageDetails.branch || void 0
1640
- })
1641
- );
1642
- if (stageError || !newStage) {
1643
- logger.error(`Stage konnte nicht erstellt werden: ${stageError || "Unbekannter Fehler"}`);
1644
- return;
1720
+ logger.newline();
1721
+ logger.info("Erstelle 3 Standard-Stages:");
1722
+ for (const stage of DEFAULT_STAGES) {
1723
+ const domain = `${stage.slug}.${projectSlug}.${baseDomain}`;
1724
+ const { data: newStage, error } = await withSpinner(
1725
+ `Erstelle ${stage.name}...`,
1726
+ () => apiClient.createStage({
1727
+ project: projectId,
1728
+ name: stage.name,
1729
+ slug: stage.slug,
1730
+ type: stage.type,
1731
+ stageOrder: stage.stageOrder,
1732
+ domain,
1733
+ branch: stage.branch
1734
+ })
1735
+ );
1736
+ if (error || !newStage) {
1737
+ logger.error(` \u2717 ${stage.name}: ${error || "Unbekannter Fehler"}`);
1738
+ } else {
1739
+ logger.success(` \u2713 ${stage.name} (${stage.slug}) - ${domain}`);
1740
+ }
1645
1741
  }
1646
- logger.success(`Stage "${newStage.name}" wurde erstellt!`);
1742
+ logger.newline();
1743
+ logger.info("Server k\xF6nnen sp\xE4ter in der Web-UI zugewiesen werden.");
1647
1744
  }
1648
1745
  async function createPipeline(projectId) {
1649
1746
  logger.newline();
@@ -1686,14 +1783,20 @@ async function createPipeline(projectId) {
1686
1783
  try {
1687
1784
  await fs4.writeFile(filePath, pipeline.content, "utf-8");
1688
1785
  logger.success(`${pipeline.filename} wurde erstellt!`);
1786
+ await apiClient.updateProjectConfig(projectId, {
1787
+ pipelineConfig: {
1788
+ hasPipeline: true,
1789
+ pipelineType: pipelineDetails.pipelineType
1790
+ }
1791
+ });
1689
1792
  logger.newline();
1690
1793
  const { data: secrets } = await apiClient.getPipelineSecrets(projectId, pipelineDetails.pipelineType);
1691
1794
  if (secrets && secrets.length > 0) {
1692
1795
  logger.header("Erforderliche CI/CD Secrets");
1693
1796
  for (const secret of secrets) {
1694
- const value = secret.isSecret ? chalk4.dim("(geheim)") : chalk4.cyan(secret.value || "-");
1695
- console.log(` ${chalk4.bold(secret.name)}: ${value}`);
1696
- console.log(` ${chalk4.dim(secret.description)}`);
1797
+ const value = secret.isSecret ? chalk5.dim("(geheim)") : chalk5.cyan(secret.value || "-");
1798
+ console.log(` ${chalk5.bold(secret.name)}: ${value}`);
1799
+ console.log(` ${chalk5.dim(secret.description)}`);
1697
1800
  }
1698
1801
  logger.newline();
1699
1802
  logger.info("F\xFCgen Sie diese Werte als CI/CD Secrets/Variables hinzu.");
@@ -1732,20 +1835,63 @@ async function showFinalSummary(project) {
1732
1835
  logger.newline();
1733
1836
  logger.header("Verf\xFCgbare Stages");
1734
1837
  for (const env of environments) {
1735
- console.log(` ${chalk4.bold(env.slug)} - ${env.name} (${env.type})`);
1838
+ console.log(` ${chalk5.bold(env.slug)} - ${env.name} (${env.type})`);
1736
1839
  }
1737
1840
  }
1738
1841
  logger.newline();
1842
+ logger.header("CI/CD Token einrichten");
1843
+ console.log(chalk5.cyan(" 1. Projekt-Token erstellen:"));
1844
+ console.log(` ${chalk5.dim("\u2192")} TurboOps Web-UI \xF6ffnen`);
1845
+ console.log(` ${chalk5.dim("\u2192")} Projekt "${project.name}" ausw\xE4hlen`);
1846
+ console.log(` ${chalk5.dim("\u2192")} Settings \u2192 Tokens \u2192 "Neuer Token"`);
1847
+ logger.newline();
1848
+ console.log(chalk5.cyan(" 2. Token als CI/CD Secret hinterlegen:"));
1849
+ console.log(chalk5.dim(" GitLab:"));
1850
+ console.log(` ${chalk5.dim("\u2192")} Settings \u2192 CI/CD \u2192 Variables`);
1851
+ console.log(` ${chalk5.dim("\u2192")} Variable: ${chalk5.bold("TURBOOPS_TOKEN")} = <dein-token>`);
1852
+ console.log(` ${chalk5.dim("\u2192")} Flags: Protected, Masked`);
1853
+ logger.newline();
1854
+ console.log(chalk5.dim(" GitHub:"));
1855
+ console.log(` ${chalk5.dim("\u2192")} Settings \u2192 Secrets and variables \u2192 Actions`);
1856
+ console.log(` ${chalk5.dim("\u2192")} New repository secret: ${chalk5.bold("TURBOOPS_TOKEN")}`);
1857
+ logger.newline();
1739
1858
  logger.header("N\xE4chste Schritte");
1740
1859
  logger.list([
1741
- "F\xFChren Sie `turbo status` aus, um alle Stages zu sehen",
1742
- "F\xFChren Sie `turbo logs <stage>` aus, um Logs anzuzeigen"
1860
+ "Projekt-Token in TurboOps erstellen (siehe oben)",
1861
+ "Token als CI/CD Secret hinterlegen",
1862
+ "Pipeline committen und pushen",
1863
+ "`turbo status` ausf\xFChren um Deployments zu pr\xFCfen"
1743
1864
  ]);
1744
1865
  }
1745
- async function offerAiAssistance(projectSlug) {
1866
+ async function offerAiAssistance(projectSlug, projectId, verbose = false) {
1746
1867
  const detection = await detectProjectConfig();
1747
- if (!detection.hasDockerCompose) {
1748
- logger.newline();
1868
+ logger.newline();
1869
+ logger.header("Docker Setup");
1870
+ if (detection.hasDockerCompose) {
1871
+ logger.success("docker-compose.yml gefunden.");
1872
+ const { error } = await apiClient.updateProjectConfig(projectId, {
1873
+ detectedConfig: {
1874
+ hasCompose: true,
1875
+ composePath: "docker-compose.yml"
1876
+ }
1877
+ });
1878
+ if (error) {
1879
+ logger.warning(`API Update fehlgeschlagen: ${error}`);
1880
+ }
1881
+ const { dockerAction } = await prompts2({
1882
+ type: "select",
1883
+ name: "dockerAction",
1884
+ message: "Docker-Setup:",
1885
+ choices: [
1886
+ { title: "Bestehende docker-compose.yml verwenden", value: "keep" },
1887
+ { title: "Neu erstellen mit AI (\xFCberschreibt bestehende)", value: "create" }
1888
+ ],
1889
+ initial: 0
1890
+ });
1891
+ if (dockerAction === "create") {
1892
+ await createDockerSetupWithAI(projectId, verbose);
1893
+ }
1894
+ } else {
1749
1895
  logger.warning("Keine docker-compose.yml gefunden.");
1750
1896
  logger.info("TurboOps Deployment ben\xF6tigt eine docker-compose.yml auf Root-Ebene.");
1751
1897
  const { shouldCreateDocker } = await prompts2({
@@ -1755,265 +1901,303 @@ async function offerAiAssistance(projectSlug) {
1755
1901
  initial: true
1756
1902
  });
1757
1903
  if (shouldCreateDocker) {
1758
- await createDockerSetupWithAI();
1904
+ await createDockerSetupWithAI(projectId, verbose);
1759
1905
  }
1760
1906
  }
1761
- if ((detection.hasGitLabPipeline || detection.hasGitHubPipeline) && !detection.hasTurboOpsInPipeline) {
1762
- logger.newline();
1907
+ logger.newline();
1908
+ logger.header("CI/CD Pipeline");
1909
+ const hasPipeline = detection.hasGitLabPipeline || detection.hasGitHubPipeline;
1910
+ if (hasPipeline) {
1763
1911
  const pipelineType = detection.hasGitLabPipeline ? "GitLab" : "GitHub";
1764
- const { shouldIntegrate } = await prompts2({
1912
+ if (detection.hasTurboOpsInPipeline) {
1913
+ logger.success(`${pipelineType} Pipeline mit TurboOps gefunden.`);
1914
+ const { error } = await apiClient.updateProjectConfig(projectId, {
1915
+ pipelineConfig: {
1916
+ hasPipeline: true,
1917
+ pipelineType: detection.hasGitLabPipeline ? "gitlab" : "github"
1918
+ }
1919
+ });
1920
+ if (error) {
1921
+ logger.warning(`API Update fehlgeschlagen: ${error}`);
1922
+ }
1923
+ const { pipelineAction } = await prompts2({
1924
+ type: "select",
1925
+ name: "pipelineAction",
1926
+ message: "Pipeline-Setup:",
1927
+ choices: [
1928
+ { title: "Bestehende Pipeline behalten", value: "keep" },
1929
+ { title: "Mit AI neu integrieren (\xFCberschreibt TurboOps-Teil)", value: "integrate" }
1930
+ ],
1931
+ initial: 0
1932
+ });
1933
+ if (pipelineAction === "integrate") {
1934
+ await integratePipelineWithAI(detection, projectSlug, verbose, projectId);
1935
+ }
1936
+ } else {
1937
+ logger.info(`${pipelineType} Pipeline gefunden (ohne TurboOps).`);
1938
+ const { shouldIntegrate } = await prompts2({
1939
+ type: "confirm",
1940
+ name: "shouldIntegrate",
1941
+ message: "TurboOps mit AI in bestehende Pipeline integrieren?",
1942
+ initial: true
1943
+ });
1944
+ if (shouldIntegrate) {
1945
+ await integratePipelineWithAI(detection, projectSlug, verbose, projectId);
1946
+ }
1947
+ }
1948
+ } else {
1949
+ logger.warning("Keine CI/CD Pipeline gefunden.");
1950
+ const { shouldCreatePipeline } = await prompts2({
1765
1951
  type: "confirm",
1766
- name: "shouldIntegrate",
1767
- message: `${pipelineType} Pipeline gefunden ohne TurboOps. Mit AI integrieren lassen?`,
1952
+ name: "shouldCreatePipeline",
1953
+ message: "Pipeline erstellen?",
1768
1954
  initial: true
1769
1955
  });
1770
- if (shouldIntegrate) {
1771
- await integratePipelineWithAI(detection, projectSlug);
1956
+ if (shouldCreatePipeline) {
1957
+ await createPipeline(projectId);
1772
1958
  }
1773
1959
  }
1774
1960
  }
1775
- async function createDockerSetupWithAI() {
1961
+ async function createDockerSetupWithAI(projectId, verbose = false) {
1776
1962
  const tool = await aiToolsService.selectTool();
1777
1963
  if (!tool) return;
1778
- const prompt = `Analysiere dieses Projekt und erstelle ein vollst\xE4ndiges Docker-Setup f\xFCr Production Deployment.
1779
-
1780
- **Wichtig: TurboOps ben\xF6tigt eine docker-compose.yml auf Root-Ebene!**
1964
+ const prompt = `Analysiere dieses Projekt und erstelle ein Docker-Setup f\xFCr TurboOps Production Deployment.
1781
1965
 
1782
- Anforderungen:
1783
- 1. Erstelle eine "docker-compose.yml" im Projekt-Root
1784
- - Services f\xFCr alle Komponenten (z.B. api, app, db wenn n\xF6tig)
1785
- - Verwende build-Direktive mit Pfad zu den Dockerfiles
1786
- - Health-Checks f\xFCr alle Services
1787
- - Production-geeignete Konfiguration
1966
+ === TURBOOPS ANFORDERUNGEN ===
1967
+ - docker-compose.yml MUSS auf Root-Ebene liegen
1968
+ - ALLE Abh\xE4ngigkeiten m\xFCssen enthalten sein (DB, Redis, etc.) - production-ready!
1969
+ - Images m\xFCssen immutable sein (keine Volume-Mounts f\xFCr Code)
1970
+ - Datenbank-Volumes f\xFCr Persistenz sind erlaubt (named volumes)
1971
+ - Environment-Variablen via \${VARIABLE} Syntax (werden zur Laufzeit injiziert)
1788
1972
 
1789
- 2. Erstelle Dockerfiles in den entsprechenden Unterordnern
1790
- - Multi-stage builds f\xFCr kleine Images
1791
- - F\xFCr jeden Service ein eigenes Dockerfile (z.B. ./api/Dockerfile, ./app/Dockerfile)
1792
- - Optimiert f\xFCr Production
1793
-
1794
- 3. Erstelle eine ".dockerignore" auf Root-Ebene
1795
- - node_modules, .git, etc. ausschlie\xDFen
1973
+ === ZU ERSTELLENDE DATEIEN ===
1796
1974
 
1797
- 4. Falls Monorepo: Beachte die Projekt-Struktur
1798
- - Pr\xFCfe ob projects/, packages/, apps/ Ordner existieren
1799
-
1800
- Beispiel docker-compose.yml Struktur:
1975
+ 1. docker-compose.yml (Root-Ebene) - ALLE Abh\xE4ngigkeiten enthalten!
1801
1976
  \`\`\`yaml
1802
- version: '3.8'
1803
1977
  services:
1978
+ # === DATENBANKEN & SERVICES ===
1979
+ mongo:
1980
+ image: mongo:7
1981
+ volumes:
1982
+ - mongo_data:/data/db
1983
+ environment:
1984
+ - MONGO_INITDB_ROOT_USERNAME=\${MONGO_USER:-admin}
1985
+ - MONGO_INITDB_ROOT_PASSWORD=\${MONGO_PASSWORD}
1986
+ healthcheck:
1987
+ test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
1988
+ interval: 30s
1989
+ timeout: 10s
1990
+ retries: 3
1991
+ restart: unless-stopped
1992
+
1993
+ redis:
1994
+ image: redis:7-alpine
1995
+ volumes:
1996
+ - redis_data:/data
1997
+ healthcheck:
1998
+ test: ["CMD", "redis-cli", "ping"]
1999
+ interval: 30s
2000
+ timeout: 10s
2001
+ retries: 3
2002
+ restart: unless-stopped
2003
+
2004
+ # === APPLICATION SERVICES ===
1804
2005
  api:
1805
2006
  build:
1806
2007
  context: .
1807
- dockerfile: ./api/Dockerfile
2008
+ dockerfile: ./projects/api/Dockerfile # oder ./api/Dockerfile
1808
2009
  ports:
1809
2010
  - "3000:3000"
2011
+ environment:
2012
+ - NODE_ENV=production
2013
+ - MONGO_URI=mongodb://\${MONGO_USER:-admin}:\${MONGO_PASSWORD}@mongo:27017/app?authSource=admin
2014
+ - REDIS_URL=redis://redis:6379
2015
+ depends_on:
2016
+ mongo:
2017
+ condition: service_healthy
2018
+ redis:
2019
+ condition: service_healthy
1810
2020
  healthcheck:
1811
- test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
2021
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
1812
2022
  interval: 30s
1813
2023
  timeout: 10s
1814
2024
  retries: 3
2025
+ start_period: 40s
2026
+ restart: unless-stopped
2027
+
2028
+ app:
2029
+ build:
2030
+ context: .
2031
+ dockerfile: ./projects/app/Dockerfile # oder ./app/Dockerfile
2032
+ ports:
2033
+ - "3001:3000"
2034
+ environment:
2035
+ - NUXT_PUBLIC_API_URL=http://api:3000
2036
+ depends_on:
2037
+ api:
2038
+ condition: service_healthy
2039
+ healthcheck:
2040
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"]
2041
+ interval: 30s
2042
+ timeout: 10s
2043
+ retries: 3
2044
+ restart: unless-stopped
2045
+
2046
+ volumes:
2047
+ mongo_data:
2048
+ redis_data:
1815
2049
  \`\`\`
1816
2050
 
1817
- Erstelle alle notwendigen Dateien.`;
1818
- const success = await aiToolsService.runWithPrompt(tool, prompt);
2051
+ 2. Dockerfile f\xFCr jeden Service (im jeweiligen Ordner)
2052
+ \`\`\`dockerfile
2053
+ # Multi-stage build
2054
+ FROM node:20-alpine AS builder
2055
+ WORKDIR /app
2056
+ COPY package*.json ./
2057
+ RUN npm ci
2058
+ COPY . .
2059
+ RUN npm run build
2060
+
2061
+ FROM node:20-alpine AS runner
2062
+ WORKDIR /app
2063
+ ENV NODE_ENV=production
2064
+ # Non-root user f\xFCr Sicherheit
2065
+ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
2066
+ COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
2067
+ COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
2068
+ COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
2069
+ USER nodejs
2070
+ EXPOSE 3000
2071
+ CMD ["node", "dist/main.js"]
2072
+ \`\`\`
2073
+
2074
+ 3. .dockerignore (Root-Ebene)
2075
+ \`\`\`
2076
+ node_modules
2077
+ .git
2078
+ .env*
2079
+ *.log
2080
+ dist
2081
+ .nuxt
2082
+ .output
2083
+ coverage
2084
+ \`\`\`
2085
+
2086
+ === WICHTIGE REGELN ===
2087
+ - Pr\xFCfe die tats\xE4chliche Projektstruktur (projects/, packages/, apps/, oder flach)
2088
+ - Passe Pfade entsprechend an
2089
+ - NestJS: CMD ["node", "dist/main.js"]
2090
+ - Nuxt: CMD ["node", ".output/server/index.mjs"]
2091
+ - Nutze wget statt curl f\xFCr healthchecks (curl nicht in alpine)
2092
+ - Named volumes f\xFCr Datenbank-Persistenz (mongo_data, redis_data, etc.)
2093
+ - Keine Code-Volume-Mounts (Code ist im Image)
2094
+ - depends_on mit condition: service_healthy f\xFCr korrekten Start-Order
2095
+ - Erkenne ben\xF6tigte Services aus package.json (mongoose \u2192 mongo, ioredis \u2192 redis, etc.)
2096
+
2097
+ Erstelle alle notwendigen Dateien basierend auf der erkannten Projektstruktur und Abh\xE4ngigkeiten.`;
2098
+ const success = await aiToolsService.runWithPrompt(tool, prompt, verbose);
1819
2099
  if (success) {
1820
2100
  logger.success("Docker-Setup wurde erstellt!");
2101
+ await apiClient.updateProjectConfig(projectId, {
2102
+ detectedConfig: {
2103
+ hasCompose: true,
2104
+ composePath: "docker-compose.yml"
2105
+ }
2106
+ });
1821
2107
  }
1822
2108
  }
1823
- async function integratePipelineWithAI(detection, projectSlug) {
2109
+ async function integratePipelineWithAI(detection, projectSlug, verbose = false, projectId) {
1824
2110
  const tool = await aiToolsService.selectTool();
1825
2111
  if (!tool) return;
1826
- const pipelineType = detection.hasGitLabPipeline ? "GitLab CI" : "GitHub Actions";
2112
+ const pipelineType = detection.hasGitLabPipeline ? "gitlab" : "github";
1827
2113
  const pipelineFile = detection.pipelinePath;
1828
- const prompt = `Integriere TurboOps Deployment in die bestehende ${pipelineType} Pipeline.
2114
+ let templateContent = "";
2115
+ if (projectId) {
2116
+ const { data: pipelineTemplate, error } = await withSpinner(
2117
+ "Lade TurboOps Pipeline-Template...",
2118
+ () => apiClient.generatePipeline(projectId, pipelineType)
2119
+ );
2120
+ if (error || !pipelineTemplate) {
2121
+ logger.warning(`Pipeline-Template konnte nicht geladen werden: ${error || "Unbekannter Fehler"}`);
2122
+ logger.info("Claude Code wird ein generisches Template verwenden.");
2123
+ } else {
2124
+ templateContent = pipelineTemplate.content;
2125
+ }
2126
+ }
2127
+ const pipelineTypeName = pipelineType === "gitlab" ? "GitLab CI" : "GitHub Actions";
2128
+ const prompt = templateContent ? `WICHTIG: Integriere das TurboOps Deployment-Template EXAKT wie vorgegeben in die bestehende ${pipelineTypeName} Pipeline.
2129
+
2130
+ Bestehende Pipeline-Datei: ${pipelineFile}
2131
+
2132
+ === TURBOOPS TEMPLATE (NICHT VER\xC4NDERN!) ===
2133
+ \`\`\`yaml
2134
+ ${templateContent}
2135
+ \`\`\`
2136
+ === ENDE TEMPLATE ===
2137
+
2138
+ STRIKTE REGELN:
2139
+ 1. Das TurboOps Template MUSS EXAKT so \xFCbernommen werden wie oben angegeben
2140
+ 2. KEINE \xC4nderungen an:
2141
+ - Variables (IMAGE_NAME, DOCKER_TLS_CERTDIR)
2142
+ - Job-Namen (build, deploy-dev, deploy-test, deploy-prod)
2143
+ - Script-Befehlen (turbo deploy, docker build, etc.)
2144
+ - Branch-Rules (die sind korrekt f\xFCr die konfigurierten Stages)
2145
+ - needs/dependencies zwischen Jobs
2146
+ 3. NUR erlaubte \xC4nderungen:
2147
+ - Stages der bestehenden Pipeline VOR den TurboOps-Stages einf\xFCgen
2148
+ - Bestehende Jobs der Pipeline BEHALTEN (z.B. lint, test)
2149
+ - TurboOps "build" Job muss von bestehenden Build-Jobs abh\xE4ngen (needs)
2150
+
2151
+ MERGE-STRATEGIE:
2152
+ - Bestehende stages: [lint, test, build] + TurboOps stages: [build, deploy-dev, deploy-test, deploy-prod]
2153
+ - Ergebnis: stages: [lint, test, build, turboops-build, deploy-dev, deploy-test, deploy-prod]
2154
+ - Der TurboOps "build" Job sollte in "turboops-build" umbenannt werden falls es bereits einen "build" Job gibt
2155
+ - turboops-build needs: [build] (vom bestehenden build Job)
2156
+
2157
+ Modifiziere die Datei "${pipelineFile}" entsprechend.` : `Erstelle eine neue ${pipelineTypeName} Pipeline f\xFCr TurboOps Deployment.
1829
2158
 
1830
2159
  Projekt-Slug: ${projectSlug}
1831
2160
  Pipeline-Datei: ${pipelineFile}
1832
2161
 
1833
- Anforderungen:
1834
- 1. Die bestehende Pipeline-Struktur beibehalten
1835
- 2. Einen neuen Deploy-Job/Step hinzuf\xFCgen der nach dem Build l\xE4uft
1836
- 3. TurboOps CLI installieren: npm install -g @turboops/cli
1837
- 4. Token setzen: turbo config set token \${TURBOOPS_TOKEN}
1838
- 5. Deploy ausf\xFChren: turbo deploy <environment> --image <image-tag> --wait
2162
+ Erstelle eine Standard-Pipeline mit:
2163
+ 1. Build-Stage: Docker Image bauen und pushen
2164
+ 2. Deploy-Stages f\xFCr jede Umgebung (dev, test, prod)
1839
2165
 
1840
- Die erste Stage braucht --image, weitere Stages k\xF6nnen ohne --image deployen (Promotion).
2166
+ Jede Stage deployed nur auf ihrem Branch (dev->dev, test->test, main->prod).
1841
2167
 
1842
- F\xFCge am Ende einen Kommentar hinzu welche Secrets ben\xF6tigt werden:
1843
- - TURBOOPS_TOKEN: TurboOps Projekt-Token
2168
+ Verwende:
2169
+ - Image: docker:24-dind f\xFCr Build
2170
+ - Image: node:20-alpine f\xFCr Deploy
2171
+ - Registry: registry.turbo-ops.de/${projectSlug}
2172
+ - TurboOps CLI: npm install -g @turboops/cli
1844
2173
 
1845
- Modifiziere die Datei "${pipelineFile}" entsprechend.`;
1846
- const success = await aiToolsService.runWithPrompt(tool, prompt);
2174
+ Befehle:
2175
+ - turbo config set token \${TURBOOPS_TOKEN}
2176
+ - turbo deploy <stage-slug> --image <image-tag> --wait
2177
+
2178
+ Secrets ben\xF6tigt: TURBOOPS_TOKEN
2179
+
2180
+ Erstelle die Datei "${pipelineFile}".`;
2181
+ const success = await aiToolsService.runWithPrompt(tool, prompt, verbose);
1847
2182
  if (success) {
1848
2183
  logger.success("Pipeline wurde mit AI aktualisiert!");
1849
- logger.newline();
1850
- logger.info("Vergessen Sie nicht, das CI/CD Secret TURBOOPS_TOKEN zu konfigurieren.");
1851
- }
1852
- }
1853
-
1854
- // src/commands/logs.ts
1855
- import { Command as Command4 } from "commander";
1856
- import chalk5 from "chalk";
1857
- import { io } from "socket.io-client";
1858
- var logsCommand = new Command4("logs").description("View deployment logs").argument("<environment>", "Environment slug").option("-n, --lines <number>", "Number of lines to show", "100").option("-f, --follow", "Follow log output (stream)").option("--service <name>", "Filter logs by service name").action(async (environment, options) => {
1859
- const { environment: env } = await getCommandContextWithEnvironment(environment);
1860
- logger.header(`Logs: ${env.name}`);
1861
- if (options.follow) {
1862
- if (isJsonMode()) {
1863
- logger.error("JSON output mode is not supported with --follow flag");
1864
- process.exit(14 /* VALIDATION_ERROR */);
1865
- }
1866
- await streamLogs(env.id, options.service);
1867
- } else {
1868
- await fetchHistoricalLogs(
1869
- env.id,
1870
- parseInt(options.lines),
1871
- options.service
1872
- );
1873
- }
1874
- });
1875
- async function fetchHistoricalLogs(environmentId, lines, service) {
1876
- logger.info("Fetching logs...");
1877
- const { data, error } = await apiClient.request(
1878
- "GET",
1879
- `/deployment/environments/${environmentId}/logs?lines=${lines}${service ? `&service=${service}` : ""}`
1880
- );
1881
- if (error) {
1882
- logger.error(`Failed to fetch logs: ${error}`);
1883
- process.exit(13 /* API_ERROR */);
1884
- }
1885
- if (!data?.logs || data.logs.length === 0) {
1886
- logger.info("No logs available.");
1887
- addJsonData({ logs: [] });
1888
- return;
1889
- }
1890
- addJsonData({
1891
- logs: data.logs.map((log) => ({
1892
- timestamp: log.timestamp,
1893
- level: log.level,
1894
- message: log.message,
1895
- step: log.step
1896
- })),
1897
- count: data.logs.length
1898
- });
1899
- logger.newline();
1900
- for (const log of data.logs) {
1901
- printLogEntry(log);
1902
- }
1903
- logger.newline();
1904
- logger.info(`Showing last ${lines} lines. Use -f to follow live logs.`);
1905
- }
1906
- async function streamLogs(environmentId, service) {
1907
- const apiUrl = configService.getApiUrl();
1908
- const token = configService.getToken();
1909
- if (!token) {
1910
- logger.error("Not authenticated");
1911
- process.exit(10 /* AUTH_ERROR */);
1912
- }
1913
- const wsUrl = buildWebSocketUrl(apiUrl);
1914
- logger.info("Connecting to log stream...");
1915
- const socket = io(`${wsUrl}/deployments`, {
1916
- auth: { token },
1917
- reconnection: true,
1918
- reconnectionAttempts: 5,
1919
- reconnectionDelay: 1e3
1920
- });
1921
- socket.on("connect", () => {
1922
- logger.success("Connected to log stream");
1923
- logger.info("Streaming logs... (Press Ctrl+C to stop)");
1924
- logger.newline();
1925
- socket.emit("join:logs", {
1926
- environmentId,
1927
- service
1928
- });
1929
- });
1930
- socket.on("log", (entry) => {
1931
- printLogEntry(entry);
1932
- });
1933
- socket.on("logs:batch", (entries) => {
1934
- for (const entry of entries) {
1935
- printLogEntry(entry);
1936
- }
1937
- });
1938
- socket.on("error", (error) => {
1939
- logger.error(`Stream error: ${error.message}`);
1940
- });
1941
- socket.on("connect_error", (error) => {
1942
- logger.error(`Connection error: ${error.message}`);
1943
- process.exit(15 /* NETWORK_ERROR */);
1944
- });
1945
- socket.on("disconnect", (reason) => {
1946
- if (reason === "io server disconnect") {
1947
- logger.warning("Disconnected by server");
1948
- } else {
1949
- logger.warning(`Disconnected: ${reason}`);
2184
+ if (projectId) {
2185
+ await apiClient.updateProjectConfig(projectId, {
2186
+ pipelineConfig: {
2187
+ hasPipeline: true,
2188
+ pipelineType
2189
+ }
2190
+ });
1950
2191
  }
1951
- });
1952
- socket.on("reconnect", () => {
1953
- logger.info("Reconnected to log stream");
1954
- socket.emit("join:logs", { environmentId, service });
1955
- });
1956
- process.on("SIGINT", () => {
1957
2192
  logger.newline();
1958
- logger.info("Stopping log stream...");
1959
- socket.emit("leave:logs", { environmentId });
1960
- socket.disconnect();
1961
- process.exit(0 /* SUCCESS */);
1962
- });
1963
- await new Promise(() => {
1964
- });
1965
- }
1966
- function buildWebSocketUrl(apiUrl) {
1967
- try {
1968
- const url = new URL(apiUrl);
1969
- const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
1970
- return `${wsProtocol}//${url.host}`;
1971
- } catch {
1972
- return apiUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:").replace(/\/api\/?$/, "");
1973
- }
1974
- }
1975
- function printLogEntry(log) {
1976
- const timestamp = formatTimestamp(log.timestamp);
1977
- const levelColor = getLevelColor(log.level);
1978
- const levelStr = log.level.toUpperCase().padEnd(5);
1979
- let output = chalk5.gray(`[${timestamp}]`) + levelColor(` ${levelStr}`);
1980
- if (log.step) {
1981
- output += chalk5.cyan(` [${log.step}]`);
1982
- }
1983
- output += ` ${log.message}`;
1984
- logger.raw(output);
1985
- }
1986
- function formatTimestamp(timestamp) {
1987
- try {
1988
- const date = new Date(timestamp);
1989
- return date.toISOString().replace("T", " ").slice(0, 19);
1990
- } catch {
1991
- return timestamp;
1992
- }
1993
- }
1994
- function getLevelColor(level) {
1995
- switch (level.toLowerCase()) {
1996
- case "error":
1997
- case "fatal":
1998
- return chalk5.red;
1999
- case "warn":
2000
- case "warning":
2001
- return chalk5.yellow;
2002
- case "info":
2003
- return chalk5.blue;
2004
- case "debug":
2005
- return chalk5.gray;
2006
- case "trace":
2007
- return chalk5.magenta;
2008
- default:
2009
- return chalk5.white;
2193
+ logger.info("Vergessen Sie nicht, das CI/CD Secret TURBOOPS_TOKEN zu konfigurieren.");
2010
2194
  }
2011
2195
  }
2012
2196
 
2013
2197
  // src/commands/deploy.ts
2014
- import { Command as Command5 } from "commander";
2198
+ import { Command as Command4 } from "commander";
2015
2199
  import chalk6 from "chalk";
2016
- var deployCommand = new Command5("deploy").description("Trigger a deployment (for CI/CD pipelines)").argument("<environment>", "Environment slug (e.g., production, staging)").option("-i, --image <tag>", "Docker image tag to deploy").option("-w, --wait", "Wait for deployment to complete", true).option("--no-wait", "Do not wait for deployment to complete").option(
2200
+ var deployCommand = new Command4("deploy").description("Trigger a deployment (for CI/CD pipelines)").argument("<environment>", "Environment slug (e.g., production, staging)").option("-i, --image <tag>", "Docker image tag to deploy").option("-w, --wait", "Wait for deployment to complete", true).option("--no-wait", "Do not wait for deployment to complete").option(
2017
2201
  "--timeout <ms>",
2018
2202
  "Timeout in milliseconds when waiting",
2019
2203
  "600000"
@@ -2117,12 +2301,12 @@ function getStatusColor2(status) {
2117
2301
  }
2118
2302
 
2119
2303
  // src/commands/pipeline.ts
2120
- import { Command as Command6 } from "commander";
2304
+ import { Command as Command5 } from "commander";
2121
2305
  import prompts3 from "prompts";
2122
2306
  import chalk7 from "chalk";
2123
2307
  import * as fs3 from "fs/promises";
2124
2308
  import * as path3 from "path";
2125
- var pipelineCommand = new Command6("pipeline").description("Manage CI/CD pipeline configuration");
2309
+ var pipelineCommand = new Command5("pipeline").description("Manage CI/CD pipeline configuration");
2126
2310
  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) => {
2127
2311
  const { project } = await getCommandContext();
2128
2312
  logger.header("CI/CD Pipeline generieren");
@@ -2355,7 +2539,7 @@ Modifiziere die Datei "${pipelinePath}" entsprechend.`;
2355
2539
  }
2356
2540
 
2357
2541
  // src/commands/docker.ts
2358
- import { Command as Command7 } from "commander";
2542
+ import { Command as Command6 } from "commander";
2359
2543
  import prompts4 from "prompts";
2360
2544
  var DOCKER_SETUP_PROMPT = `Analysiere dieses Projekt und erstelle ein vollst\xE4ndiges Docker-Setup f\xFCr Production Deployment.
2361
2545
 
@@ -2397,7 +2581,7 @@ services:
2397
2581
  \`\`\`
2398
2582
 
2399
2583
  Erstelle alle notwendigen Dateien.`;
2400
- var dockerCommand = new Command7("docker").description("Manage Docker configuration");
2584
+ var dockerCommand = new Command6("docker").description("Manage Docker configuration");
2401
2585
  dockerCommand.command("generate").description("Docker-Setup (docker-compose + Dockerfiles) mit AI erstellen").option("-f, --force", "Bestehende Dateien \xFCberschreiben").action(async (options) => {
2402
2586
  logger.header("Docker-Setup generieren");
2403
2587
  const detection = await detectProjectConfig();
@@ -2440,7 +2624,7 @@ dockerCommand.command("generate").description("Docker-Setup (docker-compose + Do
2440
2624
  // src/index.ts
2441
2625
  var VERSION = getCurrentVersion();
2442
2626
  var shouldCheckUpdate = true;
2443
- var program = new Command8();
2627
+ var program = new Command7();
2444
2628
  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");
2445
2629
  program.hook("preAction", (thisCommand) => {
2446
2630
  const opts = thisCommand.opts();
@@ -2476,7 +2660,6 @@ program.addCommand(whoamiCommand);
2476
2660
  program.addCommand(initCommand);
2477
2661
  program.addCommand(statusCommand);
2478
2662
  program.addCommand(configCommand);
2479
- program.addCommand(logsCommand);
2480
2663
  program.addCommand(deployCommand);
2481
2664
  program.addCommand(pipelineCommand);
2482
2665
  program.addCommand(dockerCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turboops/cli",
3
- "version": "1.0.0-dev.588",
3
+ "version": "1.0.0-dev.590",
4
4
  "description": "TurboCLI - Command line interface for TurboOps deployments",
5
5
  "author": "lenne.tech GmbH",
6
6
  "license": "MIT",
@@ -14,7 +14,10 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "tsup src/index.ts --format esm --dts --clean",
17
- "dev": "tsup src/index.ts --format esm --watch",
17
+ "build:local": "tsup src/index.ts --format esm --dts --clean --define.__TURBOOPS_ENV__=\"'local'\"",
18
+ "build:dev": "tsup src/index.ts --format esm --dts --clean --define.__TURBOOPS_ENV__=\"'dev'\"",
19
+ "build:prod": "tsup src/index.ts --format esm --dts --clean --define.__TURBOOPS_ENV__=\"'prod'\"",
20
+ "dev": "tsup src/index.ts --format esm --watch --define.__TURBOOPS_ENV__=\"'local'\"",
18
21
  "start": "node dist/index.js",
19
22
  "lint": "oxlint --fix -c oxlint.json",
20
23
  "typecheck": "tsc --noEmit",