@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/README.md +167 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1507 -0
- package/package.json +56 -0
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
|
+
}
|