@turboops/cli 1.0.0-dev.570

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 ADDED
@@ -0,0 +1,1507 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command7 } from "commander";
5
+ import chalk8 from "chalk";
6
+
7
+ // src/services/config.ts
8
+ import Conf from "conf";
9
+
10
+ // src/utils/logger.ts
11
+ import chalk from "chalk";
12
+
13
+ // src/utils/output.ts
14
+ var currentMode = "normal";
15
+ var jsonData = {};
16
+ function setOutputMode(mode) {
17
+ currentMode = mode;
18
+ }
19
+ function isJsonMode() {
20
+ return currentMode === "json";
21
+ }
22
+ function isQuietMode() {
23
+ return currentMode === "quiet";
24
+ }
25
+ function shouldShowOutput() {
26
+ return currentMode === "normal";
27
+ }
28
+ function addJsonData(data) {
29
+ jsonData = { ...jsonData, ...data };
30
+ }
31
+ function clearJsonData() {
32
+ jsonData = {};
33
+ }
34
+ function printJsonOutput() {
35
+ if (isJsonMode()) {
36
+ console.log(JSON.stringify(jsonData, null, 2));
37
+ }
38
+ }
39
+
40
+ // src/utils/logger.ts
41
+ var logger = {
42
+ info: (message) => {
43
+ if (shouldShowOutput()) {
44
+ console.log(chalk.blue("\u2139"), message);
45
+ }
46
+ },
47
+ success: (message) => {
48
+ if (isJsonMode()) {
49
+ addJsonData({ success: true, message });
50
+ } else if (shouldShowOutput()) {
51
+ console.log(chalk.green("\u2713"), message);
52
+ }
53
+ },
54
+ warning: (message) => {
55
+ if (isJsonMode()) {
56
+ addJsonData({ warning: message });
57
+ } else if (!isQuietMode()) {
58
+ console.log(chalk.yellow("\u26A0"), message);
59
+ }
60
+ },
61
+ error: (message) => {
62
+ if (isJsonMode()) {
63
+ addJsonData({ error: message, success: false });
64
+ } else {
65
+ console.error(chalk.red("\u2717"), message);
66
+ }
67
+ },
68
+ debug: (message) => {
69
+ if (process.env.DEBUG && shouldShowOutput()) {
70
+ console.log(chalk.gray("\u2299"), message);
71
+ }
72
+ },
73
+ log: (message) => {
74
+ if (shouldShowOutput()) {
75
+ console.log(message);
76
+ }
77
+ },
78
+ newline: () => {
79
+ if (shouldShowOutput()) {
80
+ console.log();
81
+ }
82
+ },
83
+ header: (title) => {
84
+ if (shouldShowOutput()) {
85
+ console.log();
86
+ console.log(chalk.bold.cyan(title));
87
+ console.log(chalk.gray("\u2500".repeat(title.length)));
88
+ }
89
+ },
90
+ table: (data) => {
91
+ if (isJsonMode()) {
92
+ addJsonData({ data });
93
+ } else if (shouldShowOutput()) {
94
+ const maxKeyLength = Math.max(...Object.keys(data).map((k) => k.length));
95
+ for (const [key, value] of Object.entries(data)) {
96
+ console.log(` ${chalk.gray(key.padEnd(maxKeyLength))} ${value}`);
97
+ }
98
+ }
99
+ },
100
+ list: (items) => {
101
+ if (isJsonMode()) {
102
+ addJsonData({ items });
103
+ } else if (shouldShowOutput()) {
104
+ for (const item of items) {
105
+ console.log(` ${chalk.gray("\u2022")} ${item}`);
106
+ }
107
+ }
108
+ },
109
+ json: (data) => {
110
+ console.log(JSON.stringify(data, null, 2));
111
+ },
112
+ /**
113
+ * Raw output - bypasses all mode checks
114
+ * Use for output that must always be shown (e.g., streaming logs)
115
+ */
116
+ raw: (message) => {
117
+ console.log(message);
118
+ }
119
+ };
120
+
121
+ // src/services/config.ts
122
+ var defaults = {
123
+ apiUrl: "https://api.turboops.io",
124
+ token: null,
125
+ project: null,
126
+ userId: null
127
+ };
128
+ var config = new Conf({
129
+ projectName: "turboops-cli",
130
+ defaults
131
+ });
132
+ var configService = {
133
+ /**
134
+ * Get API URL
135
+ */
136
+ getApiUrl() {
137
+ return process.env.TURBOOPS_API_URL || config.get("apiUrl");
138
+ },
139
+ /**
140
+ * Set API URL
141
+ */
142
+ setApiUrl(url) {
143
+ config.set("apiUrl", url);
144
+ },
145
+ /**
146
+ * Get authentication token
147
+ */
148
+ getToken() {
149
+ return process.env.TURBOOPS_TOKEN || config.get("token");
150
+ },
151
+ /**
152
+ * Set authentication token
153
+ */
154
+ setToken(token) {
155
+ config.set("token", token);
156
+ },
157
+ /**
158
+ * Clear authentication token
159
+ */
160
+ clearToken() {
161
+ config.set("token", null);
162
+ config.set("userId", null);
163
+ },
164
+ /**
165
+ * Get current project
166
+ */
167
+ getProject() {
168
+ return process.env.TURBOOPS_PROJECT || config.get("project");
169
+ },
170
+ /**
171
+ * Set current project
172
+ */
173
+ setProject(project) {
174
+ config.set("project", project);
175
+ },
176
+ /**
177
+ * Clear current project
178
+ */
179
+ clearProject() {
180
+ config.set("project", null);
181
+ },
182
+ /**
183
+ * Get user ID
184
+ */
185
+ getUserId() {
186
+ return config.get("userId");
187
+ },
188
+ /**
189
+ * Set user ID
190
+ */
191
+ setUserId(userId) {
192
+ config.set("userId", userId);
193
+ },
194
+ /**
195
+ * Check if user is authenticated
196
+ */
197
+ isAuthenticated() {
198
+ return !!this.getToken();
199
+ },
200
+ /**
201
+ * Check if project is configured
202
+ */
203
+ hasProject() {
204
+ return !!this.getProject();
205
+ },
206
+ /**
207
+ * Get all configuration
208
+ */
209
+ getAll() {
210
+ return {
211
+ apiUrl: this.getApiUrl(),
212
+ token: this.getToken() ? "***" : null,
213
+ project: this.getProject(),
214
+ userId: this.getUserId()
215
+ };
216
+ },
217
+ /**
218
+ * Clear all configuration
219
+ */
220
+ clearAll() {
221
+ config.clear();
222
+ },
223
+ /**
224
+ * Check if using a project token (turbo_xxx format)
225
+ */
226
+ isProjectToken() {
227
+ const token = this.getToken();
228
+ return !!token && token.startsWith("turbo_");
229
+ },
230
+ /**
231
+ * Show current configuration
232
+ */
233
+ show() {
234
+ const data = this.getAll();
235
+ const isProjectToken2 = this.isProjectToken();
236
+ logger.header("Configuration");
237
+ logger.table({
238
+ "API URL": data.apiUrl,
239
+ Token: data.token || "Not set",
240
+ "Token Type": isProjectToken2 ? "Project Token" : data.token ? "User Token" : "N/A",
241
+ Project: data.project || (isProjectToken2 ? "(from token)" : "Not set"),
242
+ "User ID": data.userId || (isProjectToken2 ? "(N/A for project token)" : "Not set")
243
+ });
244
+ }
245
+ };
246
+
247
+ // src/utils/update-check.ts
248
+ import chalk2 from "chalk";
249
+ import { createRequire } from "module";
250
+ import { dirname, join } from "path";
251
+ import { fileURLToPath } from "url";
252
+ import { existsSync } from "fs";
253
+ function loadPackageJson() {
254
+ const require2 = createRequire(import.meta.url);
255
+ const __dirname = dirname(fileURLToPath(import.meta.url));
256
+ const paths = [
257
+ join(__dirname, "../../package.json"),
258
+ // From src/utils/
259
+ join(__dirname, "../package.json")
260
+ // From dist/
261
+ ];
262
+ for (const path of paths) {
263
+ if (existsSync(path)) {
264
+ return require2(path);
265
+ }
266
+ }
267
+ return { version: "0.0.0", name: "@turboops/cli" };
268
+ }
269
+ var pkg = loadPackageJson();
270
+ async function checkForUpdates() {
271
+ try {
272
+ const response = await fetch(
273
+ `https://registry.npmjs.org/${pkg.name}/latest`,
274
+ {
275
+ signal: AbortSignal.timeout(3e3)
276
+ // 3 second timeout
277
+ }
278
+ );
279
+ if (!response.ok) {
280
+ return;
281
+ }
282
+ const data = await response.json();
283
+ const latestVersion = data.version;
284
+ const currentVersion = pkg.version;
285
+ if (isNewerVersion(latestVersion, currentVersion)) {
286
+ showUpdateNotice(currentVersion, latestVersion);
287
+ }
288
+ } catch {
289
+ }
290
+ }
291
+ function isNewerVersion(version1, version2) {
292
+ const v1Parts = version1.split(".").map(Number);
293
+ const v2Parts = version2.split(".").map(Number);
294
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
295
+ const v1 = v1Parts[i] || 0;
296
+ const v2 = v2Parts[i] || 0;
297
+ if (v1 > v2) return true;
298
+ if (v1 < v2) return false;
299
+ }
300
+ return false;
301
+ }
302
+ function showUpdateNotice(currentVersion, latestVersion) {
303
+ console.log();
304
+ console.log(chalk2.yellow("\u2500".repeat(50)));
305
+ console.log(
306
+ chalk2.yellow(" Update available: ") + chalk2.gray(currentVersion) + chalk2.yellow(" \u2192 ") + chalk2.green(latestVersion)
307
+ );
308
+ console.log(
309
+ chalk2.yellow(" Run ") + chalk2.cyan(`npm update -g ${pkg.name}`) + chalk2.yellow(" to update")
310
+ );
311
+ console.log(chalk2.yellow("\u2500".repeat(50)));
312
+ }
313
+ function getCurrentVersion() {
314
+ return pkg.version;
315
+ }
316
+ function getPackageName() {
317
+ return pkg.name;
318
+ }
319
+
320
+ // src/commands/login.ts
321
+ import { Command } from "commander";
322
+ import prompts from "prompts";
323
+
324
+ // src/services/api.ts
325
+ function isProjectToken(token) {
326
+ return token.startsWith("turbo_");
327
+ }
328
+ var cachedProjectInfo = null;
329
+ var apiClient = {
330
+ /**
331
+ * Make an API request
332
+ */
333
+ async request(method, path, body) {
334
+ const apiUrl = configService.getApiUrl();
335
+ const token = configService.getToken();
336
+ if (!token) {
337
+ return {
338
+ error: "Not authenticated. Run `turbo login` or set TURBOOPS_TOKEN.",
339
+ status: 401
340
+ };
341
+ }
342
+ const url = `${apiUrl}${path}`;
343
+ const headers = {
344
+ "Content-Type": "application/json",
345
+ Authorization: `Bearer ${token}`
346
+ };
347
+ try {
348
+ const fetchOptions = {
349
+ method,
350
+ headers
351
+ };
352
+ if (method !== "GET" && body) {
353
+ fetchOptions.body = JSON.stringify(body);
354
+ }
355
+ const response = await fetch(url, fetchOptions);
356
+ const data = await response.json().catch(() => null);
357
+ if (!response.ok) {
358
+ const errorData = data;
359
+ return {
360
+ error: errorData?.message || `HTTP ${response.status}`,
361
+ status: response.status
362
+ };
363
+ }
364
+ return { data, status: response.status };
365
+ } catch (error) {
366
+ const message = error instanceof Error ? error.message : "Network error";
367
+ return { error: message, status: 0 };
368
+ }
369
+ },
370
+ /**
371
+ * Check if using a project token
372
+ */
373
+ isUsingProjectToken() {
374
+ const token = configService.getToken();
375
+ return !!token && isProjectToken(token);
376
+ },
377
+ /**
378
+ * Validate project token and get project info
379
+ * Returns cached result if already validated
380
+ */
381
+ async validateProjectToken() {
382
+ const token = configService.getToken();
383
+ if (!token) {
384
+ return { error: "No token configured", status: 401 };
385
+ }
386
+ if (!isProjectToken(token)) {
387
+ return { error: "Not a project token", status: 400 };
388
+ }
389
+ if (cachedProjectInfo) {
390
+ return {
391
+ data: {
392
+ valid: true,
393
+ projectId: cachedProjectInfo.projectId,
394
+ projectSlug: cachedProjectInfo.projectSlug,
395
+ projectName: cachedProjectInfo.projectSlug
396
+ },
397
+ status: 200
398
+ };
399
+ }
400
+ const apiUrl = configService.getApiUrl();
401
+ try {
402
+ const response = await fetch(`${apiUrl}/deployment/tokens/validate`, {
403
+ method: "POST",
404
+ headers: { "Content-Type": "application/json" },
405
+ body: JSON.stringify({ token })
406
+ });
407
+ const data = await response.json();
408
+ if (!data.valid) {
409
+ return { error: "Invalid or expired token", status: 401 };
410
+ }
411
+ cachedProjectInfo = {
412
+ projectId: data.projectId,
413
+ projectSlug: data.projectSlug
414
+ };
415
+ return {
416
+ data: {
417
+ valid: true,
418
+ projectId: data.projectId,
419
+ projectSlug: data.projectSlug,
420
+ projectName: data.projectName || data.projectSlug
421
+ },
422
+ status: 200
423
+ };
424
+ } catch (error) {
425
+ const message = error instanceof Error ? error.message : "Network error";
426
+ return { error: message, status: 0 };
427
+ }
428
+ },
429
+ /**
430
+ * Get project info (works with both user and project tokens)
431
+ */
432
+ async getProjectInfo() {
433
+ if (this.isUsingProjectToken()) {
434
+ const validation = await this.validateProjectToken();
435
+ if (validation.error) {
436
+ return { error: validation.error, status: validation.status };
437
+ }
438
+ return this.getProject(validation.data.projectSlug);
439
+ }
440
+ const projectSlug = configService.getProject();
441
+ if (!projectSlug) {
442
+ return {
443
+ error: "No project configured. Run `turbo project use <slug>` or use a project token.",
444
+ status: 400
445
+ };
446
+ }
447
+ return this.getProject(projectSlug);
448
+ },
449
+ /**
450
+ * Login and get token
451
+ */
452
+ async login(email, password) {
453
+ const apiUrl = configService.getApiUrl();
454
+ try {
455
+ const response = await fetch(`${apiUrl}/user/login`, {
456
+ method: "POST",
457
+ headers: { "Content-Type": "application/json" },
458
+ body: JSON.stringify({ email, password })
459
+ });
460
+ const data = await response.json();
461
+ if (!response.ok) {
462
+ const errorData = data;
463
+ return {
464
+ error: errorData?.message || "Login failed",
465
+ status: response.status
466
+ };
467
+ }
468
+ return { data, status: response.status };
469
+ } catch (error) {
470
+ const message = error instanceof Error ? error.message : "Network error";
471
+ return { error: message, status: 0 };
472
+ }
473
+ },
474
+ /**
475
+ * Get current user
476
+ */
477
+ async whoami() {
478
+ return this.request("GET", "/user/me");
479
+ },
480
+ /**
481
+ * Get project by slug
482
+ */
483
+ async getProject(slug) {
484
+ return this.request("GET", `/deployment/projects/by-slug/${slug}`);
485
+ },
486
+ /**
487
+ * Get environments for project
488
+ */
489
+ async getEnvironments(projectId) {
490
+ return this.request(
491
+ "GET",
492
+ `/deployment/environments?projectId=${projectId}`
493
+ );
494
+ },
495
+ /**
496
+ * Get environment by slug
497
+ */
498
+ async getEnvironmentBySlug(projectId, slug) {
499
+ return this.request(
500
+ "GET",
501
+ `/deployment/environments/by-slug/${projectId}/${slug}`
502
+ );
503
+ },
504
+ /**
505
+ * Trigger deployment
506
+ */
507
+ async deploy(environmentId, imageTag) {
508
+ return this.request("POST", `/deployment/deployments`, {
509
+ environmentId,
510
+ imageTag
511
+ });
512
+ },
513
+ /**
514
+ * Get deployment status
515
+ */
516
+ async getDeploymentStatus(deploymentId) {
517
+ return this.request("GET", `/deployment/deployments/${deploymentId}`);
518
+ },
519
+ /**
520
+ * Rollback deployment
521
+ */
522
+ async rollback(environmentId, targetDeploymentId) {
523
+ return this.request("POST", `/deployment/deployments/rollback`, {
524
+ environmentId,
525
+ targetDeploymentId
526
+ });
527
+ },
528
+ /**
529
+ * Restart containers
530
+ */
531
+ async restart(environmentId) {
532
+ return this.request(
533
+ "POST",
534
+ `/deployment/environments/${environmentId}/restart`
535
+ );
536
+ },
537
+ /**
538
+ * Stop containers
539
+ */
540
+ async stop(environmentId) {
541
+ return this.request(
542
+ "POST",
543
+ `/deployment/environments/${environmentId}/stop`
544
+ );
545
+ },
546
+ /**
547
+ * Wake containers
548
+ */
549
+ async wake(environmentId) {
550
+ return this.request(
551
+ "POST",
552
+ `/deployment/environments/${environmentId}/wake`
553
+ );
554
+ },
555
+ /**
556
+ * Get deployment logs
557
+ */
558
+ async getLogs(deploymentId) {
559
+ return this.request("GET", `/deployment/deployments/${deploymentId}/logs`);
560
+ },
561
+ /**
562
+ * Get environment variables
563
+ */
564
+ async getEnvVars(environmentId) {
565
+ return this.request(
566
+ "GET",
567
+ `/deployment/environments/${environmentId}/env-vars`
568
+ );
569
+ },
570
+ /**
571
+ * Set environment variable
572
+ */
573
+ async setEnvVar(environmentId, key, value, secret) {
574
+ return this.request(
575
+ "PUT",
576
+ `/deployment/environments/${environmentId}/env-vars`,
577
+ {
578
+ key,
579
+ value,
580
+ secret
581
+ }
582
+ );
583
+ },
584
+ /**
585
+ * Delete environment variable
586
+ */
587
+ async deleteEnvVar(environmentId, key) {
588
+ return this.request(
589
+ "DELETE",
590
+ `/deployment/environments/${environmentId}/env-vars/${key}`
591
+ );
592
+ },
593
+ /**
594
+ * Wait for deployment to complete
595
+ */
596
+ async waitForDeployment(deploymentId, options) {
597
+ const timeout = options?.timeout || 6e5;
598
+ const pollInterval = options?.pollInterval || 3e3;
599
+ const startTime = Date.now();
600
+ while (Date.now() - startTime < timeout) {
601
+ const { data, error } = await this.getDeploymentStatus(deploymentId);
602
+ if (error) {
603
+ throw new Error(error);
604
+ }
605
+ if (!data) {
606
+ throw new Error("No deployment data received");
607
+ }
608
+ if (options?.onProgress) {
609
+ options.onProgress(data);
610
+ }
611
+ if (["running", "failed", "stopped", "rolled_back"].includes(data.status)) {
612
+ return data;
613
+ }
614
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
615
+ }
616
+ throw new Error(`Deployment timeout after ${timeout / 1e3} seconds`);
617
+ }
618
+ };
619
+
620
+ // src/utils/spinner.ts
621
+ import ora from "ora";
622
+ function createSpinner(text) {
623
+ return ora({
624
+ text,
625
+ spinner: "dots"
626
+ });
627
+ }
628
+ async function withSpinner(text, fn, options) {
629
+ const spinner = createSpinner(text);
630
+ spinner.start();
631
+ try {
632
+ const result = await fn();
633
+ spinner.succeed(options?.successText || text);
634
+ return result;
635
+ } catch (error) {
636
+ spinner.fail(options?.failText || `Failed: ${text}`);
637
+ throw error;
638
+ }
639
+ }
640
+
641
+ // src/commands/login.ts
642
+ var loginCommand = new Command("login").description("Authenticate with TurboOps API").option("-t, --token <token>", "Use API token directly").action(async (options) => {
643
+ logger.header("TurboOps Login");
644
+ if (options.token) {
645
+ configService.setToken(options.token);
646
+ const { data: data2, error: error2 } = await withSpinner(
647
+ "Verifying token...",
648
+ () => apiClient.whoami()
649
+ );
650
+ if (error2 || !data2) {
651
+ configService.clearToken();
652
+ logger.error(`Authentication failed: ${error2 || "Invalid token"}`);
653
+ process.exit(10 /* AUTH_ERROR */);
654
+ }
655
+ configService.setUserId(data2.id);
656
+ addJsonData({
657
+ authenticated: true,
658
+ user: { id: data2.id, email: data2.email }
659
+ });
660
+ logger.success(`Authenticated as ${data2.email}`);
661
+ return;
662
+ }
663
+ const response = await prompts([
664
+ {
665
+ type: "text",
666
+ name: "email",
667
+ message: "Email:",
668
+ validate: (value) => value.includes("@") || "Please enter a valid email"
669
+ },
670
+ {
671
+ type: "password",
672
+ name: "password",
673
+ message: "Password:"
674
+ }
675
+ ]);
676
+ if (!response.email || !response.password) {
677
+ logger.warning("Login cancelled");
678
+ addJsonData({ authenticated: false, cancelled: true });
679
+ return;
680
+ }
681
+ const { data, error } = await withSpinner(
682
+ "Logging in...",
683
+ () => apiClient.login(response.email, response.password)
684
+ );
685
+ if (error || !data) {
686
+ logger.error(`Login failed: ${error || "Unknown error"}`);
687
+ addJsonData({ authenticated: false, error: error || "Unknown error" });
688
+ process.exit(10 /* AUTH_ERROR */);
689
+ }
690
+ configService.setToken(data.token);
691
+ configService.setUserId(data.user.id);
692
+ addJsonData({
693
+ authenticated: true,
694
+ user: { id: data.user.id, email: data.user.email }
695
+ });
696
+ logger.success(`Logged in as ${data.user.email}`);
697
+ logger.newline();
698
+ logger.info("Your credentials have been saved.");
699
+ logger.info("Run `turbo whoami` to verify your authentication.");
700
+ });
701
+ var logoutCommand = new Command("logout").description("Remove stored authentication").action(() => {
702
+ configService.clearToken();
703
+ addJsonData({ loggedOut: true });
704
+ logger.success("Logged out successfully");
705
+ });
706
+ var whoamiCommand = new Command("whoami").description("Show current authenticated user").action(async () => {
707
+ if (!configService.isAuthenticated()) {
708
+ logger.error("Not authenticated. Run `turbo login` first.");
709
+ addJsonData({ authenticated: false });
710
+ process.exit(10 /* AUTH_ERROR */);
711
+ }
712
+ const { data, error } = await withSpinner(
713
+ "Fetching user info...",
714
+ () => apiClient.whoami()
715
+ );
716
+ if (error || !data) {
717
+ logger.error(`Failed to fetch user info: ${error || "Unknown error"}`);
718
+ addJsonData({ authenticated: false, error: error || "Unknown error" });
719
+ process.exit(13 /* API_ERROR */);
720
+ }
721
+ addJsonData({
722
+ authenticated: true,
723
+ user: {
724
+ id: data.id,
725
+ email: data.email,
726
+ name: data.name || null
727
+ }
728
+ });
729
+ logger.header("Current User");
730
+ logger.table({
731
+ ID: data.id,
732
+ Email: data.email,
733
+ Name: data.name || "-"
734
+ });
735
+ });
736
+
737
+ // src/commands/deploy.ts
738
+ import { Command as Command2 } from "commander";
739
+
740
+ // src/utils/guards.ts
741
+ async function requireAuth() {
742
+ if (!configService.isAuthenticated()) {
743
+ logger.error("Not authenticated. Run `turbo login` or set TURBOOPS_TOKEN.");
744
+ process.exit(10 /* AUTH_ERROR */);
745
+ }
746
+ if (apiClient.isUsingProjectToken()) {
747
+ const { error } = await apiClient.validateProjectToken();
748
+ if (error) {
749
+ logger.error(`Invalid project token: ${error}`);
750
+ process.exit(10 /* AUTH_ERROR */);
751
+ }
752
+ }
753
+ }
754
+ async function requireProject() {
755
+ if (apiClient.isUsingProjectToken()) {
756
+ const { data, error } = await apiClient.validateProjectToken();
757
+ if (error || !data) {
758
+ logger.error(`Invalid project token: ${error || "Unknown error"}`);
759
+ process.exit(10 /* AUTH_ERROR */);
760
+ }
761
+ return data.projectSlug;
762
+ }
763
+ const projectSlug = configService.getProject();
764
+ if (!projectSlug) {
765
+ logger.error(
766
+ "No project configured. Run `turbo init` or use --project flag, or use a project token."
767
+ );
768
+ process.exit(11 /* CONFIG_ERROR */);
769
+ }
770
+ return projectSlug;
771
+ }
772
+ async function requireAuthAndProject() {
773
+ await requireAuth();
774
+ return requireProject();
775
+ }
776
+ async function fetchProject(projectSlug, showSpinner = true) {
777
+ const fetch2 = () => apiClient.getProject(projectSlug);
778
+ const { data, error } = showSpinner ? await withSpinner("Fetching project...", fetch2) : await fetch2();
779
+ if (error || !data) {
780
+ logger.error(`Failed to fetch project: ${error || "Not found"}`);
781
+ process.exit(12 /* NOT_FOUND */);
782
+ }
783
+ return data;
784
+ }
785
+ async function fetchEnvironment(projectId, environmentSlug, showSpinner = true) {
786
+ const fetch2 = () => apiClient.getEnvironmentBySlug(projectId, environmentSlug);
787
+ const { data, error } = showSpinner ? await withSpinner("Fetching environment...", fetch2) : await fetch2();
788
+ if (error || !data) {
789
+ logger.error(`Environment "${environmentSlug}" not found.`);
790
+ const { data: envs } = await apiClient.getEnvironments(projectId);
791
+ if (envs && envs.length > 0) {
792
+ logger.info("Available environments:");
793
+ logger.list(envs.map((e) => `${e.slug} (${e.name})`));
794
+ }
795
+ process.exit(12 /* NOT_FOUND */);
796
+ }
797
+ return data;
798
+ }
799
+ async function getCommandContext(showSpinner = true) {
800
+ const projectSlug = await requireAuthAndProject();
801
+ const project = await fetchProject(projectSlug, showSpinner);
802
+ return { projectSlug, project };
803
+ }
804
+ async function getCommandContextWithEnvironment(environmentSlug, showSpinner = true) {
805
+ const { projectSlug, project } = await getCommandContext(showSpinner);
806
+ const environment = await fetchEnvironment(
807
+ project.id,
808
+ environmentSlug,
809
+ showSpinner
810
+ );
811
+ return { projectSlug, project, environment };
812
+ }
813
+
814
+ // src/commands/deploy.ts
815
+ import chalk3 from "chalk";
816
+ var deployCommand = new Command2("deploy").description("Deploy to an environment").argument(
817
+ "<environment>",
818
+ "Environment slug (e.g., production, staging, dev)"
819
+ ).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) => {
820
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
821
+ logger.header(`Deploying to ${environment}`);
822
+ const { data: deployment, error: deployError } = await withSpinner(
823
+ `Starting deployment...`,
824
+ () => apiClient.deploy(env.id, options.image)
825
+ );
826
+ if (deployError || !deployment) {
827
+ logger.error(
828
+ `Failed to start deployment: ${deployError || "Unknown error"}`
829
+ );
830
+ process.exit(13 /* API_ERROR */);
831
+ }
832
+ logger.success(`Deployment started: ${deployment.id}`);
833
+ addJsonData({ deploymentId: deployment.id });
834
+ if (!options.wait) {
835
+ logger.info(`View deployment status: turbo status ${environment}`);
836
+ return;
837
+ }
838
+ logger.newline();
839
+ const spinner = createSpinner("Deploying...");
840
+ spinner.start();
841
+ try {
842
+ const finalStatus = await apiClient.waitForDeployment(deployment.id, {
843
+ timeout: parseInt(options.timeout),
844
+ onProgress: (status) => {
845
+ spinner.text = getStatusText(status);
846
+ }
847
+ });
848
+ if (finalStatus.status === "running") {
849
+ spinner.succeed(`Deployment successful!`);
850
+ const resultData = {
851
+ environment: env.name,
852
+ environmentSlug: env.slug,
853
+ imageTag: finalStatus.imageTag,
854
+ status: finalStatus.status,
855
+ healthyContainers: finalStatus.healthStatus?.healthy || 0,
856
+ totalContainers: finalStatus.healthStatus?.total || 0,
857
+ domain: env.domain
858
+ };
859
+ addJsonData(resultData);
860
+ logger.newline();
861
+ logger.table({
862
+ Environment: env.name,
863
+ Image: finalStatus.imageTag,
864
+ Status: chalk3.green(finalStatus.status),
865
+ Health: `${finalStatus.healthStatus?.healthy || 0}/${finalStatus.healthStatus?.total || 0} healthy`
866
+ });
867
+ logger.newline();
868
+ logger.info(`View at: https://${env.domain}`);
869
+ } else if (finalStatus.status === "failed") {
870
+ spinner.fail("Deployment failed!");
871
+ addJsonData({ status: "failed", error: finalStatus.errorMessage });
872
+ logger.error(finalStatus.errorMessage || "Unknown error");
873
+ process.exit(3 /* HEALTH_CHECK_FAILED */);
874
+ } else {
875
+ spinner.warn(`Deployment ended with status: ${finalStatus.status}`);
876
+ addJsonData({ status: finalStatus.status });
877
+ process.exit(1 /* ERROR */);
878
+ }
879
+ } catch (error) {
880
+ spinner.fail("Deployment failed!");
881
+ const message = error instanceof Error ? error.message : "Unknown error";
882
+ addJsonData({ error: message });
883
+ logger.error(message);
884
+ if (message.includes("timeout")) {
885
+ process.exit(2 /* TIMEOUT */);
886
+ }
887
+ process.exit(1 /* ERROR */);
888
+ }
889
+ });
890
+ 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) => {
891
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
892
+ logger.header(`Rolling back ${environment}`);
893
+ const { data, error } = await withSpinner(
894
+ "Starting rollback...",
895
+ () => apiClient.rollback(env.id, options.to)
896
+ );
897
+ if (error || !data) {
898
+ logger.error(`Rollback failed: ${error || "Unknown error"}`);
899
+ process.exit(13 /* API_ERROR */);
900
+ }
901
+ addJsonData({
902
+ status: "rollback_initiated",
903
+ imageTag: data.imageTag,
904
+ deploymentId: data.id
905
+ });
906
+ logger.success(`Rollback initiated to ${data.imageTag}`);
907
+ logger.info("Run `turbo status` to monitor the rollback.");
908
+ });
909
+ var restartCommand = new Command2("restart").description("Restart containers in an environment").argument("<environment>", "Environment slug").action(async (environment) => {
910
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
911
+ const { error } = await withSpinner(
912
+ "Restarting containers...",
913
+ () => apiClient.restart(env.id)
914
+ );
915
+ if (error) {
916
+ logger.error(`Restart failed: ${error}`);
917
+ process.exit(13 /* API_ERROR */);
918
+ }
919
+ addJsonData({ status: "restarting", environment: env.slug });
920
+ logger.success("Containers are being restarted");
921
+ });
922
+ var stopCommand = new Command2("stop").description("Stop containers in an environment").argument("<environment>", "Environment slug").action(async (environment) => {
923
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
924
+ const { error } = await withSpinner(
925
+ "Stopping containers...",
926
+ () => apiClient.stop(env.id)
927
+ );
928
+ if (error) {
929
+ logger.error(`Stop failed: ${error}`);
930
+ process.exit(13 /* API_ERROR */);
931
+ }
932
+ addJsonData({ status: "stopped", environment: env.slug });
933
+ logger.success("Containers have been stopped");
934
+ });
935
+ var wakeCommand = new Command2("wake").description("Wake (start) stopped containers in an environment").argument("<environment>", "Environment slug").action(async (environment) => {
936
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
937
+ const { error } = await withSpinner(
938
+ "Starting containers...",
939
+ () => apiClient.wake(env.id)
940
+ );
941
+ if (error) {
942
+ logger.error(`Wake failed: ${error}`);
943
+ process.exit(13 /* API_ERROR */);
944
+ }
945
+ addJsonData({ status: "starting", environment: env.slug });
946
+ logger.success("Containers are starting");
947
+ });
948
+ function getStatusText(status) {
949
+ switch (status.status) {
950
+ case "pending":
951
+ return "Waiting for deployment to start...";
952
+ case "deploying":
953
+ return `Deploying ${status.imageTag}...`;
954
+ case "running":
955
+ return "Deployment complete!";
956
+ case "failed":
957
+ return "Deployment failed";
958
+ default:
959
+ return `Status: ${status.status}`;
960
+ }
961
+ }
962
+
963
+ // src/commands/status.ts
964
+ import { Command as Command3 } from "commander";
965
+ import chalk4 from "chalk";
966
+ var statusCommand = new Command3("status").description("Show deployment status").argument(
967
+ "[environment]",
968
+ "Environment slug (optional, shows all if not specified)"
969
+ ).action(async (environment) => {
970
+ const { project } = await getCommandContext();
971
+ logger.header(`Status: ${project.name}`);
972
+ const { data: environments, error: envError } = await apiClient.getEnvironments(project.id);
973
+ if (envError || !environments) {
974
+ logger.error(
975
+ `Failed to fetch environments: ${envError || "Unknown error"}`
976
+ );
977
+ process.exit(13 /* API_ERROR */);
978
+ }
979
+ const envsToShow = environment ? environments.filter((e) => e.slug === environment) : environments;
980
+ if (envsToShow.length === 0) {
981
+ if (environment) {
982
+ logger.error(`Environment "${environment}" not found.`);
983
+ logger.info("Available environments:");
984
+ logger.list(environments.map((e) => `${e.slug} (${e.name})`));
985
+ addJsonData({
986
+ error: `Environment "${environment}" not found`,
987
+ availableEnvironments: environments.map((e) => ({
988
+ slug: e.slug,
989
+ name: e.name
990
+ }))
991
+ });
992
+ } else {
993
+ logger.warning("No environments configured for this project.");
994
+ addJsonData({ environments: [] });
995
+ }
996
+ process.exit(12 /* NOT_FOUND */);
997
+ }
998
+ addJsonData({
999
+ project: { name: project.name, slug: project.slug },
1000
+ environments: envsToShow.map((e) => ({
1001
+ name: e.name,
1002
+ slug: e.slug,
1003
+ type: e.type,
1004
+ status: e.status,
1005
+ domain: e.domain
1006
+ }))
1007
+ });
1008
+ for (const env of envsToShow) {
1009
+ logger.newline();
1010
+ console.log(chalk4.bold(env.name) + ` (${env.slug})`);
1011
+ console.log(chalk4.gray("\u2500".repeat(40)));
1012
+ const statusColor = getStatusColor(env.status);
1013
+ logger.table({
1014
+ Status: statusColor(env.status),
1015
+ Type: env.type,
1016
+ Domain: chalk4.cyan(env.domain)
1017
+ });
1018
+ }
1019
+ });
1020
+ var configCommand = new Command3("config").description("Show current configuration").action(() => {
1021
+ const config2 = configService.getAll();
1022
+ addJsonData({ config: config2 });
1023
+ configService.show();
1024
+ });
1025
+ function getStatusColor(status) {
1026
+ switch (status) {
1027
+ case "healthy":
1028
+ case "running":
1029
+ return chalk4.green;
1030
+ case "deploying":
1031
+ return chalk4.yellow;
1032
+ case "failed":
1033
+ case "stopped":
1034
+ return chalk4.red;
1035
+ default:
1036
+ return chalk4.gray;
1037
+ }
1038
+ }
1039
+
1040
+ // src/commands/env.ts
1041
+ import { Command as Command4 } from "commander";
1042
+ import chalk5 from "chalk";
1043
+ 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) => {
1044
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
1045
+ const { data: envVars, error } = await withSpinner(
1046
+ "Fetching environment variables...",
1047
+ () => apiClient.getEnvVars(env.id)
1048
+ );
1049
+ if (error || !envVars) {
1050
+ logger.error(
1051
+ `Failed to fetch environment variables: ${error || "Unknown error"}`
1052
+ );
1053
+ process.exit(13 /* API_ERROR */);
1054
+ }
1055
+ addJsonData({
1056
+ environment: env.name,
1057
+ environmentSlug: env.slug,
1058
+ variables: envVars.map((v) => ({
1059
+ key: v.key,
1060
+ secret: v.secret
1061
+ }))
1062
+ });
1063
+ logger.header(`Environment Variables: ${env.name}`);
1064
+ if (envVars.length === 0) {
1065
+ logger.warning("No environment variables configured.");
1066
+ return;
1067
+ }
1068
+ for (const v of envVars) {
1069
+ const icon = v.secret ? chalk5.yellow("\u{1F512}") : chalk5.gray(" ");
1070
+ const value = v.secret && !options.reveal ? chalk5.gray("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022") : chalk5.green("(set)");
1071
+ console.log(`${icon} ${chalk5.bold(v.key)} = ${value}`);
1072
+ }
1073
+ logger.newline();
1074
+ logger.info(
1075
+ "Use `turbo env <environment> set KEY=value` to add/update variables."
1076
+ );
1077
+ logger.info("Use `turbo env <environment> unset KEY` to remove variables.");
1078
+ });
1079
+ 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) => {
1080
+ const environment = cmd.parent?.args[0];
1081
+ if (!environment) {
1082
+ logger.error("Environment not specified");
1083
+ process.exit(14 /* VALIDATION_ERROR */);
1084
+ }
1085
+ const [key, ...valueParts] = keyValue.split("=");
1086
+ const value = valueParts.join("=");
1087
+ if (!key || !value) {
1088
+ logger.error("Invalid format. Use: KEY=value");
1089
+ process.exit(14 /* VALIDATION_ERROR */);
1090
+ }
1091
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
1092
+ const { error } = await withSpinner(
1093
+ `Setting ${key}...`,
1094
+ () => apiClient.setEnvVar(env.id, key, value, options.secret || false)
1095
+ );
1096
+ if (error) {
1097
+ logger.error(`Failed to set variable: ${error}`);
1098
+ process.exit(13 /* API_ERROR */);
1099
+ }
1100
+ addJsonData({
1101
+ action: "set",
1102
+ key,
1103
+ secret: options.secret || false,
1104
+ environment: env.slug
1105
+ });
1106
+ logger.success(`Set ${key}${options.secret ? " (secret)" : ""}`);
1107
+ logger.info("Changes will take effect on the next deployment.");
1108
+ });
1109
+ var envUnsetCommand = new Command4("unset").description("Remove an environment variable").argument("<key>", "Variable key to remove").action(async (key, _options, cmd) => {
1110
+ const environment = cmd.parent?.args[0];
1111
+ if (!environment) {
1112
+ logger.error("Environment not specified");
1113
+ process.exit(14 /* VALIDATION_ERROR */);
1114
+ }
1115
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
1116
+ const { error } = await withSpinner(
1117
+ `Removing ${key}...`,
1118
+ () => apiClient.deleteEnvVar(env.id, key)
1119
+ );
1120
+ if (error) {
1121
+ logger.error(`Failed to remove variable: ${error}`);
1122
+ process.exit(13 /* API_ERROR */);
1123
+ }
1124
+ addJsonData({
1125
+ action: "unset",
1126
+ key,
1127
+ environment: env.slug
1128
+ });
1129
+ logger.success(`Removed ${key}`);
1130
+ logger.info("Changes will take effect on the next deployment.");
1131
+ });
1132
+ envCommand.addCommand(envSetCommand);
1133
+ envCommand.addCommand(envUnsetCommand);
1134
+
1135
+ // src/commands/init.ts
1136
+ import { Command as Command5 } from "commander";
1137
+ import prompts2 from "prompts";
1138
+ import chalk6 from "chalk";
1139
+ var initCommand = new Command5("init").description("Initialize TurboOps project in current directory").action(async () => {
1140
+ logger.header("TurboOps Project Initialization");
1141
+ if (!configService.isAuthenticated()) {
1142
+ logger.warning("Not authenticated. Please log in first.");
1143
+ logger.newline();
1144
+ const { shouldLogin } = await prompts2({
1145
+ type: "confirm",
1146
+ name: "shouldLogin",
1147
+ message: "Would you like to log in now?",
1148
+ initial: true
1149
+ });
1150
+ if (!shouldLogin) {
1151
+ logger.info("Run `turbo login` when ready.");
1152
+ addJsonData({ initialized: false, reason: "not_authenticated" });
1153
+ return;
1154
+ }
1155
+ const credentials = await prompts2([
1156
+ {
1157
+ type: "text",
1158
+ name: "email",
1159
+ message: "Email:",
1160
+ validate: (value) => value.includes("@") || "Please enter a valid email"
1161
+ },
1162
+ {
1163
+ type: "password",
1164
+ name: "password",
1165
+ message: "Password:"
1166
+ }
1167
+ ]);
1168
+ if (!credentials.email || !credentials.password) {
1169
+ logger.warning("Login cancelled");
1170
+ addJsonData({ initialized: false, reason: "login_cancelled" });
1171
+ return;
1172
+ }
1173
+ const { data, error } = await withSpinner(
1174
+ "Logging in...",
1175
+ () => apiClient.login(credentials.email, credentials.password)
1176
+ );
1177
+ if (error || !data) {
1178
+ logger.error(`Login failed: ${error || "Unknown error"}`);
1179
+ addJsonData({
1180
+ initialized: false,
1181
+ reason: "login_failed",
1182
+ error: error || "Unknown error"
1183
+ });
1184
+ process.exit(10 /* AUTH_ERROR */);
1185
+ }
1186
+ configService.setToken(data.token);
1187
+ configService.setUserId(data.user.id);
1188
+ logger.success(`Logged in as ${data.user.email}`);
1189
+ }
1190
+ logger.newline();
1191
+ const { projectSlug } = await prompts2({
1192
+ type: "text",
1193
+ name: "projectSlug",
1194
+ message: "Project slug:",
1195
+ hint: "The slug of your TurboOps project",
1196
+ validate: (value) => value.length > 0 || "Project slug is required"
1197
+ });
1198
+ if (!projectSlug) {
1199
+ logger.warning("Initialization cancelled");
1200
+ addJsonData({ initialized: false, reason: "cancelled" });
1201
+ return;
1202
+ }
1203
+ const { data: project, error: projectError } = await withSpinner(
1204
+ "Verifying project...",
1205
+ () => apiClient.getProject(projectSlug)
1206
+ );
1207
+ if (projectError || !project) {
1208
+ logger.error(
1209
+ `Project "${projectSlug}" not found or you don't have access.`
1210
+ );
1211
+ logger.info(
1212
+ "Make sure the project exists in TurboOps and you have permission to access it."
1213
+ );
1214
+ addJsonData({
1215
+ initialized: false,
1216
+ reason: "project_not_found",
1217
+ projectSlug
1218
+ });
1219
+ process.exit(12 /* NOT_FOUND */);
1220
+ }
1221
+ configService.setProject(projectSlug);
1222
+ logger.newline();
1223
+ logger.success("Project initialized!");
1224
+ logger.newline();
1225
+ logger.header("Project Details");
1226
+ logger.table({
1227
+ Name: project.name,
1228
+ Slug: project.slug,
1229
+ Repository: project.repositoryUrl || "-"
1230
+ });
1231
+ const { data: environments } = await apiClient.getEnvironments(project.id);
1232
+ const envList = environments && environments.length > 0 ? environments.map((env) => ({
1233
+ slug: env.slug,
1234
+ name: env.name,
1235
+ type: env.type
1236
+ })) : [];
1237
+ addJsonData({
1238
+ initialized: true,
1239
+ project: {
1240
+ name: project.name,
1241
+ slug: project.slug,
1242
+ repositoryUrl: project.repositoryUrl || null
1243
+ },
1244
+ environments: envList
1245
+ });
1246
+ if (environments && environments.length > 0) {
1247
+ logger.newline();
1248
+ logger.header("Available Environments");
1249
+ for (const env of environments) {
1250
+ console.log(` ${chalk6.bold(env.slug)} - ${env.name} (${env.type})`);
1251
+ }
1252
+ }
1253
+ logger.newline();
1254
+ logger.header("Next Steps");
1255
+ logger.list([
1256
+ "Run `turbo status` to see all environments",
1257
+ "Run `turbo deploy <environment>` to deploy",
1258
+ "Run `turbo logs <environment>` to view logs"
1259
+ ]);
1260
+ });
1261
+
1262
+ // src/commands/logs.ts
1263
+ import { Command as Command6 } from "commander";
1264
+ import chalk7 from "chalk";
1265
+ import { io } from "socket.io-client";
1266
+ var logsCommand = new Command6("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) => {
1267
+ const { environment: env } = await getCommandContextWithEnvironment(environment);
1268
+ logger.header(`Logs: ${env.name}`);
1269
+ if (options.follow) {
1270
+ if (isJsonMode()) {
1271
+ logger.error("JSON output mode is not supported with --follow flag");
1272
+ process.exit(14 /* VALIDATION_ERROR */);
1273
+ }
1274
+ await streamLogs(env.id, options.service);
1275
+ } else {
1276
+ await fetchHistoricalLogs(
1277
+ env.id,
1278
+ parseInt(options.lines),
1279
+ options.service
1280
+ );
1281
+ }
1282
+ });
1283
+ async function fetchHistoricalLogs(environmentId, lines, service) {
1284
+ logger.info("Fetching logs...");
1285
+ const { data, error } = await apiClient.request(
1286
+ "GET",
1287
+ `/deployment/environments/${environmentId}/logs?lines=${lines}${service ? `&service=${service}` : ""}`
1288
+ );
1289
+ if (error) {
1290
+ logger.error(`Failed to fetch logs: ${error}`);
1291
+ process.exit(13 /* API_ERROR */);
1292
+ }
1293
+ if (!data?.logs || data.logs.length === 0) {
1294
+ logger.info("No logs available.");
1295
+ addJsonData({ logs: [] });
1296
+ return;
1297
+ }
1298
+ addJsonData({
1299
+ logs: data.logs.map((log) => ({
1300
+ timestamp: log.timestamp,
1301
+ level: log.level,
1302
+ message: log.message,
1303
+ step: log.step
1304
+ })),
1305
+ count: data.logs.length
1306
+ });
1307
+ logger.newline();
1308
+ for (const log of data.logs) {
1309
+ printLogEntry(log);
1310
+ }
1311
+ logger.newline();
1312
+ logger.info(`Showing last ${lines} lines. Use -f to follow live logs.`);
1313
+ }
1314
+ async function streamLogs(environmentId, service) {
1315
+ const apiUrl = configService.getApiUrl();
1316
+ const token = configService.getToken();
1317
+ if (!token) {
1318
+ logger.error("Not authenticated");
1319
+ process.exit(10 /* AUTH_ERROR */);
1320
+ }
1321
+ const wsUrl = buildWebSocketUrl(apiUrl);
1322
+ logger.info("Connecting to log stream...");
1323
+ const socket = io(`${wsUrl}/deployments`, {
1324
+ auth: { token },
1325
+ reconnection: true,
1326
+ reconnectionAttempts: 5,
1327
+ reconnectionDelay: 1e3
1328
+ });
1329
+ socket.on("connect", () => {
1330
+ logger.success("Connected to log stream");
1331
+ logger.info("Streaming logs... (Press Ctrl+C to stop)");
1332
+ logger.newline();
1333
+ socket.emit("join:logs", {
1334
+ environmentId,
1335
+ service
1336
+ });
1337
+ });
1338
+ socket.on("log", (entry) => {
1339
+ printLogEntry(entry);
1340
+ });
1341
+ socket.on("logs:batch", (entries) => {
1342
+ for (const entry of entries) {
1343
+ printLogEntry(entry);
1344
+ }
1345
+ });
1346
+ socket.on("error", (error) => {
1347
+ logger.error(`Stream error: ${error.message}`);
1348
+ });
1349
+ socket.on("connect_error", (error) => {
1350
+ logger.error(`Connection error: ${error.message}`);
1351
+ process.exit(15 /* NETWORK_ERROR */);
1352
+ });
1353
+ socket.on("disconnect", (reason) => {
1354
+ if (reason === "io server disconnect") {
1355
+ logger.warning("Disconnected by server");
1356
+ } else {
1357
+ logger.warning(`Disconnected: ${reason}`);
1358
+ }
1359
+ });
1360
+ socket.on("reconnect", () => {
1361
+ logger.info("Reconnected to log stream");
1362
+ socket.emit("join:logs", { environmentId, service });
1363
+ });
1364
+ process.on("SIGINT", () => {
1365
+ logger.newline();
1366
+ logger.info("Stopping log stream...");
1367
+ socket.emit("leave:logs", { environmentId });
1368
+ socket.disconnect();
1369
+ process.exit(0 /* SUCCESS */);
1370
+ });
1371
+ await new Promise(() => {
1372
+ });
1373
+ }
1374
+ function buildWebSocketUrl(apiUrl) {
1375
+ try {
1376
+ const url = new URL(apiUrl);
1377
+ const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
1378
+ return `${wsProtocol}//${url.host}`;
1379
+ } catch {
1380
+ return apiUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:").replace(/\/api\/?$/, "");
1381
+ }
1382
+ }
1383
+ function printLogEntry(log) {
1384
+ const timestamp = formatTimestamp(log.timestamp);
1385
+ const levelColor = getLevelColor(log.level);
1386
+ const levelStr = log.level.toUpperCase().padEnd(5);
1387
+ let output = chalk7.gray(`[${timestamp}]`) + levelColor(` ${levelStr}`);
1388
+ if (log.step) {
1389
+ output += chalk7.cyan(` [${log.step}]`);
1390
+ }
1391
+ output += ` ${log.message}`;
1392
+ logger.raw(output);
1393
+ }
1394
+ function formatTimestamp(timestamp) {
1395
+ try {
1396
+ const date = new Date(timestamp);
1397
+ return date.toISOString().replace("T", " ").slice(0, 19);
1398
+ } catch {
1399
+ return timestamp;
1400
+ }
1401
+ }
1402
+ function getLevelColor(level) {
1403
+ switch (level.toLowerCase()) {
1404
+ case "error":
1405
+ case "fatal":
1406
+ return chalk7.red;
1407
+ case "warn":
1408
+ case "warning":
1409
+ return chalk7.yellow;
1410
+ case "info":
1411
+ return chalk7.blue;
1412
+ case "debug":
1413
+ return chalk7.gray;
1414
+ case "trace":
1415
+ return chalk7.magenta;
1416
+ default:
1417
+ return chalk7.white;
1418
+ }
1419
+ }
1420
+
1421
+ // src/index.ts
1422
+ var VERSION = getCurrentVersion();
1423
+ var shouldCheckUpdate = true;
1424
+ var program = new Command7();
1425
+ 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("--api <url>", "Override API URL").option("--json", "Output as JSON").option("--quiet", "Only show errors").option("--verbose", "Show debug output").option("--no-update-check", "Skip version check");
1426
+ program.hook("preAction", (thisCommand) => {
1427
+ const opts = thisCommand.opts();
1428
+ clearJsonData();
1429
+ if (opts.json) {
1430
+ setOutputMode("json");
1431
+ } else if (opts.quiet) {
1432
+ setOutputMode("quiet");
1433
+ } else {
1434
+ setOutputMode("normal");
1435
+ }
1436
+ if (opts.project) {
1437
+ configService.setProject(opts.project);
1438
+ }
1439
+ if (opts.token) {
1440
+ configService.setToken(opts.token);
1441
+ }
1442
+ if (opts.api) {
1443
+ configService.setApiUrl(opts.api);
1444
+ }
1445
+ if (opts.verbose) {
1446
+ process.env.DEBUG = "true";
1447
+ }
1448
+ shouldCheckUpdate = opts.updateCheck !== false;
1449
+ });
1450
+ program.hook("postAction", async () => {
1451
+ printJsonOutput();
1452
+ if (shouldCheckUpdate && !process.env.CI) {
1453
+ checkForUpdates().catch(() => {
1454
+ });
1455
+ }
1456
+ });
1457
+ program.addCommand(loginCommand);
1458
+ program.addCommand(logoutCommand);
1459
+ program.addCommand(whoamiCommand);
1460
+ program.addCommand(initCommand);
1461
+ program.addCommand(statusCommand);
1462
+ program.addCommand(configCommand);
1463
+ program.addCommand(deployCommand);
1464
+ program.addCommand(rollbackCommand);
1465
+ program.addCommand(restartCommand);
1466
+ program.addCommand(stopCommand);
1467
+ program.addCommand(wakeCommand);
1468
+ program.addCommand(envCommand);
1469
+ program.addCommand(logsCommand);
1470
+ program.command("self-update").description("Update TurboCLI to the latest version").action(async () => {
1471
+ logger.info("Checking for updates...");
1472
+ try {
1473
+ const response = await fetch(
1474
+ `https://registry.npmjs.org/${getPackageName()}/latest`
1475
+ );
1476
+ const data = await response.json();
1477
+ const latestVersion = data.version;
1478
+ if (latestVersion === VERSION) {
1479
+ logger.success(`You are already on the latest version (${VERSION})`);
1480
+ } else {
1481
+ logger.info(`Current version: ${VERSION}`);
1482
+ logger.info(`Latest version: ${latestVersion}`);
1483
+ logger.newline();
1484
+ logger.info("To update TurboCLI, run:");
1485
+ console.log(chalk8.cyan(` npm update -g ${getPackageName()}`));
1486
+ console.log();
1487
+ logger.info("Or if using npx:");
1488
+ console.log(chalk8.cyan(` npx ${getPackageName()}@latest`));
1489
+ }
1490
+ } catch {
1491
+ logger.warning("Could not check for updates");
1492
+ logger.info("To update TurboCLI, run:");
1493
+ console.log(chalk8.cyan(` npm update -g ${getPackageName()}`));
1494
+ }
1495
+ });
1496
+ program.configureHelp({
1497
+ subcommandTerm: (cmd) => cmd.name()
1498
+ });
1499
+ program.showHelpAfterError("(add --help for additional information)");
1500
+ program.parse();
1501
+ if (!process.argv.slice(2).length) {
1502
+ console.log();
1503
+ console.log(chalk8.bold.cyan(" TurboCLI") + chalk8.gray(` v${VERSION}`));
1504
+ console.log(chalk8.gray(" Command line interface for TurboOps deployments"));
1505
+ console.log();
1506
+ program.outputHelp();
1507
+ }