@turboops/cli 1.0.0-dev.580 → 1.0.0-dev.583
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/README.md +84 -66
- package/dist/index.js +800 -436
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import chalk8 from "chalk";
|
|
|
6
6
|
|
|
7
7
|
// src/services/config.ts
|
|
8
8
|
import Conf from "conf";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
9
11
|
|
|
10
12
|
// src/utils/logger.ts
|
|
11
13
|
import chalk from "chalk";
|
|
@@ -133,9 +135,9 @@ function loadPackageJson() {
|
|
|
133
135
|
join(__dirname, "../package.json")
|
|
134
136
|
// From dist/
|
|
135
137
|
];
|
|
136
|
-
for (const
|
|
137
|
-
if (existsSync(
|
|
138
|
-
return require2(
|
|
138
|
+
for (const path3 of paths) {
|
|
139
|
+
if (existsSync(path3)) {
|
|
140
|
+
return require2(path3);
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
return { version: "0.0.0", name: "@turboops/cli" };
|
|
@@ -200,41 +202,76 @@ var APP_URLS = {
|
|
|
200
202
|
production: "https://turbo-ops.de",
|
|
201
203
|
dev: "https://dev.turbo-ops.de"
|
|
202
204
|
};
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const isDevVersion = version.includes("-dev");
|
|
206
|
-
return isDevVersion ? API_URLS.dev : API_URLS.production;
|
|
207
|
-
}
|
|
208
|
-
var defaults = {
|
|
209
|
-
apiUrl: API_URLS.production,
|
|
205
|
+
var LOCAL_CONFIG_FILE = ".turboops.json";
|
|
206
|
+
var globalDefaults = {
|
|
210
207
|
token: null,
|
|
211
|
-
project: null,
|
|
212
208
|
userId: null
|
|
213
209
|
};
|
|
214
|
-
var
|
|
210
|
+
var globalConfig = new Conf({
|
|
215
211
|
projectName: "turboops-cli",
|
|
216
|
-
defaults
|
|
212
|
+
defaults: globalDefaults
|
|
217
213
|
});
|
|
214
|
+
function findProjectRoot() {
|
|
215
|
+
let currentDir = process.cwd();
|
|
216
|
+
const root = path.parse(currentDir).root;
|
|
217
|
+
while (currentDir !== root) {
|
|
218
|
+
if (fs.existsSync(path.join(currentDir, LOCAL_CONFIG_FILE))) {
|
|
219
|
+
return currentDir;
|
|
220
|
+
}
|
|
221
|
+
if (fs.existsSync(path.join(currentDir, ".git"))) {
|
|
222
|
+
return currentDir;
|
|
223
|
+
}
|
|
224
|
+
if (fs.existsSync(path.join(currentDir, "package.json"))) {
|
|
225
|
+
return currentDir;
|
|
226
|
+
}
|
|
227
|
+
currentDir = path.dirname(currentDir);
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function getLocalConfigPath() {
|
|
232
|
+
const projectRoot = findProjectRoot();
|
|
233
|
+
if (!projectRoot) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return path.join(projectRoot, LOCAL_CONFIG_FILE);
|
|
237
|
+
}
|
|
238
|
+
function readLocalConfig() {
|
|
239
|
+
const configPath = getLocalConfigPath();
|
|
240
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
245
|
+
return JSON.parse(content);
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function writeLocalConfig(config) {
|
|
251
|
+
const projectRoot = findProjectRoot() || process.cwd();
|
|
252
|
+
const configPath = path.join(projectRoot, LOCAL_CONFIG_FILE);
|
|
253
|
+
try {
|
|
254
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
255
|
+
return true;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
logger.error(`Failed to write ${LOCAL_CONFIG_FILE}: ${error}`);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function getApiUrl() {
|
|
262
|
+
const version = getCurrentVersion();
|
|
263
|
+
const isDevVersion = version.includes("-dev");
|
|
264
|
+
return isDevVersion ? API_URLS.dev : API_URLS.production;
|
|
265
|
+
}
|
|
218
266
|
var configService = {
|
|
219
267
|
/**
|
|
220
|
-
* Get API URL
|
|
221
|
-
* Priority: ENV variable > stored config > version-based default
|
|
268
|
+
* Get API URL (fixed based on CLI version)
|
|
222
269
|
*/
|
|
223
270
|
getApiUrl() {
|
|
224
271
|
if (process.env.TURBOOPS_API_URL) {
|
|
225
272
|
return process.env.TURBOOPS_API_URL;
|
|
226
273
|
}
|
|
227
|
-
|
|
228
|
-
if (storedUrl === "https://api.turboops.io" || storedUrl === API_URLS.production) {
|
|
229
|
-
return getDefaultApiUrl();
|
|
230
|
-
}
|
|
231
|
-
return storedUrl;
|
|
232
|
-
},
|
|
233
|
-
/**
|
|
234
|
-
* Set API URL
|
|
235
|
-
*/
|
|
236
|
-
setApiUrl(url) {
|
|
237
|
-
config.set("apiUrl", url);
|
|
274
|
+
return getApiUrl();
|
|
238
275
|
},
|
|
239
276
|
/**
|
|
240
277
|
* Get App URL (frontend)
|
|
@@ -246,53 +283,65 @@ var configService = {
|
|
|
246
283
|
return isDevVersion ? APP_URLS.dev : APP_URLS.production;
|
|
247
284
|
},
|
|
248
285
|
/**
|
|
249
|
-
* Get authentication token
|
|
286
|
+
* Get authentication token (global - user session)
|
|
250
287
|
*/
|
|
251
288
|
getToken() {
|
|
252
|
-
return process.env.TURBOOPS_TOKEN ||
|
|
289
|
+
return process.env.TURBOOPS_TOKEN || globalConfig.get("token");
|
|
253
290
|
},
|
|
254
291
|
/**
|
|
255
|
-
* Set authentication token
|
|
292
|
+
* Set authentication token (global - user session)
|
|
256
293
|
*/
|
|
257
294
|
setToken(token) {
|
|
258
|
-
|
|
295
|
+
globalConfig.set("token", token);
|
|
259
296
|
},
|
|
260
297
|
/**
|
|
261
298
|
* Clear authentication token
|
|
262
299
|
*/
|
|
263
300
|
clearToken() {
|
|
264
|
-
|
|
265
|
-
|
|
301
|
+
globalConfig.set("token", null);
|
|
302
|
+
globalConfig.set("userId", null);
|
|
266
303
|
},
|
|
267
304
|
/**
|
|
268
|
-
* Get current project
|
|
305
|
+
* Get current project (local - project specific)
|
|
269
306
|
*/
|
|
270
307
|
getProject() {
|
|
271
|
-
|
|
308
|
+
if (process.env.TURBOOPS_PROJECT) {
|
|
309
|
+
return process.env.TURBOOPS_PROJECT;
|
|
310
|
+
}
|
|
311
|
+
const localConfig = readLocalConfig();
|
|
312
|
+
return localConfig?.project || null;
|
|
272
313
|
},
|
|
273
314
|
/**
|
|
274
|
-
* Set current project
|
|
315
|
+
* Set current project (local - writes to .turboops.json)
|
|
275
316
|
*/
|
|
276
317
|
setProject(project) {
|
|
277
|
-
|
|
318
|
+
const localConfig = readLocalConfig() || { project: "" };
|
|
319
|
+
localConfig.project = project;
|
|
320
|
+
writeLocalConfig(localConfig);
|
|
278
321
|
},
|
|
279
322
|
/**
|
|
280
323
|
* Clear current project
|
|
281
324
|
*/
|
|
282
325
|
clearProject() {
|
|
283
|
-
|
|
326
|
+
const configPath = getLocalConfigPath();
|
|
327
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
328
|
+
try {
|
|
329
|
+
fs.unlinkSync(configPath);
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
284
333
|
},
|
|
285
334
|
/**
|
|
286
|
-
* Get user ID
|
|
335
|
+
* Get user ID (global - user session)
|
|
287
336
|
*/
|
|
288
337
|
getUserId() {
|
|
289
|
-
return
|
|
338
|
+
return globalConfig.get("userId");
|
|
290
339
|
},
|
|
291
340
|
/**
|
|
292
|
-
* Set user ID
|
|
341
|
+
* Set user ID (global - user session)
|
|
293
342
|
*/
|
|
294
343
|
setUserId(userId) {
|
|
295
|
-
|
|
344
|
+
globalConfig.set("userId", userId);
|
|
296
345
|
},
|
|
297
346
|
/**
|
|
298
347
|
* Check if user is authenticated
|
|
@@ -306,6 +355,19 @@ var configService = {
|
|
|
306
355
|
hasProject() {
|
|
307
356
|
return !!this.getProject();
|
|
308
357
|
},
|
|
358
|
+
/**
|
|
359
|
+
* Check if local config exists
|
|
360
|
+
*/
|
|
361
|
+
hasLocalConfig() {
|
|
362
|
+
const configPath = getLocalConfigPath();
|
|
363
|
+
return !!configPath && fs.existsSync(configPath);
|
|
364
|
+
},
|
|
365
|
+
/**
|
|
366
|
+
* Get local config file path (for display purposes)
|
|
367
|
+
*/
|
|
368
|
+
getLocalConfigPath() {
|
|
369
|
+
return getLocalConfigPath();
|
|
370
|
+
},
|
|
309
371
|
/**
|
|
310
372
|
* Get all configuration
|
|
311
373
|
*/
|
|
@@ -318,17 +380,25 @@ var configService = {
|
|
|
318
380
|
};
|
|
319
381
|
},
|
|
320
382
|
/**
|
|
321
|
-
* Clear all configuration
|
|
383
|
+
* Clear all configuration (both global and local)
|
|
322
384
|
*/
|
|
323
385
|
clearAll() {
|
|
324
|
-
|
|
386
|
+
globalConfig.clear();
|
|
387
|
+
this.clearProject();
|
|
325
388
|
},
|
|
326
389
|
/**
|
|
327
|
-
* Check if using a project token (turbo_xxx format)
|
|
390
|
+
* Check if using a project token (turbo_xxx format, but not turbo_cli_xxx)
|
|
328
391
|
*/
|
|
329
392
|
isProjectToken() {
|
|
330
393
|
const token = this.getToken();
|
|
331
|
-
return !!token && token.startsWith("turbo_");
|
|
394
|
+
return !!token && token.startsWith("turbo_") && !token.startsWith("turbo_cli_");
|
|
395
|
+
},
|
|
396
|
+
/**
|
|
397
|
+
* Check if using a CLI session token (turbo_cli_xxx format)
|
|
398
|
+
*/
|
|
399
|
+
isCliSessionToken() {
|
|
400
|
+
const token = this.getToken();
|
|
401
|
+
return !!token && token.startsWith("turbo_cli_");
|
|
332
402
|
},
|
|
333
403
|
/**
|
|
334
404
|
* Show current configuration
|
|
@@ -336,12 +406,15 @@ var configService = {
|
|
|
336
406
|
show() {
|
|
337
407
|
const data = this.getAll();
|
|
338
408
|
const isProjectToken2 = this.isProjectToken();
|
|
409
|
+
const isCliToken = this.isCliSessionToken();
|
|
410
|
+
const localConfigPath = this.getLocalConfigPath();
|
|
339
411
|
logger.header("Configuration");
|
|
340
412
|
logger.table({
|
|
341
413
|
"API URL": data.apiUrl,
|
|
342
414
|
Token: data.token || "Not set",
|
|
343
|
-
"Token Type": isProjectToken2 ? "Project Token" : data.token ? "User Token" : "N/A",
|
|
415
|
+
"Token Type": isProjectToken2 ? "Project Token (CI/CD)" : isCliToken ? "CLI Session Token" : data.token ? "User Token" : "N/A",
|
|
344
416
|
Project: data.project || (isProjectToken2 ? "(from token)" : "Not set"),
|
|
417
|
+
"Project Config": localConfigPath || "(not found)",
|
|
345
418
|
"User ID": data.userId || (isProjectToken2 ? "(N/A for project token)" : "Not set")
|
|
346
419
|
});
|
|
347
420
|
}
|
|
@@ -352,14 +425,14 @@ import { Command } from "commander";
|
|
|
352
425
|
|
|
353
426
|
// src/services/api.ts
|
|
354
427
|
function isProjectToken(token) {
|
|
355
|
-
return token.startsWith("turbo_");
|
|
428
|
+
return token.startsWith("turbo_") && !token.startsWith("turbo_cli_");
|
|
356
429
|
}
|
|
357
430
|
var cachedProjectInfo = null;
|
|
358
431
|
var apiClient = {
|
|
359
432
|
/**
|
|
360
433
|
* Make an API request
|
|
361
434
|
*/
|
|
362
|
-
async request(method,
|
|
435
|
+
async request(method, path3, body) {
|
|
363
436
|
const apiUrl = configService.getApiUrl();
|
|
364
437
|
const token = configService.getToken();
|
|
365
438
|
if (!token) {
|
|
@@ -368,7 +441,7 @@ var apiClient = {
|
|
|
368
441
|
status: 401
|
|
369
442
|
};
|
|
370
443
|
}
|
|
371
|
-
const url = `${apiUrl}${
|
|
444
|
+
const url = `${apiUrl}${path3}`;
|
|
372
445
|
const headers = {
|
|
373
446
|
"Content-Type": "application/json",
|
|
374
447
|
Authorization: `Bearer ${token}`
|
|
@@ -513,136 +586,106 @@ var apiClient = {
|
|
|
513
586
|
return this.request("GET", `/deployment/projects/by-slug/${slug}`);
|
|
514
587
|
},
|
|
515
588
|
/**
|
|
516
|
-
*
|
|
517
|
-
*/
|
|
518
|
-
async getEnvironments(projectId) {
|
|
519
|
-
return this.request(
|
|
520
|
-
"GET",
|
|
521
|
-
`/deployment/environments?projectId=${projectId}`
|
|
522
|
-
);
|
|
523
|
-
},
|
|
524
|
-
/**
|
|
525
|
-
* Get environment by slug
|
|
526
|
-
*/
|
|
527
|
-
async getEnvironmentBySlug(projectId, slug) {
|
|
528
|
-
return this.request(
|
|
529
|
-
"GET",
|
|
530
|
-
`/deployment/environments/by-slug/${projectId}/${slug}`
|
|
531
|
-
);
|
|
532
|
-
},
|
|
533
|
-
/**
|
|
534
|
-
* Trigger deployment
|
|
589
|
+
* Create a new project
|
|
535
590
|
*/
|
|
536
|
-
async
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
591
|
+
async createProject(data) {
|
|
592
|
+
const payload = {
|
|
593
|
+
name: data.name,
|
|
594
|
+
slug: data.slug
|
|
595
|
+
};
|
|
596
|
+
if (data.customer) payload.customer = data.customer;
|
|
597
|
+
if (data.description) payload.description = data.description;
|
|
598
|
+
if (data.repositoryUrl) payload.repositoryUrl = data.repositoryUrl;
|
|
599
|
+
return this.request("POST", "/deployment/projects/simple", payload);
|
|
541
600
|
},
|
|
542
601
|
/**
|
|
543
|
-
* Get
|
|
602
|
+
* Get all customers
|
|
544
603
|
*/
|
|
545
|
-
async
|
|
546
|
-
return this.request("GET",
|
|
604
|
+
async getCustomers() {
|
|
605
|
+
return this.request("GET", "/customer");
|
|
547
606
|
},
|
|
548
607
|
/**
|
|
549
|
-
*
|
|
608
|
+
* Get environments (stages) for project
|
|
550
609
|
*/
|
|
551
|
-
async
|
|
552
|
-
return this.request(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
610
|
+
async getEnvironments(projectId) {
|
|
611
|
+
return this.request(
|
|
612
|
+
"GET",
|
|
613
|
+
`/deployment/stages?projectId=${projectId}`
|
|
614
|
+
);
|
|
556
615
|
},
|
|
557
616
|
/**
|
|
558
|
-
*
|
|
617
|
+
* Get environment (stage) by slug
|
|
559
618
|
*/
|
|
560
|
-
async
|
|
619
|
+
async getEnvironmentBySlug(projectId, slug) {
|
|
561
620
|
return this.request(
|
|
562
|
-
"
|
|
563
|
-
`/deployment/
|
|
621
|
+
"GET",
|
|
622
|
+
`/deployment/stages/by-slug/${projectId}/${slug}`
|
|
564
623
|
);
|
|
565
624
|
},
|
|
566
625
|
/**
|
|
567
|
-
*
|
|
626
|
+
* Create a new stage
|
|
568
627
|
*/
|
|
569
|
-
async
|
|
570
|
-
return this.request(
|
|
571
|
-
"POST",
|
|
572
|
-
`/deployment/environments/${environmentId}/stop`
|
|
573
|
-
);
|
|
628
|
+
async createStage(data) {
|
|
629
|
+
return this.request("POST", "/deployment/stages/simple", data);
|
|
574
630
|
},
|
|
575
631
|
/**
|
|
576
|
-
*
|
|
632
|
+
* Get deployment-ready servers
|
|
577
633
|
*/
|
|
578
|
-
async
|
|
579
|
-
return this.request(
|
|
580
|
-
"POST",
|
|
581
|
-
`/deployment/environments/${environmentId}/wake`
|
|
582
|
-
);
|
|
634
|
+
async getDeploymentServers() {
|
|
635
|
+
return this.request("GET", "/server?deploymentReady=true");
|
|
583
636
|
},
|
|
584
637
|
/**
|
|
585
|
-
*
|
|
638
|
+
* Generate CI/CD pipeline configuration
|
|
586
639
|
*/
|
|
587
|
-
async
|
|
588
|
-
return this.request("GET", `/deployment/
|
|
640
|
+
async generatePipeline(projectId, type) {
|
|
641
|
+
return this.request("GET", `/deployment/projects/${projectId}/pipeline/${type}`);
|
|
589
642
|
},
|
|
590
643
|
/**
|
|
591
|
-
* Get
|
|
644
|
+
* Get required secrets for pipeline
|
|
592
645
|
*/
|
|
593
|
-
async
|
|
594
|
-
return this.request(
|
|
595
|
-
"GET",
|
|
596
|
-
`/deployment/environments/${environmentId}/env-vars`
|
|
597
|
-
);
|
|
646
|
+
async getPipelineSecrets(projectId, type) {
|
|
647
|
+
return this.request("GET", `/deployment/projects/${projectId}/pipeline/${type}/secrets`);
|
|
598
648
|
},
|
|
599
649
|
/**
|
|
600
|
-
*
|
|
650
|
+
* Trigger a deployment
|
|
601
651
|
*/
|
|
602
|
-
async
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
{
|
|
607
|
-
key,
|
|
608
|
-
value,
|
|
609
|
-
secret
|
|
610
|
-
}
|
|
611
|
-
);
|
|
652
|
+
async deploy(stageId, imageTag) {
|
|
653
|
+
const body = { stage: stageId };
|
|
654
|
+
if (imageTag) body.imageTag = imageTag;
|
|
655
|
+
return this.request("POST", "/deployment/deployments", body);
|
|
612
656
|
},
|
|
613
657
|
/**
|
|
614
|
-
*
|
|
658
|
+
* Get deployment status
|
|
615
659
|
*/
|
|
616
|
-
async
|
|
617
|
-
return this.request(
|
|
618
|
-
"DELETE",
|
|
619
|
-
`/deployment/environments/${environmentId}/env-vars/${key}`
|
|
620
|
-
);
|
|
660
|
+
async getDeploymentStatus(deploymentId) {
|
|
661
|
+
return this.request("GET", `/deployment/deployments/${deploymentId}`);
|
|
621
662
|
},
|
|
622
663
|
/**
|
|
623
664
|
* Wait for deployment to complete
|
|
624
665
|
*/
|
|
625
|
-
async waitForDeployment(deploymentId, options) {
|
|
626
|
-
const timeout =
|
|
627
|
-
const pollInterval = options?.pollInterval || 3e3;
|
|
666
|
+
async waitForDeployment(deploymentId, options = {}) {
|
|
667
|
+
const { timeout = 6e5, pollInterval = 3e3, onProgress } = options;
|
|
628
668
|
const startTime = Date.now();
|
|
629
|
-
while (
|
|
669
|
+
while (true) {
|
|
630
670
|
const { data, error } = await this.getDeploymentStatus(deploymentId);
|
|
631
671
|
if (error) {
|
|
632
|
-
throw new Error(error);
|
|
672
|
+
throw new Error(`Failed to get deployment status: ${error}`);
|
|
633
673
|
}
|
|
634
674
|
if (!data) {
|
|
635
675
|
throw new Error("No deployment data received");
|
|
636
676
|
}
|
|
637
|
-
if (
|
|
638
|
-
|
|
677
|
+
if (onProgress) {
|
|
678
|
+
onProgress(data);
|
|
639
679
|
}
|
|
640
|
-
|
|
680
|
+
const terminalStatuses = ["running", "failed", "stopped", "rolled_back"];
|
|
681
|
+
if (terminalStatuses.includes(data.status)) {
|
|
641
682
|
return data;
|
|
642
683
|
}
|
|
643
|
-
|
|
684
|
+
if (Date.now() - startTime > timeout) {
|
|
685
|
+
throw new Error(`Deployment timeout after ${timeout / 1e3} seconds`);
|
|
686
|
+
}
|
|
687
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
644
688
|
}
|
|
645
|
-
throw new Error(`Deployment timeout after ${timeout / 1e3} seconds`);
|
|
646
689
|
}
|
|
647
690
|
};
|
|
648
691
|
|
|
@@ -758,7 +801,7 @@ var authService = {
|
|
|
758
801
|
if (result.status === "expired" || result.status === "consumed") {
|
|
759
802
|
return { error: "Autorisierungscode abgelaufen oder bereits verwendet", success: false };
|
|
760
803
|
}
|
|
761
|
-
await new Promise((
|
|
804
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
762
805
|
}
|
|
763
806
|
return { error: "Zeit\xFCberschreitung bei der Anmeldung", success: false };
|
|
764
807
|
},
|
|
@@ -923,8 +966,9 @@ var whoamiCommand = new Command("whoami").description("Show current authenticate
|
|
|
923
966
|
});
|
|
924
967
|
});
|
|
925
968
|
|
|
926
|
-
// src/commands/
|
|
969
|
+
// src/commands/status.ts
|
|
927
970
|
import { Command as Command2 } from "commander";
|
|
971
|
+
import chalk3 from "chalk";
|
|
928
972
|
|
|
929
973
|
// src/utils/guards.ts
|
|
930
974
|
async function requireAuth() {
|
|
@@ -1000,159 +1044,8 @@ async function getCommandContextWithEnvironment(environmentSlug, showSpinner = t
|
|
|
1000
1044
|
return { projectSlug, project, environment };
|
|
1001
1045
|
}
|
|
1002
1046
|
|
|
1003
|
-
// src/commands/deploy.ts
|
|
1004
|
-
import chalk3 from "chalk";
|
|
1005
|
-
var deployCommand = new Command2("deploy").description("Deploy to an environment").argument(
|
|
1006
|
-
"<environment>",
|
|
1007
|
-
"Environment slug (e.g., production, staging, dev)"
|
|
1008
|
-
).option("-i, --image <tag>", "Specific image tag to deploy").option("-w, --wait", "Wait for deployment to complete (default: true)", true).option("--no-wait", "Do not wait for deployment to complete").option("--timeout <ms>", "Timeout in milliseconds for --wait", "600000").action(async (environment, options) => {
|
|
1009
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1010
|
-
logger.header(`Deploying to ${environment}`);
|
|
1011
|
-
const { data: deployment, error: deployError } = await withSpinner(
|
|
1012
|
-
`Starting deployment...`,
|
|
1013
|
-
() => apiClient.deploy(env.id, options.image)
|
|
1014
|
-
);
|
|
1015
|
-
if (deployError || !deployment) {
|
|
1016
|
-
logger.error(
|
|
1017
|
-
`Failed to start deployment: ${deployError || "Unknown error"}`
|
|
1018
|
-
);
|
|
1019
|
-
process.exit(13 /* API_ERROR */);
|
|
1020
|
-
}
|
|
1021
|
-
logger.success(`Deployment started: ${deployment.id}`);
|
|
1022
|
-
addJsonData({ deploymentId: deployment.id });
|
|
1023
|
-
if (!options.wait) {
|
|
1024
|
-
logger.info(`View deployment status: turbo status ${environment}`);
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
logger.newline();
|
|
1028
|
-
const spinner = createSpinner("Deploying...");
|
|
1029
|
-
spinner.start();
|
|
1030
|
-
try {
|
|
1031
|
-
const finalStatus = await apiClient.waitForDeployment(deployment.id, {
|
|
1032
|
-
timeout: parseInt(options.timeout),
|
|
1033
|
-
onProgress: (status) => {
|
|
1034
|
-
spinner.text = getStatusText(status);
|
|
1035
|
-
}
|
|
1036
|
-
});
|
|
1037
|
-
if (finalStatus.status === "running") {
|
|
1038
|
-
spinner.succeed(`Deployment successful!`);
|
|
1039
|
-
const resultData = {
|
|
1040
|
-
environment: env.name,
|
|
1041
|
-
environmentSlug: env.slug,
|
|
1042
|
-
imageTag: finalStatus.imageTag,
|
|
1043
|
-
status: finalStatus.status,
|
|
1044
|
-
healthyContainers: finalStatus.healthStatus?.healthy || 0,
|
|
1045
|
-
totalContainers: finalStatus.healthStatus?.total || 0,
|
|
1046
|
-
domain: env.domain
|
|
1047
|
-
};
|
|
1048
|
-
addJsonData(resultData);
|
|
1049
|
-
logger.newline();
|
|
1050
|
-
logger.table({
|
|
1051
|
-
Environment: env.name,
|
|
1052
|
-
Image: finalStatus.imageTag,
|
|
1053
|
-
Status: chalk3.green(finalStatus.status),
|
|
1054
|
-
Health: `${finalStatus.healthStatus?.healthy || 0}/${finalStatus.healthStatus?.total || 0} healthy`
|
|
1055
|
-
});
|
|
1056
|
-
logger.newline();
|
|
1057
|
-
logger.info(`View at: https://${env.domain}`);
|
|
1058
|
-
} else if (finalStatus.status === "failed") {
|
|
1059
|
-
spinner.fail("Deployment failed!");
|
|
1060
|
-
addJsonData({ status: "failed", error: finalStatus.errorMessage });
|
|
1061
|
-
logger.error(finalStatus.errorMessage || "Unknown error");
|
|
1062
|
-
process.exit(3 /* HEALTH_CHECK_FAILED */);
|
|
1063
|
-
} else {
|
|
1064
|
-
spinner.warn(`Deployment ended with status: ${finalStatus.status}`);
|
|
1065
|
-
addJsonData({ status: finalStatus.status });
|
|
1066
|
-
process.exit(1 /* ERROR */);
|
|
1067
|
-
}
|
|
1068
|
-
} catch (error) {
|
|
1069
|
-
spinner.fail("Deployment failed!");
|
|
1070
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1071
|
-
addJsonData({ error: message });
|
|
1072
|
-
logger.error(message);
|
|
1073
|
-
if (message.includes("timeout")) {
|
|
1074
|
-
process.exit(2 /* TIMEOUT */);
|
|
1075
|
-
}
|
|
1076
|
-
process.exit(1 /* ERROR */);
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
var rollbackCommand = new Command2("rollback").description("Rollback to a previous deployment").argument("<environment>", "Environment slug").option("--to <id>", "Specific deployment ID to rollback to").action(async (environment, options) => {
|
|
1080
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1081
|
-
logger.header(`Rolling back ${environment}`);
|
|
1082
|
-
const { data, error } = await withSpinner(
|
|
1083
|
-
"Starting rollback...",
|
|
1084
|
-
() => apiClient.rollback(env.id, options.to)
|
|
1085
|
-
);
|
|
1086
|
-
if (error || !data) {
|
|
1087
|
-
logger.error(`Rollback failed: ${error || "Unknown error"}`);
|
|
1088
|
-
process.exit(13 /* API_ERROR */);
|
|
1089
|
-
}
|
|
1090
|
-
addJsonData({
|
|
1091
|
-
status: "rollback_initiated",
|
|
1092
|
-
imageTag: data.imageTag,
|
|
1093
|
-
deploymentId: data.id
|
|
1094
|
-
});
|
|
1095
|
-
logger.success(`Rollback initiated to ${data.imageTag}`);
|
|
1096
|
-
logger.info("Run `turbo status` to monitor the rollback.");
|
|
1097
|
-
});
|
|
1098
|
-
var restartCommand = new Command2("restart").description("Restart containers in an environment").argument("<environment>", "Environment slug").action(async (environment) => {
|
|
1099
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1100
|
-
const { error } = await withSpinner(
|
|
1101
|
-
"Restarting containers...",
|
|
1102
|
-
() => apiClient.restart(env.id)
|
|
1103
|
-
);
|
|
1104
|
-
if (error) {
|
|
1105
|
-
logger.error(`Restart failed: ${error}`);
|
|
1106
|
-
process.exit(13 /* API_ERROR */);
|
|
1107
|
-
}
|
|
1108
|
-
addJsonData({ status: "restarting", environment: env.slug });
|
|
1109
|
-
logger.success("Containers are being restarted");
|
|
1110
|
-
});
|
|
1111
|
-
var stopCommand = new Command2("stop").description("Stop containers in an environment").argument("<environment>", "Environment slug").action(async (environment) => {
|
|
1112
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1113
|
-
const { error } = await withSpinner(
|
|
1114
|
-
"Stopping containers...",
|
|
1115
|
-
() => apiClient.stop(env.id)
|
|
1116
|
-
);
|
|
1117
|
-
if (error) {
|
|
1118
|
-
logger.error(`Stop failed: ${error}`);
|
|
1119
|
-
process.exit(13 /* API_ERROR */);
|
|
1120
|
-
}
|
|
1121
|
-
addJsonData({ status: "stopped", environment: env.slug });
|
|
1122
|
-
logger.success("Containers have been stopped");
|
|
1123
|
-
});
|
|
1124
|
-
var wakeCommand = new Command2("wake").description("Wake (start) stopped containers in an environment").argument("<environment>", "Environment slug").action(async (environment) => {
|
|
1125
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1126
|
-
const { error } = await withSpinner(
|
|
1127
|
-
"Starting containers...",
|
|
1128
|
-
() => apiClient.wake(env.id)
|
|
1129
|
-
);
|
|
1130
|
-
if (error) {
|
|
1131
|
-
logger.error(`Wake failed: ${error}`);
|
|
1132
|
-
process.exit(13 /* API_ERROR */);
|
|
1133
|
-
}
|
|
1134
|
-
addJsonData({ status: "starting", environment: env.slug });
|
|
1135
|
-
logger.success("Containers are starting");
|
|
1136
|
-
});
|
|
1137
|
-
function getStatusText(status) {
|
|
1138
|
-
switch (status.status) {
|
|
1139
|
-
case "pending":
|
|
1140
|
-
return "Waiting for deployment to start...";
|
|
1141
|
-
case "deploying":
|
|
1142
|
-
return `Deploying ${status.imageTag}...`;
|
|
1143
|
-
case "running":
|
|
1144
|
-
return "Deployment complete!";
|
|
1145
|
-
case "failed":
|
|
1146
|
-
return "Deployment failed";
|
|
1147
|
-
default:
|
|
1148
|
-
return `Status: ${status.status}`;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
1047
|
// src/commands/status.ts
|
|
1153
|
-
|
|
1154
|
-
import chalk4 from "chalk";
|
|
1155
|
-
var statusCommand = new Command3("status").description("Show deployment status").argument(
|
|
1048
|
+
var statusCommand = new Command2("status").description("Show deployment status").argument(
|
|
1156
1049
|
"[environment]",
|
|
1157
1050
|
"Environment slug (optional, shows all if not specified)"
|
|
1158
1051
|
).action(async (environment) => {
|
|
@@ -1196,136 +1089,107 @@ var statusCommand = new Command3("status").description("Show deployment status")
|
|
|
1196
1089
|
});
|
|
1197
1090
|
for (const env of envsToShow) {
|
|
1198
1091
|
logger.newline();
|
|
1199
|
-
console.log(
|
|
1200
|
-
console.log(
|
|
1092
|
+
console.log(chalk3.bold(env.name) + ` (${env.slug})`);
|
|
1093
|
+
console.log(chalk3.gray("\u2500".repeat(40)));
|
|
1201
1094
|
const statusColor = getStatusColor(env.status);
|
|
1202
1095
|
logger.table({
|
|
1203
1096
|
Status: statusColor(env.status),
|
|
1204
1097
|
Type: env.type,
|
|
1205
|
-
Domain:
|
|
1098
|
+
Domain: chalk3.cyan(env.domain)
|
|
1206
1099
|
});
|
|
1207
1100
|
}
|
|
1208
1101
|
});
|
|
1209
|
-
var configCommand = new
|
|
1210
|
-
|
|
1211
|
-
|
|
1102
|
+
var configCommand = new Command2("config").description("Manage configuration");
|
|
1103
|
+
configCommand.command("show", { isDefault: true }).description("Show current configuration").action(() => {
|
|
1104
|
+
const config = configService.getAll();
|
|
1105
|
+
addJsonData({ config });
|
|
1212
1106
|
configService.show();
|
|
1213
1107
|
});
|
|
1108
|
+
configCommand.command("set").description("Set a configuration value").argument("<key>", "Configuration key (token, project)").argument("<value>", "Value to set").action((key, value) => {
|
|
1109
|
+
switch (key.toLowerCase()) {
|
|
1110
|
+
case "token":
|
|
1111
|
+
configService.setToken(value);
|
|
1112
|
+
logger.success("Token saved (global)");
|
|
1113
|
+
addJsonData({ key: "token", saved: true, scope: "global" });
|
|
1114
|
+
break;
|
|
1115
|
+
case "project":
|
|
1116
|
+
configService.setProject(value);
|
|
1117
|
+
logger.success(`Project saved to .turboops.json`);
|
|
1118
|
+
addJsonData({ key: "project", value, saved: true, scope: "local" });
|
|
1119
|
+
break;
|
|
1120
|
+
default:
|
|
1121
|
+
logger.error(`Unknown config key: ${key}`);
|
|
1122
|
+
logger.info("Available keys: token, project");
|
|
1123
|
+
addJsonData({ error: `Unknown config key: ${key}` });
|
|
1124
|
+
process.exit(14 /* VALIDATION_ERROR */);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
configCommand.command("get").description("Get a configuration value").argument("<key>", "Configuration key (token, project, api-url)").action((key) => {
|
|
1128
|
+
let value = null;
|
|
1129
|
+
switch (key.toLowerCase()) {
|
|
1130
|
+
case "token":
|
|
1131
|
+
value = configService.getToken();
|
|
1132
|
+
if (value) {
|
|
1133
|
+
console.log("***");
|
|
1134
|
+
addJsonData({ key: "token", set: true });
|
|
1135
|
+
} else {
|
|
1136
|
+
console.log("(not set)");
|
|
1137
|
+
addJsonData({ key: "token", set: false });
|
|
1138
|
+
}
|
|
1139
|
+
return;
|
|
1140
|
+
case "project":
|
|
1141
|
+
value = configService.getProject();
|
|
1142
|
+
break;
|
|
1143
|
+
case "api-url":
|
|
1144
|
+
value = configService.getApiUrl();
|
|
1145
|
+
break;
|
|
1146
|
+
default:
|
|
1147
|
+
logger.error(`Unknown config key: ${key}`);
|
|
1148
|
+
logger.info("Available keys: token, project, api-url");
|
|
1149
|
+
addJsonData({ error: `Unknown config key: ${key}` });
|
|
1150
|
+
process.exit(14 /* VALIDATION_ERROR */);
|
|
1151
|
+
}
|
|
1152
|
+
console.log(value || "(not set)");
|
|
1153
|
+
addJsonData({ key, value });
|
|
1154
|
+
});
|
|
1155
|
+
configCommand.command("clear").description("Clear configuration").option("--all", "Clear all configuration").option("--token", "Clear token only").option("--project", "Clear project only").action((opts) => {
|
|
1156
|
+
if (opts.all) {
|
|
1157
|
+
configService.clearAll();
|
|
1158
|
+
logger.success("All configuration cleared");
|
|
1159
|
+
addJsonData({ cleared: "all" });
|
|
1160
|
+
} else if (opts.token) {
|
|
1161
|
+
configService.clearToken();
|
|
1162
|
+
logger.success("Token cleared");
|
|
1163
|
+
addJsonData({ cleared: "token" });
|
|
1164
|
+
} else if (opts.project) {
|
|
1165
|
+
configService.clearProject();
|
|
1166
|
+
logger.success("Project configuration cleared");
|
|
1167
|
+
addJsonData({ cleared: "project" });
|
|
1168
|
+
} else {
|
|
1169
|
+
logger.error("Specify what to clear: --all, --token, or --project");
|
|
1170
|
+
process.exit(14 /* VALIDATION_ERROR */);
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1214
1173
|
function getStatusColor(status) {
|
|
1215
1174
|
switch (status) {
|
|
1216
1175
|
case "healthy":
|
|
1217
1176
|
case "running":
|
|
1218
|
-
return
|
|
1177
|
+
return chalk3.green;
|
|
1219
1178
|
case "deploying":
|
|
1220
|
-
return
|
|
1179
|
+
return chalk3.yellow;
|
|
1221
1180
|
case "failed":
|
|
1222
1181
|
case "stopped":
|
|
1223
|
-
return
|
|
1182
|
+
return chalk3.red;
|
|
1224
1183
|
default:
|
|
1225
|
-
return
|
|
1184
|
+
return chalk3.gray;
|
|
1226
1185
|
}
|
|
1227
1186
|
}
|
|
1228
1187
|
|
|
1229
|
-
// src/commands/env.ts
|
|
1230
|
-
import { Command as Command4 } from "commander";
|
|
1231
|
-
import chalk5 from "chalk";
|
|
1232
|
-
var envCommand = new Command4("env").description("Manage environment variables").argument("<environment>", "Environment slug").option("-r, --reveal", "Show secret values (normally masked)").action(async (environment, options) => {
|
|
1233
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1234
|
-
const { data: envVars, error } = await withSpinner(
|
|
1235
|
-
"Fetching environment variables...",
|
|
1236
|
-
() => apiClient.getEnvVars(env.id)
|
|
1237
|
-
);
|
|
1238
|
-
if (error || !envVars) {
|
|
1239
|
-
logger.error(
|
|
1240
|
-
`Failed to fetch environment variables: ${error || "Unknown error"}`
|
|
1241
|
-
);
|
|
1242
|
-
process.exit(13 /* API_ERROR */);
|
|
1243
|
-
}
|
|
1244
|
-
addJsonData({
|
|
1245
|
-
environment: env.name,
|
|
1246
|
-
environmentSlug: env.slug,
|
|
1247
|
-
variables: envVars.map((v) => ({
|
|
1248
|
-
key: v.key,
|
|
1249
|
-
secret: v.secret
|
|
1250
|
-
}))
|
|
1251
|
-
});
|
|
1252
|
-
logger.header(`Environment Variables: ${env.name}`);
|
|
1253
|
-
if (envVars.length === 0) {
|
|
1254
|
-
logger.warning("No environment variables configured.");
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
for (const v of envVars) {
|
|
1258
|
-
const icon = v.secret ? chalk5.yellow("\u{1F512}") : chalk5.gray(" ");
|
|
1259
|
-
const value = v.secret && !options.reveal ? chalk5.gray("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022") : chalk5.green("(set)");
|
|
1260
|
-
console.log(`${icon} ${chalk5.bold(v.key)} = ${value}`);
|
|
1261
|
-
}
|
|
1262
|
-
logger.newline();
|
|
1263
|
-
logger.info(
|
|
1264
|
-
"Use `turbo env <environment> set KEY=value` to add/update variables."
|
|
1265
|
-
);
|
|
1266
|
-
logger.info("Use `turbo env <environment> unset KEY` to remove variables.");
|
|
1267
|
-
});
|
|
1268
|
-
var envSetCommand = new Command4("set").description("Set an environment variable").argument("<key=value>", "Variable to set (e.g., API_KEY=secret123)").option("-s, --secret", "Mark as secret (value will be encrypted)").action(async (keyValue, options, cmd) => {
|
|
1269
|
-
const environment = cmd.parent?.args[0];
|
|
1270
|
-
if (!environment) {
|
|
1271
|
-
logger.error("Environment not specified");
|
|
1272
|
-
process.exit(14 /* VALIDATION_ERROR */);
|
|
1273
|
-
}
|
|
1274
|
-
const [key, ...valueParts] = keyValue.split("=");
|
|
1275
|
-
const value = valueParts.join("=");
|
|
1276
|
-
if (!key || !value) {
|
|
1277
|
-
logger.error("Invalid format. Use: KEY=value");
|
|
1278
|
-
process.exit(14 /* VALIDATION_ERROR */);
|
|
1279
|
-
}
|
|
1280
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1281
|
-
const { error } = await withSpinner(
|
|
1282
|
-
`Setting ${key}...`,
|
|
1283
|
-
() => apiClient.setEnvVar(env.id, key, value, options.secret || false)
|
|
1284
|
-
);
|
|
1285
|
-
if (error) {
|
|
1286
|
-
logger.error(`Failed to set variable: ${error}`);
|
|
1287
|
-
process.exit(13 /* API_ERROR */);
|
|
1288
|
-
}
|
|
1289
|
-
addJsonData({
|
|
1290
|
-
action: "set",
|
|
1291
|
-
key,
|
|
1292
|
-
secret: options.secret || false,
|
|
1293
|
-
environment: env.slug
|
|
1294
|
-
});
|
|
1295
|
-
logger.success(`Set ${key}${options.secret ? " (secret)" : ""}`);
|
|
1296
|
-
logger.info("Changes will take effect on the next deployment.");
|
|
1297
|
-
});
|
|
1298
|
-
var envUnsetCommand = new Command4("unset").description("Remove an environment variable").argument("<key>", "Variable key to remove").action(async (key, _options, cmd) => {
|
|
1299
|
-
const environment = cmd.parent?.args[0];
|
|
1300
|
-
if (!environment) {
|
|
1301
|
-
logger.error("Environment not specified");
|
|
1302
|
-
process.exit(14 /* VALIDATION_ERROR */);
|
|
1303
|
-
}
|
|
1304
|
-
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1305
|
-
const { error } = await withSpinner(
|
|
1306
|
-
`Removing ${key}...`,
|
|
1307
|
-
() => apiClient.deleteEnvVar(env.id, key)
|
|
1308
|
-
);
|
|
1309
|
-
if (error) {
|
|
1310
|
-
logger.error(`Failed to remove variable: ${error}`);
|
|
1311
|
-
process.exit(13 /* API_ERROR */);
|
|
1312
|
-
}
|
|
1313
|
-
addJsonData({
|
|
1314
|
-
action: "unset",
|
|
1315
|
-
key,
|
|
1316
|
-
environment: env.slug
|
|
1317
|
-
});
|
|
1318
|
-
logger.success(`Removed ${key}`);
|
|
1319
|
-
logger.info("Changes will take effect on the next deployment.");
|
|
1320
|
-
});
|
|
1321
|
-
envCommand.addCommand(envSetCommand);
|
|
1322
|
-
envCommand.addCommand(envUnsetCommand);
|
|
1323
|
-
|
|
1324
1188
|
// src/commands/init.ts
|
|
1325
|
-
import { Command as
|
|
1189
|
+
import { Command as Command3 } from "commander";
|
|
1326
1190
|
import prompts from "prompts";
|
|
1327
|
-
import
|
|
1328
|
-
var initCommand = new
|
|
1191
|
+
import chalk4 from "chalk";
|
|
1192
|
+
var initCommand = new Command3("init").description("Initialize TurboOps project in current directory").action(async () => {
|
|
1329
1193
|
logger.header("TurboOps Project Initialization");
|
|
1330
1194
|
if (!configService.isAuthenticated()) {
|
|
1331
1195
|
logger.warning("Nicht authentifiziert. Bitte melden Sie sich zuerst an.");
|
|
@@ -1366,25 +1230,295 @@ var initCommand = new Command5("init").description("Initialize TurboOps project
|
|
|
1366
1230
|
addJsonData({ initialized: false, reason: "cancelled" });
|
|
1367
1231
|
return;
|
|
1368
1232
|
}
|
|
1369
|
-
const { data: project
|
|
1370
|
-
"
|
|
1233
|
+
const { data: project } = await withSpinner(
|
|
1234
|
+
"Suche Projekt...",
|
|
1371
1235
|
() => apiClient.getProject(projectSlug)
|
|
1372
1236
|
);
|
|
1373
|
-
if (
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1237
|
+
if (project) {
|
|
1238
|
+
await setupProject(project);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
logger.newline();
|
|
1242
|
+
logger.warning(`Projekt "${projectSlug}" nicht gefunden.`);
|
|
1243
|
+
const { shouldCreate } = await prompts({
|
|
1244
|
+
initial: true,
|
|
1245
|
+
message: "M\xF6chten Sie ein neues Projekt anlegen?",
|
|
1246
|
+
name: "shouldCreate",
|
|
1247
|
+
type: "confirm"
|
|
1248
|
+
});
|
|
1249
|
+
if (!shouldCreate) {
|
|
1250
|
+
logger.info("Initialisierung abgebrochen.");
|
|
1251
|
+
addJsonData({ initialized: false, reason: "project_not_found" });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
await createNewProject(projectSlug);
|
|
1255
|
+
});
|
|
1256
|
+
async function setupProject(project) {
|
|
1257
|
+
configService.setProject(project.slug);
|
|
1258
|
+
const { data: environments } = await apiClient.getEnvironments(project.id);
|
|
1259
|
+
if (!environments || environments.length === 0) {
|
|
1260
|
+
logger.newline();
|
|
1261
|
+
const { shouldCreateStage } = await prompts({
|
|
1262
|
+
initial: true,
|
|
1263
|
+
message: "Das Projekt hat noch keine Stages. M\xF6chten Sie jetzt eine erstellen?",
|
|
1264
|
+
name: "shouldCreateStage",
|
|
1265
|
+
type: "confirm"
|
|
1266
|
+
});
|
|
1267
|
+
if (shouldCreateStage) {
|
|
1268
|
+
await createFirstStage(project.id, project.slug);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const fs3 = await import("fs/promises");
|
|
1272
|
+
const path3 = await import("path");
|
|
1273
|
+
const gitlabCiPath = path3.join(process.cwd(), ".gitlab-ci.yml");
|
|
1274
|
+
let hasGitLabPipeline = false;
|
|
1275
|
+
try {
|
|
1276
|
+
await fs3.access(gitlabCiPath);
|
|
1277
|
+
hasGitLabPipeline = true;
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
if (!hasGitLabPipeline) {
|
|
1281
|
+
logger.newline();
|
|
1282
|
+
const { shouldCreatePipeline } = await prompts({
|
|
1283
|
+
initial: false,
|
|
1284
|
+
message: "M\xF6chten Sie eine CI/CD Pipeline anlegen?",
|
|
1285
|
+
name: "shouldCreatePipeline",
|
|
1286
|
+
type: "confirm"
|
|
1287
|
+
});
|
|
1288
|
+
if (shouldCreatePipeline) {
|
|
1289
|
+
await createPipeline(project.id);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
await showFinalSummary(project);
|
|
1293
|
+
}
|
|
1294
|
+
async function createNewProject(slug) {
|
|
1295
|
+
logger.newline();
|
|
1296
|
+
logger.header("Neues Projekt erstellen");
|
|
1297
|
+
const { data: customers } = await withSpinner(
|
|
1298
|
+
"Lade verf\xFCgbare Kunden...",
|
|
1299
|
+
() => apiClient.getCustomers()
|
|
1300
|
+
);
|
|
1301
|
+
const promptQuestions = [
|
|
1302
|
+
{
|
|
1303
|
+
initial: slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, " "),
|
|
1304
|
+
message: "Projektname:",
|
|
1305
|
+
name: "name",
|
|
1306
|
+
type: "text",
|
|
1307
|
+
validate: (value) => value.length > 0 || "Projektname ist erforderlich"
|
|
1308
|
+
}
|
|
1309
|
+
];
|
|
1310
|
+
if (customers && customers.length > 0) {
|
|
1311
|
+
promptQuestions.push({
|
|
1312
|
+
choices: [
|
|
1313
|
+
{ title: "(Kein Kunde)", value: "" },
|
|
1314
|
+
...customers.map((c) => ({ title: c.name, value: c.id }))
|
|
1315
|
+
],
|
|
1316
|
+
message: "Kunde (optional):",
|
|
1317
|
+
name: "customer",
|
|
1318
|
+
type: "select"
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
promptQuestions.push(
|
|
1322
|
+
{
|
|
1323
|
+
message: "Beschreibung (optional):",
|
|
1324
|
+
name: "description",
|
|
1325
|
+
type: "text"
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
message: "Repository URL (optional):",
|
|
1329
|
+
name: "repositoryUrl",
|
|
1330
|
+
type: "text"
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
const projectDetails = await prompts(promptQuestions);
|
|
1334
|
+
if (!projectDetails.name) {
|
|
1335
|
+
logger.warning("Projekterstellung abgebrochen");
|
|
1336
|
+
addJsonData({ initialized: false, reason: "cancelled" });
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const { data: newProject, error: createError } = await withSpinner(
|
|
1340
|
+
"Erstelle Projekt...",
|
|
1341
|
+
() => apiClient.createProject({
|
|
1342
|
+
customer: projectDetails.customer || void 0,
|
|
1343
|
+
description: projectDetails.description || void 0,
|
|
1344
|
+
name: projectDetails.name,
|
|
1345
|
+
repositoryUrl: projectDetails.repositoryUrl || void 0,
|
|
1346
|
+
slug
|
|
1347
|
+
})
|
|
1348
|
+
);
|
|
1349
|
+
if (createError || !newProject) {
|
|
1350
|
+
logger.error(`Projekt konnte nicht erstellt werden: ${createError || "Unbekannter Fehler"}`);
|
|
1380
1351
|
addJsonData({
|
|
1352
|
+
error: createError || "Unknown error",
|
|
1381
1353
|
initialized: false,
|
|
1382
|
-
|
|
1383
|
-
reason: "project_not_found"
|
|
1354
|
+
reason: "create_failed"
|
|
1384
1355
|
});
|
|
1385
|
-
process.exit(
|
|
1356
|
+
process.exit(13 /* API_ERROR */);
|
|
1357
|
+
}
|
|
1358
|
+
logger.success(`Projekt "${newProject.name}" wurde erstellt!`);
|
|
1359
|
+
configService.setProject(newProject.slug);
|
|
1360
|
+
logger.newline();
|
|
1361
|
+
const { shouldCreateStage } = await prompts({
|
|
1362
|
+
initial: true,
|
|
1363
|
+
message: "M\xF6chten Sie jetzt die erste Stage anlegen?",
|
|
1364
|
+
name: "shouldCreateStage",
|
|
1365
|
+
type: "confirm"
|
|
1366
|
+
});
|
|
1367
|
+
if (shouldCreateStage) {
|
|
1368
|
+
await createFirstStage(newProject.id, newProject.slug);
|
|
1386
1369
|
}
|
|
1387
|
-
|
|
1370
|
+
logger.newline();
|
|
1371
|
+
const { shouldCreatePipeline } = await prompts({
|
|
1372
|
+
initial: true,
|
|
1373
|
+
message: "M\xF6chten Sie eine CI/CD Pipeline anlegen?",
|
|
1374
|
+
name: "shouldCreatePipeline",
|
|
1375
|
+
type: "confirm"
|
|
1376
|
+
});
|
|
1377
|
+
if (shouldCreatePipeline) {
|
|
1378
|
+
await createPipeline(newProject.id);
|
|
1379
|
+
}
|
|
1380
|
+
await showFinalSummary(newProject);
|
|
1381
|
+
}
|
|
1382
|
+
async function createFirstStage(projectId, projectSlug) {
|
|
1383
|
+
logger.newline();
|
|
1384
|
+
logger.header("Erste Stage erstellen");
|
|
1385
|
+
const { data: servers } = await withSpinner(
|
|
1386
|
+
"Lade verf\xFCgbare Server...",
|
|
1387
|
+
() => apiClient.getDeploymentServers()
|
|
1388
|
+
);
|
|
1389
|
+
const stageTypes = [
|
|
1390
|
+
{ title: "Development", value: "development" },
|
|
1391
|
+
{ title: "Staging", value: "staging" },
|
|
1392
|
+
{ title: "Production", value: "production" }
|
|
1393
|
+
];
|
|
1394
|
+
const stageQuestions = [
|
|
1395
|
+
{
|
|
1396
|
+
choices: stageTypes,
|
|
1397
|
+
initial: 0,
|
|
1398
|
+
message: "Stage-Typ:",
|
|
1399
|
+
name: "type",
|
|
1400
|
+
type: "select"
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
initial: (prev) => prev === "production" ? "Production" : prev === "staging" ? "Staging" : "Development",
|
|
1404
|
+
message: "Stage-Name:",
|
|
1405
|
+
name: "name",
|
|
1406
|
+
type: "text",
|
|
1407
|
+
validate: (value) => value.length > 0 || "Name ist erforderlich"
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
initial: (_prev, values) => values.type || "",
|
|
1411
|
+
message: "Stage-Slug:",
|
|
1412
|
+
name: "slug",
|
|
1413
|
+
type: "text",
|
|
1414
|
+
validate: (value) => value.length > 0 || "Slug ist erforderlich"
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
initial: (_prev, values) => `${values.slug || ""}.${projectSlug}.example.com`,
|
|
1418
|
+
message: "Domain:",
|
|
1419
|
+
name: "domain",
|
|
1420
|
+
type: "text"
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
initial: (_prev, values) => values.type === "production" ? "main" : values.type === "staging" ? "staging" : "develop",
|
|
1424
|
+
message: "Branch:",
|
|
1425
|
+
name: "branch",
|
|
1426
|
+
type: "text"
|
|
1427
|
+
}
|
|
1428
|
+
];
|
|
1429
|
+
if (servers && servers.length > 0) {
|
|
1430
|
+
stageQuestions.push({
|
|
1431
|
+
choices: [
|
|
1432
|
+
{ title: "(Sp\xE4ter ausw\xE4hlen)", value: "" },
|
|
1433
|
+
...servers.map((s) => ({ title: `${s.name} (${s.host})`, value: s.id }))
|
|
1434
|
+
],
|
|
1435
|
+
message: "Server (optional):",
|
|
1436
|
+
name: "server",
|
|
1437
|
+
type: "select"
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
const stageDetails = await prompts(stageQuestions);
|
|
1441
|
+
if (!stageDetails.name || !stageDetails.slug) {
|
|
1442
|
+
logger.warning("Stage-Erstellung abgebrochen");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const { data: newStage, error: stageError } = await withSpinner(
|
|
1446
|
+
"Erstelle Stage...",
|
|
1447
|
+
() => apiClient.createStage({
|
|
1448
|
+
project: projectId,
|
|
1449
|
+
name: stageDetails.name,
|
|
1450
|
+
slug: stageDetails.slug,
|
|
1451
|
+
type: stageDetails.type,
|
|
1452
|
+
domain: stageDetails.domain || void 0,
|
|
1453
|
+
server: stageDetails.server || void 0,
|
|
1454
|
+
branch: stageDetails.branch || void 0
|
|
1455
|
+
})
|
|
1456
|
+
);
|
|
1457
|
+
if (stageError || !newStage) {
|
|
1458
|
+
logger.error(`Stage konnte nicht erstellt werden: ${stageError || "Unbekannter Fehler"}`);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
logger.success(`Stage "${newStage.name}" wurde erstellt!`);
|
|
1462
|
+
}
|
|
1463
|
+
async function createPipeline(projectId) {
|
|
1464
|
+
logger.newline();
|
|
1465
|
+
logger.header("CI/CD Pipeline erstellen");
|
|
1466
|
+
const pipelineQuestions = [
|
|
1467
|
+
{
|
|
1468
|
+
choices: [
|
|
1469
|
+
{ title: "GitLab CI/CD", value: "gitlab" },
|
|
1470
|
+
{ title: "GitHub Actions", value: "github" }
|
|
1471
|
+
],
|
|
1472
|
+
initial: 0,
|
|
1473
|
+
message: "Pipeline-Typ:",
|
|
1474
|
+
name: "pipelineType",
|
|
1475
|
+
type: "select"
|
|
1476
|
+
}
|
|
1477
|
+
];
|
|
1478
|
+
const pipelineDetails = await prompts(pipelineQuestions);
|
|
1479
|
+
if (!pipelineDetails.pipelineType) {
|
|
1480
|
+
logger.warning("Pipeline-Erstellung abgebrochen");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const { data: pipeline, error } = await withSpinner(
|
|
1484
|
+
"Generiere Pipeline...",
|
|
1485
|
+
() => apiClient.generatePipeline(projectId, pipelineDetails.pipelineType)
|
|
1486
|
+
);
|
|
1487
|
+
if (error || !pipeline) {
|
|
1488
|
+
logger.error(`Pipeline konnte nicht generiert werden: ${error || "Unbekannter Fehler"}`);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const fs3 = await import("fs/promises");
|
|
1492
|
+
const path3 = await import("path");
|
|
1493
|
+
let filePath;
|
|
1494
|
+
if (pipelineDetails.pipelineType === "github") {
|
|
1495
|
+
const workflowsDir = path3.join(process.cwd(), ".github", "workflows");
|
|
1496
|
+
await fs3.mkdir(workflowsDir, { recursive: true });
|
|
1497
|
+
filePath = path3.join(workflowsDir, "deploy.yml");
|
|
1498
|
+
} else {
|
|
1499
|
+
filePath = path3.join(process.cwd(), ".gitlab-ci.yml");
|
|
1500
|
+
}
|
|
1501
|
+
try {
|
|
1502
|
+
await fs3.writeFile(filePath, pipeline.content, "utf-8");
|
|
1503
|
+
logger.success(`${pipeline.filename} wurde erstellt!`);
|
|
1504
|
+
logger.newline();
|
|
1505
|
+
const { data: secrets } = await apiClient.getPipelineSecrets(projectId, pipelineDetails.pipelineType);
|
|
1506
|
+
if (secrets && secrets.length > 0) {
|
|
1507
|
+
logger.header("Erforderliche CI/CD Secrets");
|
|
1508
|
+
for (const secret of secrets) {
|
|
1509
|
+
const value = secret.isSecret ? chalk4.dim("(geheim)") : chalk4.cyan(secret.value || "-");
|
|
1510
|
+
console.log(` ${chalk4.bold(secret.name)}: ${value}`);
|
|
1511
|
+
console.log(` ${chalk4.dim(secret.description)}`);
|
|
1512
|
+
}
|
|
1513
|
+
logger.newline();
|
|
1514
|
+
logger.info("F\xFCgen Sie diese Werte als CI/CD Secrets/Variables hinzu.");
|
|
1515
|
+
logger.info("Projekt-Token k\xF6nnen Sie in der TurboOps Web-UI erstellen.");
|
|
1516
|
+
}
|
|
1517
|
+
} catch (error2) {
|
|
1518
|
+
logger.error(`Fehler beim Schreiben der Pipeline-Datei: ${error2}`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async function showFinalSummary(project) {
|
|
1388
1522
|
logger.newline();
|
|
1389
1523
|
logger.success("Projekt initialisiert!");
|
|
1390
1524
|
logger.newline();
|
|
@@ -1411,25 +1545,24 @@ var initCommand = new Command5("init").description("Initialize TurboOps project
|
|
|
1411
1545
|
});
|
|
1412
1546
|
if (environments && environments.length > 0) {
|
|
1413
1547
|
logger.newline();
|
|
1414
|
-
logger.header("Verf\xFCgbare
|
|
1548
|
+
logger.header("Verf\xFCgbare Stages");
|
|
1415
1549
|
for (const env of environments) {
|
|
1416
|
-
console.log(` ${
|
|
1550
|
+
console.log(` ${chalk4.bold(env.slug)} - ${env.name} (${env.type})`);
|
|
1417
1551
|
}
|
|
1418
1552
|
}
|
|
1419
1553
|
logger.newline();
|
|
1420
1554
|
logger.header("N\xE4chste Schritte");
|
|
1421
1555
|
logger.list([
|
|
1422
|
-
"F\xFChren Sie `turbo status` aus, um alle
|
|
1423
|
-
"F\xFChren Sie `turbo
|
|
1424
|
-
"F\xFChren Sie `turbo logs <umgebung>` aus, um Logs anzuzeigen"
|
|
1556
|
+
"F\xFChren Sie `turbo status` aus, um alle Stages zu sehen",
|
|
1557
|
+
"F\xFChren Sie `turbo logs <stage>` aus, um Logs anzuzeigen"
|
|
1425
1558
|
]);
|
|
1426
|
-
}
|
|
1559
|
+
}
|
|
1427
1560
|
|
|
1428
1561
|
// src/commands/logs.ts
|
|
1429
|
-
import { Command as
|
|
1430
|
-
import
|
|
1562
|
+
import { Command as Command4 } from "commander";
|
|
1563
|
+
import chalk5 from "chalk";
|
|
1431
1564
|
import { io } from "socket.io-client";
|
|
1432
|
-
var logsCommand = new
|
|
1565
|
+
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) => {
|
|
1433
1566
|
const { environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1434
1567
|
logger.header(`Logs: ${env.name}`);
|
|
1435
1568
|
if (options.follow) {
|
|
@@ -1550,9 +1683,9 @@ function printLogEntry(log) {
|
|
|
1550
1683
|
const timestamp = formatTimestamp(log.timestamp);
|
|
1551
1684
|
const levelColor = getLevelColor(log.level);
|
|
1552
1685
|
const levelStr = log.level.toUpperCase().padEnd(5);
|
|
1553
|
-
let output =
|
|
1686
|
+
let output = chalk5.gray(`[${timestamp}]`) + levelColor(` ${levelStr}`);
|
|
1554
1687
|
if (log.step) {
|
|
1555
|
-
output +=
|
|
1688
|
+
output += chalk5.cyan(` [${log.step}]`);
|
|
1556
1689
|
}
|
|
1557
1690
|
output += ` ${log.message}`;
|
|
1558
1691
|
logger.raw(output);
|
|
@@ -1569,26 +1702,264 @@ function getLevelColor(level) {
|
|
|
1569
1702
|
switch (level.toLowerCase()) {
|
|
1570
1703
|
case "error":
|
|
1571
1704
|
case "fatal":
|
|
1572
|
-
return
|
|
1705
|
+
return chalk5.red;
|
|
1573
1706
|
case "warn":
|
|
1574
1707
|
case "warning":
|
|
1575
|
-
return
|
|
1708
|
+
return chalk5.yellow;
|
|
1576
1709
|
case "info":
|
|
1577
|
-
return
|
|
1710
|
+
return chalk5.blue;
|
|
1578
1711
|
case "debug":
|
|
1579
|
-
return
|
|
1712
|
+
return chalk5.gray;
|
|
1580
1713
|
case "trace":
|
|
1581
|
-
return
|
|
1714
|
+
return chalk5.magenta;
|
|
1582
1715
|
default:
|
|
1583
|
-
return
|
|
1716
|
+
return chalk5.white;
|
|
1584
1717
|
}
|
|
1585
1718
|
}
|
|
1586
1719
|
|
|
1720
|
+
// src/commands/deploy.ts
|
|
1721
|
+
import { Command as Command5 } from "commander";
|
|
1722
|
+
import chalk6 from "chalk";
|
|
1723
|
+
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(
|
|
1724
|
+
"--timeout <ms>",
|
|
1725
|
+
"Timeout in milliseconds when waiting",
|
|
1726
|
+
"600000"
|
|
1727
|
+
).action(async (environment, options) => {
|
|
1728
|
+
const { project, environment: env } = await getCommandContextWithEnvironment(environment);
|
|
1729
|
+
logger.header(`Deploying: ${project.name} \u2192 ${env.name}`);
|
|
1730
|
+
logger.info("Triggering deployment...");
|
|
1731
|
+
const { data: deployment, error } = await apiClient.deploy(
|
|
1732
|
+
env.id,
|
|
1733
|
+
options.image
|
|
1734
|
+
);
|
|
1735
|
+
if (error) {
|
|
1736
|
+
logger.error(`Failed to trigger deployment: ${error}`);
|
|
1737
|
+
process.exit(13 /* API_ERROR */);
|
|
1738
|
+
}
|
|
1739
|
+
if (!deployment) {
|
|
1740
|
+
logger.error("No deployment data received");
|
|
1741
|
+
process.exit(13 /* API_ERROR */);
|
|
1742
|
+
}
|
|
1743
|
+
logger.success(`Deployment triggered: ${deployment.id}`);
|
|
1744
|
+
addJsonData({
|
|
1745
|
+
deploymentId: deployment.id,
|
|
1746
|
+
status: deployment.status,
|
|
1747
|
+
environment: env.slug,
|
|
1748
|
+
project: project.slug,
|
|
1749
|
+
imageTag: options.image || "latest"
|
|
1750
|
+
});
|
|
1751
|
+
if (!options.wait) {
|
|
1752
|
+
logger.info("Deployment started. Use --wait to wait for completion.");
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
logger.newline();
|
|
1756
|
+
logger.info("Waiting for deployment to complete...");
|
|
1757
|
+
const timeout = parseInt(options.timeout);
|
|
1758
|
+
let lastStatus = "";
|
|
1759
|
+
try {
|
|
1760
|
+
const finalStatus = await apiClient.waitForDeployment(deployment.id, {
|
|
1761
|
+
timeout,
|
|
1762
|
+
pollInterval: 3e3,
|
|
1763
|
+
onProgress: (status) => {
|
|
1764
|
+
if (status.status !== lastStatus) {
|
|
1765
|
+
lastStatus = status.status;
|
|
1766
|
+
const statusColor = getStatusColor2(status.status);
|
|
1767
|
+
if (!isJsonMode()) {
|
|
1768
|
+
logger.info(`Status: ${statusColor(status.status)}`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
addJsonData({
|
|
1774
|
+
finalStatus: finalStatus.status,
|
|
1775
|
+
completedAt: finalStatus.completedAt,
|
|
1776
|
+
healthStatus: finalStatus.healthStatus
|
|
1777
|
+
});
|
|
1778
|
+
if (finalStatus.status === "running") {
|
|
1779
|
+
logger.newline();
|
|
1780
|
+
logger.success("Deployment completed successfully!");
|
|
1781
|
+
if (finalStatus.healthStatus) {
|
|
1782
|
+
logger.info(
|
|
1783
|
+
`Health: ${finalStatus.healthStatus.healthy}/${finalStatus.healthStatus.total} containers healthy`
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
} else if (finalStatus.status === "failed") {
|
|
1787
|
+
logger.newline();
|
|
1788
|
+
logger.error("Deployment failed!");
|
|
1789
|
+
if (finalStatus.errorMessage) {
|
|
1790
|
+
logger.error(`Error: ${finalStatus.errorMessage}`);
|
|
1791
|
+
}
|
|
1792
|
+
process.exit(1 /* ERROR */);
|
|
1793
|
+
} else {
|
|
1794
|
+
logger.newline();
|
|
1795
|
+
logger.warning(`Deployment ended with status: ${finalStatus.status}`);
|
|
1796
|
+
process.exit(1 /* ERROR */);
|
|
1797
|
+
}
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1800
|
+
if (message.includes("timeout")) {
|
|
1801
|
+
logger.error(`Deployment timed out after ${timeout / 1e3} seconds`);
|
|
1802
|
+
addJsonData({ error: "timeout" });
|
|
1803
|
+
process.exit(2 /* TIMEOUT */);
|
|
1804
|
+
}
|
|
1805
|
+
logger.error(`Error waiting for deployment: ${message}`);
|
|
1806
|
+
process.exit(1 /* ERROR */);
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
function getStatusColor2(status) {
|
|
1810
|
+
switch (status) {
|
|
1811
|
+
case "running":
|
|
1812
|
+
return chalk6.green;
|
|
1813
|
+
case "failed":
|
|
1814
|
+
case "rolled_back":
|
|
1815
|
+
return chalk6.red;
|
|
1816
|
+
case "deploying":
|
|
1817
|
+
case "pending":
|
|
1818
|
+
return chalk6.yellow;
|
|
1819
|
+
case "stopped":
|
|
1820
|
+
return chalk6.gray;
|
|
1821
|
+
default:
|
|
1822
|
+
return chalk6.white;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/commands/pipeline.ts
|
|
1827
|
+
import { Command as Command6 } from "commander";
|
|
1828
|
+
import prompts2 from "prompts";
|
|
1829
|
+
import chalk7 from "chalk";
|
|
1830
|
+
import * as fs2 from "fs/promises";
|
|
1831
|
+
import * as path2 from "path";
|
|
1832
|
+
var pipelineCommand = new Command6("pipeline").description("Manage CI/CD pipeline configuration");
|
|
1833
|
+
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
|
+
const { project } = await getCommandContext();
|
|
1835
|
+
logger.header("CI/CD Pipeline generieren");
|
|
1836
|
+
let pipelineType = options.type;
|
|
1837
|
+
if (!pipelineType) {
|
|
1838
|
+
const { selectedType } = await prompts2({
|
|
1839
|
+
choices: [
|
|
1840
|
+
{ title: "GitLab CI/CD", value: "gitlab" },
|
|
1841
|
+
{ title: "GitHub Actions", value: "github" }
|
|
1842
|
+
],
|
|
1843
|
+
initial: 0,
|
|
1844
|
+
message: "Pipeline-Typ:",
|
|
1845
|
+
name: "selectedType",
|
|
1846
|
+
type: "select"
|
|
1847
|
+
});
|
|
1848
|
+
if (!selectedType) {
|
|
1849
|
+
logger.warning("Pipeline-Generierung abgebrochen");
|
|
1850
|
+
addJsonData({ generated: false, reason: "cancelled" });
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
pipelineType = selectedType;
|
|
1854
|
+
}
|
|
1855
|
+
if (!["gitlab", "github"].includes(pipelineType)) {
|
|
1856
|
+
logger.error(`Ung\xFCltiger Pipeline-Typ: ${pipelineType}. Erlaubt: gitlab, github`);
|
|
1857
|
+
process.exit(14 /* VALIDATION_ERROR */);
|
|
1858
|
+
}
|
|
1859
|
+
let outputPath;
|
|
1860
|
+
if (options.output) {
|
|
1861
|
+
outputPath = path2.resolve(options.output);
|
|
1862
|
+
} else if (pipelineType === "github") {
|
|
1863
|
+
outputPath = path2.join(process.cwd(), ".github", "workflows", "deploy.yml");
|
|
1864
|
+
} else {
|
|
1865
|
+
outputPath = path2.join(process.cwd(), ".gitlab-ci.yml");
|
|
1866
|
+
}
|
|
1867
|
+
if (!options.force) {
|
|
1868
|
+
try {
|
|
1869
|
+
await fs2.access(outputPath);
|
|
1870
|
+
const { shouldOverwrite } = await prompts2({
|
|
1871
|
+
initial: false,
|
|
1872
|
+
message: `${path2.basename(outputPath)} existiert bereits. \xDCberschreiben?`,
|
|
1873
|
+
name: "shouldOverwrite",
|
|
1874
|
+
type: "confirm"
|
|
1875
|
+
});
|
|
1876
|
+
if (!shouldOverwrite) {
|
|
1877
|
+
logger.info("Pipeline-Generierung abgebrochen");
|
|
1878
|
+
addJsonData({ generated: false, reason: "file_exists" });
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const { data: pipeline, error } = await withSpinner(
|
|
1885
|
+
"Generiere Pipeline...",
|
|
1886
|
+
() => apiClient.generatePipeline(project.id, pipelineType)
|
|
1887
|
+
);
|
|
1888
|
+
if (error || !pipeline) {
|
|
1889
|
+
logger.error(`Pipeline konnte nicht generiert werden: ${error || "Unbekannter Fehler"}`);
|
|
1890
|
+
addJsonData({ error: error || "Unknown error", generated: false });
|
|
1891
|
+
process.exit(13 /* API_ERROR */);
|
|
1892
|
+
}
|
|
1893
|
+
const outputDir = path2.dirname(outputPath);
|
|
1894
|
+
await fs2.mkdir(outputDir, { recursive: true });
|
|
1895
|
+
try {
|
|
1896
|
+
await fs2.writeFile(outputPath, pipeline.content, "utf-8");
|
|
1897
|
+
logger.success(`${path2.relative(process.cwd(), outputPath)} wurde erstellt!`);
|
|
1898
|
+
addJsonData({
|
|
1899
|
+
filename: path2.relative(process.cwd(), outputPath),
|
|
1900
|
+
generated: true,
|
|
1901
|
+
project: project.slug,
|
|
1902
|
+
type: pipelineType
|
|
1903
|
+
});
|
|
1904
|
+
} catch (err) {
|
|
1905
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1906
|
+
logger.error(`Fehler beim Schreiben der Datei: ${message}`);
|
|
1907
|
+
addJsonData({ error: message, generated: false });
|
|
1908
|
+
process.exit(1 /* ERROR */);
|
|
1909
|
+
}
|
|
1910
|
+
logger.newline();
|
|
1911
|
+
await showSecrets(project.id, pipelineType);
|
|
1912
|
+
});
|
|
1913
|
+
pipelineCommand.command("secrets").description("Show required CI/CD secrets").option("-t, --type <type>", "Pipeline type (gitlab, github)", "gitlab").action(async (options) => {
|
|
1914
|
+
const { project } = await getCommandContext();
|
|
1915
|
+
const pipelineType = options.type;
|
|
1916
|
+
if (!["gitlab", "github"].includes(pipelineType)) {
|
|
1917
|
+
logger.error(`Ung\xFCltiger Pipeline-Typ: ${pipelineType}. Erlaubt: gitlab, github`);
|
|
1918
|
+
process.exit(14 /* VALIDATION_ERROR */);
|
|
1919
|
+
}
|
|
1920
|
+
logger.header(`CI/CD Secrets f\xFCr ${pipelineType === "gitlab" ? "GitLab" : "GitHub"}`);
|
|
1921
|
+
await showSecrets(project.id, pipelineType);
|
|
1922
|
+
});
|
|
1923
|
+
async function showSecrets(projectId, pipelineType) {
|
|
1924
|
+
const { data: secrets, error } = await apiClient.getPipelineSecrets(projectId, pipelineType);
|
|
1925
|
+
if (error) {
|
|
1926
|
+
logger.error(`Fehler beim Laden der Secrets: ${error}`);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (!secrets || secrets.length === 0) {
|
|
1930
|
+
logger.info("Keine Secrets erforderlich.");
|
|
1931
|
+
addJsonData({ secrets: [] });
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const secretsList = [];
|
|
1935
|
+
for (const secret of secrets) {
|
|
1936
|
+
const value = secret.isSecret ? chalk7.dim("(geheim)") : chalk7.cyan(secret.value || "-");
|
|
1937
|
+
console.log(` ${chalk7.bold(secret.name)}: ${value}`);
|
|
1938
|
+
console.log(` ${chalk7.dim(secret.description)}`);
|
|
1939
|
+
console.log();
|
|
1940
|
+
secretsList.push({
|
|
1941
|
+
description: secret.description,
|
|
1942
|
+
name: secret.name,
|
|
1943
|
+
value: secret.isSecret ? void 0 : secret.value
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
addJsonData({ secrets: secretsList });
|
|
1947
|
+
logger.info("F\xFCgen Sie diese Werte als CI/CD Secrets/Variables hinzu.");
|
|
1948
|
+
if (pipelineType === "gitlab") {
|
|
1949
|
+
logger.info("GitLab: Settings \u2192 CI/CD \u2192 Variables");
|
|
1950
|
+
} else {
|
|
1951
|
+
logger.info("GitHub: Settings \u2192 Secrets and variables \u2192 Actions");
|
|
1952
|
+
}
|
|
1953
|
+
logger.newline();
|
|
1954
|
+
logger.info("Projekt-Token k\xF6nnen Sie in der TurboOps Web-UI erstellen:");
|
|
1955
|
+
logger.info("Projekt \u2192 Settings \u2192 Tokens \u2192 Neuen Token erstellen");
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1587
1958
|
// src/index.ts
|
|
1588
1959
|
var VERSION = getCurrentVersion();
|
|
1589
1960
|
var shouldCheckUpdate = true;
|
|
1590
1961
|
var program = new Command7();
|
|
1591
|
-
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("--
|
|
1962
|
+
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");
|
|
1592
1963
|
program.hook("preAction", (thisCommand) => {
|
|
1593
1964
|
const opts = thisCommand.opts();
|
|
1594
1965
|
clearJsonData();
|
|
@@ -1605,9 +1976,6 @@ program.hook("preAction", (thisCommand) => {
|
|
|
1605
1976
|
if (opts.token) {
|
|
1606
1977
|
configService.setToken(opts.token);
|
|
1607
1978
|
}
|
|
1608
|
-
if (opts.api) {
|
|
1609
|
-
configService.setApiUrl(opts.api);
|
|
1610
|
-
}
|
|
1611
1979
|
if (opts.verbose) {
|
|
1612
1980
|
process.env.DEBUG = "true";
|
|
1613
1981
|
}
|
|
@@ -1626,13 +1994,9 @@ program.addCommand(whoamiCommand);
|
|
|
1626
1994
|
program.addCommand(initCommand);
|
|
1627
1995
|
program.addCommand(statusCommand);
|
|
1628
1996
|
program.addCommand(configCommand);
|
|
1629
|
-
program.addCommand(deployCommand);
|
|
1630
|
-
program.addCommand(rollbackCommand);
|
|
1631
|
-
program.addCommand(restartCommand);
|
|
1632
|
-
program.addCommand(stopCommand);
|
|
1633
|
-
program.addCommand(wakeCommand);
|
|
1634
|
-
program.addCommand(envCommand);
|
|
1635
1997
|
program.addCommand(logsCommand);
|
|
1998
|
+
program.addCommand(deployCommand);
|
|
1999
|
+
program.addCommand(pipelineCommand);
|
|
1636
2000
|
program.command("self-update").description("Update TurboCLI to the latest version").action(async () => {
|
|
1637
2001
|
logger.info("Checking for updates...");
|
|
1638
2002
|
try {
|