@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.
- package/dist/index.js +530 -55
- 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
|
|
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
|
|
139
|
-
if (existsSync(
|
|
140
|
-
return require2(
|
|
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,
|
|
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}${
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1272
|
-
const
|
|
1273
|
-
const gitlabCiPath =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1492
|
-
const
|
|
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 =
|
|
1496
|
-
await
|
|
1497
|
-
filePath =
|
|
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 =
|
|
1676
|
+
filePath = path4.join(process.cwd(), ".gitlab-ci.yml");
|
|
1500
1677
|
}
|
|
1501
1678
|
try {
|
|
1502
|
-
await
|
|
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
|
|
2113
|
+
import prompts3 from "prompts";
|
|
1829
2114
|
import chalk7 from "chalk";
|
|
1830
|
-
import * as
|
|
1831
|
-
import * as
|
|
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
|
|
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 =
|
|
2162
|
+
outputPath = path3.resolve(options.output);
|
|
1862
2163
|
} else if (pipelineType === "github") {
|
|
1863
|
-
outputPath =
|
|
2164
|
+
outputPath = path3.join(process.cwd(), ".github", "workflows", "deploy.yml");
|
|
1864
2165
|
} else {
|
|
1865
|
-
outputPath =
|
|
2166
|
+
outputPath = path3.join(process.cwd(), ".gitlab-ci.yml");
|
|
1866
2167
|
}
|
|
1867
2168
|
if (!options.force) {
|
|
1868
2169
|
try {
|
|
1869
|
-
await
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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 =
|
|
1894
|
-
await
|
|
2218
|
+
const outputDir = path3.dirname(outputPath);
|
|
2219
|
+
await fs3.mkdir(outputDir, { recursive: true });
|
|
1895
2220
|
try {
|
|
1896
|
-
await
|
|
1897
|
-
logger.success(`${
|
|
2221
|
+
await fs3.writeFile(outputPath, pipeline.content, "utf-8");
|
|
2222
|
+
logger.success(`${path3.relative(process.cwd(), outputPath)} wurde erstellt!`);
|
|
1898
2223
|
addJsonData({
|
|
1899
|
-
filename:
|
|
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
|
|
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 {
|