@turboops/cli 1.0.0-dev.587 → 1.0.0-dev.589
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 +564 -381
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
264
|
+
function getVersionBasedEnvironment() {
|
|
262
265
|
const version = getCurrentVersion();
|
|
263
266
|
const isDevVersion = version.includes("-dev");
|
|
264
|
-
return isDevVersion ?
|
|
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
|
|
287
|
+
* Returns the appropriate frontend URL based on environment
|
|
279
288
|
*/
|
|
280
289
|
getAppUrl() {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
1594
|
+
await createDefaultStages(project.id, project.slug);
|
|
1448
1595
|
}
|
|
1449
1596
|
}
|
|
1450
|
-
|
|
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: "
|
|
1674
|
+
message: "Standard-Stages erstellen (dev, test, prod)?",
|
|
1549
1675
|
name: "shouldCreateStage",
|
|
1550
1676
|
type: "confirm"
|
|
1551
1677
|
});
|
|
1552
1678
|
if (shouldCreateStage) {
|
|
1553
|
-
await
|
|
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
|
-
|
|
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("
|
|
1570
|
-
const {
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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.
|
|
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 ?
|
|
1695
|
-
console.log(` ${
|
|
1696
|
-
console.log(` ${
|
|
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(` ${
|
|
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
|
-
"
|
|
1742
|
-
"
|
|
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
|
-
|
|
1748
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
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
|
-
|
|
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: "
|
|
1767
|
-
message:
|
|
1952
|
+
name: "shouldCreatePipeline",
|
|
1953
|
+
message: "Pipeline erstellen?",
|
|
1768
1954
|
initial: true
|
|
1769
1955
|
});
|
|
1770
|
-
if (
|
|
1771
|
-
await
|
|
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
|
|
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
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
1818
|
-
|
|
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 ? "
|
|
2112
|
+
const pipelineType = detection.hasGitLabPipeline ? "gitlab" : "github";
|
|
1827
2113
|
const pipelineFile = detection.pipelinePath;
|
|
1828
|
-
|
|
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
|
-
|
|
1834
|
-
1.
|
|
1835
|
-
2.
|
|
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
|
-
|
|
2166
|
+
Jede Stage deployed nur auf ihrem Branch (dev->dev, test->test, main->prod).
|
|
1841
2167
|
|
|
1842
|
-
|
|
1843
|
-
-
|
|
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
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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("
|
|
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
|
|
2198
|
+
import { Command as Command4 } from "commander";
|
|
2015
2199
|
import chalk6 from "chalk";
|
|
2016
|
-
var deployCommand = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.0.0-dev.589",
|
|
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
|
-
"
|
|
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",
|