@tarout/cli 0.1.2 → 0.1.4
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 +1921 -1183
- package/package.json +62 -60
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
failSpinner,
|
|
4
4
|
startSpinner,
|
|
5
|
+
stopSpinner,
|
|
5
6
|
succeedSpinner,
|
|
6
7
|
updateSpinner
|
|
7
8
|
} from "./chunk-GSKD67K4.js";
|
|
@@ -9,9 +10,78 @@ import {
|
|
|
9
10
|
// src/index.ts
|
|
10
11
|
import { Command } from "commander";
|
|
11
12
|
|
|
12
|
-
// src/
|
|
13
|
-
import
|
|
14
|
-
|
|
13
|
+
// src/commands/apps.ts
|
|
14
|
+
import open from "open";
|
|
15
|
+
|
|
16
|
+
// src/lib/api.ts
|
|
17
|
+
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
|
18
|
+
import superjson from "superjson";
|
|
19
|
+
|
|
20
|
+
// src/lib/config.ts
|
|
21
|
+
import Conf from "conf";
|
|
22
|
+
var config = new Conf({
|
|
23
|
+
projectName: "tarout",
|
|
24
|
+
defaults: {
|
|
25
|
+
currentProfile: "default",
|
|
26
|
+
profiles: {}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
function getConfig() {
|
|
30
|
+
return config.store;
|
|
31
|
+
}
|
|
32
|
+
function getCurrentProfile() {
|
|
33
|
+
const cfg = getConfig();
|
|
34
|
+
return cfg.profiles[cfg.currentProfile] || null;
|
|
35
|
+
}
|
|
36
|
+
function setProfile(name, profile) {
|
|
37
|
+
config.set(`profiles.${name}`, profile);
|
|
38
|
+
}
|
|
39
|
+
function setCurrentProfile(name) {
|
|
40
|
+
config.set("currentProfile", name);
|
|
41
|
+
}
|
|
42
|
+
function clearConfig() {
|
|
43
|
+
config.clear();
|
|
44
|
+
}
|
|
45
|
+
function isLoggedIn() {
|
|
46
|
+
const profile = getCurrentProfile();
|
|
47
|
+
return profile !== null && !!profile.token;
|
|
48
|
+
}
|
|
49
|
+
function getToken() {
|
|
50
|
+
const profile = getCurrentProfile();
|
|
51
|
+
return profile?.token || null;
|
|
52
|
+
}
|
|
53
|
+
function getApiUrl() {
|
|
54
|
+
const profile = getCurrentProfile();
|
|
55
|
+
return profile?.apiUrl || "https://tarout.sa";
|
|
56
|
+
}
|
|
57
|
+
function updateProfile(updates) {
|
|
58
|
+
const cfg = getConfig();
|
|
59
|
+
const currentProfileName = cfg.currentProfile;
|
|
60
|
+
const currentProfile = cfg.profiles[currentProfileName];
|
|
61
|
+
if (currentProfile) {
|
|
62
|
+
config.set(`profiles.${currentProfileName}`, {
|
|
63
|
+
...currentProfile,
|
|
64
|
+
...updates
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/utils/exit-codes.ts
|
|
70
|
+
var ExitCode = {
|
|
71
|
+
SUCCESS: 0,
|
|
72
|
+
GENERAL_ERROR: 1,
|
|
73
|
+
INVALID_ARGUMENTS: 2,
|
|
74
|
+
AUTH_ERROR: 3,
|
|
75
|
+
NOT_FOUND: 4,
|
|
76
|
+
PERMISSION_DENIED: 5,
|
|
77
|
+
// Deployment-specific exit codes
|
|
78
|
+
DEPLOYMENT_FAILED: 10,
|
|
79
|
+
DEPLOYMENT_TIMEOUT: 11,
|
|
80
|
+
BUILD_FAILED: 12
|
|
81
|
+
};
|
|
82
|
+
function exit(code) {
|
|
83
|
+
process.exit(code);
|
|
84
|
+
}
|
|
15
85
|
|
|
16
86
|
// src/utils/json.ts
|
|
17
87
|
function jsonSuccess(data, meta) {
|
|
@@ -36,6 +106,8 @@ function outputJson(response) {
|
|
|
36
106
|
}
|
|
37
107
|
|
|
38
108
|
// src/lib/output.ts
|
|
109
|
+
import chalk from "chalk";
|
|
110
|
+
import Table from "cli-table3";
|
|
39
111
|
var globalOptions = {
|
|
40
112
|
json: false,
|
|
41
113
|
quiet: false,
|
|
@@ -152,174 +224,6 @@ function box(title, content) {
|
|
|
152
224
|
console.log("");
|
|
153
225
|
}
|
|
154
226
|
|
|
155
|
-
// src/commands/auth.ts
|
|
156
|
-
import open from "open";
|
|
157
|
-
|
|
158
|
-
// src/lib/config.ts
|
|
159
|
-
import Conf from "conf";
|
|
160
|
-
var config = new Conf({
|
|
161
|
-
projectName: "tarout",
|
|
162
|
-
defaults: {
|
|
163
|
-
currentProfile: "default",
|
|
164
|
-
profiles: {}
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
function getConfig() {
|
|
168
|
-
return config.store;
|
|
169
|
-
}
|
|
170
|
-
function getCurrentProfile() {
|
|
171
|
-
const cfg = getConfig();
|
|
172
|
-
return cfg.profiles[cfg.currentProfile] || null;
|
|
173
|
-
}
|
|
174
|
-
function setProfile(name, profile) {
|
|
175
|
-
config.set(`profiles.${name}`, profile);
|
|
176
|
-
}
|
|
177
|
-
function setCurrentProfile(name) {
|
|
178
|
-
config.set("currentProfile", name);
|
|
179
|
-
}
|
|
180
|
-
function clearConfig() {
|
|
181
|
-
config.clear();
|
|
182
|
-
}
|
|
183
|
-
function isLoggedIn() {
|
|
184
|
-
const profile = getCurrentProfile();
|
|
185
|
-
return profile !== null && !!profile.token;
|
|
186
|
-
}
|
|
187
|
-
function getToken() {
|
|
188
|
-
const profile = getCurrentProfile();
|
|
189
|
-
return profile?.token || null;
|
|
190
|
-
}
|
|
191
|
-
function getApiUrl() {
|
|
192
|
-
const profile = getCurrentProfile();
|
|
193
|
-
return profile?.apiUrl || "https://tarout.sa";
|
|
194
|
-
}
|
|
195
|
-
function updateProfile(updates) {
|
|
196
|
-
const cfg = getConfig();
|
|
197
|
-
const currentProfileName = cfg.currentProfile;
|
|
198
|
-
const currentProfile = cfg.profiles[currentProfileName];
|
|
199
|
-
if (currentProfile) {
|
|
200
|
-
config.set(`profiles.${currentProfileName}`, {
|
|
201
|
-
...currentProfile,
|
|
202
|
-
...updates
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// src/lib/auth-server.ts
|
|
208
|
-
import express from "express";
|
|
209
|
-
function startAuthServer() {
|
|
210
|
-
return new Promise((resolve) => {
|
|
211
|
-
const app = express();
|
|
212
|
-
let server;
|
|
213
|
-
let callbackResolver;
|
|
214
|
-
let callbackRejecter;
|
|
215
|
-
const callbackPromise = new Promise((res, rej) => {
|
|
216
|
-
callbackResolver = res;
|
|
217
|
-
callbackRejecter = rej;
|
|
218
|
-
});
|
|
219
|
-
app.get("/callback", (req, res) => {
|
|
220
|
-
const {
|
|
221
|
-
token,
|
|
222
|
-
userId,
|
|
223
|
-
userEmail,
|
|
224
|
-
userName,
|
|
225
|
-
organizationId,
|
|
226
|
-
organizationName,
|
|
227
|
-
environmentId,
|
|
228
|
-
environmentName,
|
|
229
|
-
error: error6
|
|
230
|
-
} = req.query;
|
|
231
|
-
if (error6) {
|
|
232
|
-
res.send(`
|
|
233
|
-
<!DOCTYPE html>
|
|
234
|
-
<html>
|
|
235
|
-
<head>
|
|
236
|
-
<title>Tarout CLI - Authentication Failed</title>
|
|
237
|
-
<style>
|
|
238
|
-
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
239
|
-
.container { text-align: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
240
|
-
h1 { color: #dc2626; margin-bottom: 16px; }
|
|
241
|
-
p { color: #666; }
|
|
242
|
-
</style>
|
|
243
|
-
</head>
|
|
244
|
-
<body>
|
|
245
|
-
<div class="container">
|
|
246
|
-
<h1>Authentication Failed</h1>
|
|
247
|
-
<p>${error6}</p>
|
|
248
|
-
<p>You can close this window and try again.</p>
|
|
249
|
-
</div>
|
|
250
|
-
</body>
|
|
251
|
-
</html>
|
|
252
|
-
`);
|
|
253
|
-
callbackRejecter(new Error(String(error6)));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (!token || !userId || !userEmail || !organizationId || !organizationName || !environmentId || !environmentName) {
|
|
257
|
-
res.status(400).send("Missing required parameters");
|
|
258
|
-
callbackRejecter(new Error("Missing required parameters from auth callback"));
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
res.send(`
|
|
262
|
-
<!DOCTYPE html>
|
|
263
|
-
<html>
|
|
264
|
-
<head>
|
|
265
|
-
<title>Tarout CLI - Authenticated</title>
|
|
266
|
-
<style>
|
|
267
|
-
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
268
|
-
.container { text-align: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
269
|
-
h1 { color: #16a34a; margin-bottom: 16px; }
|
|
270
|
-
p { color: #666; }
|
|
271
|
-
.checkmark { font-size: 64px; margin-bottom: 16px; }
|
|
272
|
-
</style>
|
|
273
|
-
</head>
|
|
274
|
-
<body>
|
|
275
|
-
<div class="container">
|
|
276
|
-
<div class="checkmark">\u2713</div>
|
|
277
|
-
<h1>Authenticated!</h1>
|
|
278
|
-
<p>You can close this window and return to the terminal.</p>
|
|
279
|
-
</div>
|
|
280
|
-
</body>
|
|
281
|
-
</html>
|
|
282
|
-
`);
|
|
283
|
-
callbackResolver({
|
|
284
|
-
token: String(token),
|
|
285
|
-
userId: String(userId),
|
|
286
|
-
userEmail: String(userEmail),
|
|
287
|
-
userName: userName ? String(userName) : void 0,
|
|
288
|
-
organizationId: String(organizationId),
|
|
289
|
-
organizationName: String(organizationName),
|
|
290
|
-
environmentId: String(environmentId),
|
|
291
|
-
environmentName: String(environmentName)
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
server = app.listen(0, () => {
|
|
295
|
-
const address = server.address();
|
|
296
|
-
const port = typeof address === "object" && address ? address.port : 0;
|
|
297
|
-
resolve({
|
|
298
|
-
port,
|
|
299
|
-
waitForCallback: () => callbackPromise,
|
|
300
|
-
close: () => server.close()
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
setTimeout(() => {
|
|
304
|
-
callbackRejecter(new Error("Authentication timed out. Please try again."));
|
|
305
|
-
server.close();
|
|
306
|
-
}, 5 * 60 * 1e3);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// src/utils/exit-codes.ts
|
|
311
|
-
var ExitCode = {
|
|
312
|
-
SUCCESS: 0,
|
|
313
|
-
GENERAL_ERROR: 1,
|
|
314
|
-
INVALID_ARGUMENTS: 2,
|
|
315
|
-
AUTH_ERROR: 3,
|
|
316
|
-
NOT_FOUND: 4,
|
|
317
|
-
PERMISSION_DENIED: 5
|
|
318
|
-
};
|
|
319
|
-
function exit(code) {
|
|
320
|
-
process.exit(code);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
227
|
// src/lib/errors.ts
|
|
324
228
|
var CliError = class extends Error {
|
|
325
229
|
constructor(message, code = ExitCode.GENERAL_ERROR, suggestions) {
|
|
@@ -344,15 +248,31 @@ var InvalidArgumentError = class extends CliError {
|
|
|
344
248
|
super(message, ExitCode.INVALID_ARGUMENTS);
|
|
345
249
|
}
|
|
346
250
|
};
|
|
251
|
+
var DeploymentFailedError = class extends CliError {
|
|
252
|
+
constructor(message, deploymentId, errorAnalysis) {
|
|
253
|
+
super(message, ExitCode.DEPLOYMENT_FAILED);
|
|
254
|
+
this.deploymentId = deploymentId;
|
|
255
|
+
this.errorAnalysis = errorAnalysis;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var DeploymentTimeoutError = class extends CliError {
|
|
259
|
+
constructor(message = "Deployment timed out", deploymentId) {
|
|
260
|
+
super(message, ExitCode.DEPLOYMENT_TIMEOUT);
|
|
261
|
+
this.deploymentId = deploymentId;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var BuildFailedError = class extends CliError {
|
|
265
|
+
constructor(message, deploymentId, errorAnalysis) {
|
|
266
|
+
super(message, ExitCode.BUILD_FAILED);
|
|
267
|
+
this.deploymentId = deploymentId;
|
|
268
|
+
this.errorAnalysis = errorAnalysis;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
347
271
|
function handleError(err) {
|
|
348
272
|
if (err instanceof CliError) {
|
|
349
273
|
if (isJsonMode()) {
|
|
350
274
|
outputJson(
|
|
351
|
-
jsonError(
|
|
352
|
-
getErrorCode(err.code),
|
|
353
|
-
err.message,
|
|
354
|
-
err.suggestions
|
|
355
|
-
)
|
|
275
|
+
jsonError(getErrorCode(err.code), err.message, err.suggestions)
|
|
356
276
|
);
|
|
357
277
|
} else {
|
|
358
278
|
error(err.message, err.suggestions);
|
|
@@ -384,7 +304,10 @@ function getErrorCode(exitCode) {
|
|
|
384
304
|
[ExitCode.INVALID_ARGUMENTS]: "INVALID_ARGUMENTS",
|
|
385
305
|
[ExitCode.AUTH_ERROR]: "AUTH_ERROR",
|
|
386
306
|
[ExitCode.NOT_FOUND]: "NOT_FOUND",
|
|
387
|
-
[ExitCode.PERMISSION_DENIED]: "PERMISSION_DENIED"
|
|
307
|
+
[ExitCode.PERMISSION_DENIED]: "PERMISSION_DENIED",
|
|
308
|
+
[ExitCode.DEPLOYMENT_FAILED]: "DEPLOYMENT_FAILED",
|
|
309
|
+
[ExitCode.DEPLOYMENT_TIMEOUT]: "DEPLOYMENT_TIMEOUT",
|
|
310
|
+
[ExitCode.BUILD_FAILED]: "BUILD_FAILED"
|
|
388
311
|
};
|
|
389
312
|
return codes[exitCode] || "ERROR";
|
|
390
313
|
}
|
|
@@ -420,155 +343,288 @@ function similarity(s1, s2) {
|
|
|
420
343
|
}
|
|
421
344
|
return 2 * intersection / (s1.length - 1 + s2.length - 1);
|
|
422
345
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
346
|
+
var ERROR_PATTERNS = [
|
|
347
|
+
{
|
|
348
|
+
patterns: [
|
|
349
|
+
/npm ERR!/i,
|
|
350
|
+
/npm error/i,
|
|
351
|
+
/ERESOLVE/i,
|
|
352
|
+
/Could not resolve dependency/i,
|
|
353
|
+
/peer dep missing/i
|
|
354
|
+
],
|
|
355
|
+
category: "npm_install",
|
|
356
|
+
type: "build_error",
|
|
357
|
+
possibleCauses: [
|
|
358
|
+
"Package version conflicts",
|
|
359
|
+
"Missing peer dependencies",
|
|
360
|
+
"Invalid package.json",
|
|
361
|
+
"Network issues during npm install",
|
|
362
|
+
"Private package without authentication"
|
|
363
|
+
],
|
|
364
|
+
suggestedFixes: [
|
|
365
|
+
"Run `npm install` locally to reproduce the issue",
|
|
366
|
+
"Check for conflicting package versions in package.json",
|
|
367
|
+
"Try deleting package-lock.json and node_modules, then reinstall",
|
|
368
|
+
"Add missing peer dependencies explicitly",
|
|
369
|
+
"Check if private packages are properly authenticated"
|
|
370
|
+
]
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
patterns: [
|
|
374
|
+
/yarn error/i,
|
|
375
|
+
/YN\d{4}/i,
|
|
376
|
+
/error An unexpected error occurred/i
|
|
377
|
+
],
|
|
378
|
+
category: "yarn_install",
|
|
379
|
+
type: "build_error",
|
|
380
|
+
possibleCauses: [
|
|
381
|
+
"Package version conflicts",
|
|
382
|
+
"Corrupted yarn.lock file",
|
|
383
|
+
"Network issues during install",
|
|
384
|
+
"Incompatible yarn version"
|
|
385
|
+
],
|
|
386
|
+
suggestedFixes: [
|
|
387
|
+
"Run `yarn install` locally to reproduce the issue",
|
|
388
|
+
"Delete yarn.lock and node_modules, then reinstall",
|
|
389
|
+
"Check for conflicting resolutions in package.json",
|
|
390
|
+
"Ensure yarn version matches the project requirements"
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
patterns: [/pnpm ERR/i, /ERR_PNPM/i],
|
|
395
|
+
category: "pnpm_install",
|
|
396
|
+
type: "build_error",
|
|
397
|
+
possibleCauses: [
|
|
398
|
+
"Package version conflicts",
|
|
399
|
+
"Incompatible pnpm version",
|
|
400
|
+
"Corrupted pnpm-lock.yaml"
|
|
401
|
+
],
|
|
402
|
+
suggestedFixes: [
|
|
403
|
+
"Run `pnpm install` locally to reproduce the issue",
|
|
404
|
+
"Delete pnpm-lock.yaml and node_modules, then reinstall",
|
|
405
|
+
"Check pnpm version compatibility"
|
|
406
|
+
]
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
patterns: [/bun install/i, /error: .* failed to resolve/i],
|
|
410
|
+
category: "bun_install",
|
|
411
|
+
type: "build_error",
|
|
412
|
+
possibleCauses: [
|
|
413
|
+
"Package resolution failure",
|
|
414
|
+
"Incompatible bun version",
|
|
415
|
+
"Missing dependencies"
|
|
416
|
+
],
|
|
417
|
+
suggestedFixes: [
|
|
418
|
+
"Run `bun install` locally to reproduce the issue",
|
|
419
|
+
"Delete bun.lockb and node_modules, then reinstall",
|
|
420
|
+
"Check bun version compatibility"
|
|
421
|
+
]
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
patterns: [
|
|
425
|
+
/error TS\d+/i,
|
|
426
|
+
/TypeScript error/i,
|
|
427
|
+
/tsc.*error/i,
|
|
428
|
+
/Type .* is not assignable to/i,
|
|
429
|
+
/Cannot find module/i,
|
|
430
|
+
/Property .* does not exist/i
|
|
431
|
+
],
|
|
432
|
+
category: "typescript",
|
|
433
|
+
type: "build_error",
|
|
434
|
+
possibleCauses: [
|
|
435
|
+
"TypeScript compilation errors in source code",
|
|
436
|
+
"Missing type definitions",
|
|
437
|
+
"Incompatible TypeScript version",
|
|
438
|
+
"Strict mode type errors"
|
|
439
|
+
],
|
|
440
|
+
suggestedFixes: [
|
|
441
|
+
"Run `npx tsc --noEmit` locally to see all type errors",
|
|
442
|
+
"Fix the TypeScript errors in the indicated files",
|
|
443
|
+
"Install missing @types/* packages",
|
|
444
|
+
"Check tsconfig.json for strict mode settings"
|
|
445
|
+
]
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
patterns: [
|
|
449
|
+
/COPY failed/i,
|
|
450
|
+
/RUN.*failed/i,
|
|
451
|
+
/failed to build/i,
|
|
452
|
+
/Error building image/i,
|
|
453
|
+
/Dockerfile/i
|
|
454
|
+
],
|
|
455
|
+
category: "docker_build",
|
|
456
|
+
type: "build_error",
|
|
457
|
+
possibleCauses: [
|
|
458
|
+
"Invalid Dockerfile syntax",
|
|
459
|
+
"Missing files in build context",
|
|
460
|
+
"Failed RUN commands",
|
|
461
|
+
"Base image not found"
|
|
462
|
+
],
|
|
463
|
+
suggestedFixes: [
|
|
464
|
+
"Test Docker build locally with `docker build .`",
|
|
465
|
+
"Check that all required files are in the build context",
|
|
466
|
+
"Verify Dockerfile syntax and commands",
|
|
467
|
+
"Ensure base image exists and is accessible"
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
patterns: [
|
|
472
|
+
/build.*failed/i,
|
|
473
|
+
/Build failed/i,
|
|
474
|
+
/exit code 1/i,
|
|
475
|
+
/Command failed/i,
|
|
476
|
+
/Script.*failed/i
|
|
477
|
+
],
|
|
478
|
+
category: "build_script",
|
|
479
|
+
type: "build_error",
|
|
480
|
+
possibleCauses: [
|
|
481
|
+
"Build script error in package.json",
|
|
482
|
+
"Missing build dependencies",
|
|
483
|
+
"Environment variable issues",
|
|
484
|
+
"Build command not found"
|
|
485
|
+
],
|
|
486
|
+
suggestedFixes: [
|
|
487
|
+
"Run `npm run build` locally to reproduce the error",
|
|
488
|
+
"Check package.json build script for errors",
|
|
489
|
+
"Ensure all required environment variables are set",
|
|
490
|
+
"Verify build dependencies are installed"
|
|
491
|
+
]
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
patterns: [
|
|
495
|
+
/out of memory/i,
|
|
496
|
+
/JavaScript heap out of memory/i,
|
|
497
|
+
/FATAL ERROR: .* allocation failed/i,
|
|
498
|
+
/OOMKilled/i
|
|
499
|
+
],
|
|
500
|
+
category: "memory_limit",
|
|
501
|
+
type: "build_error",
|
|
502
|
+
possibleCauses: [
|
|
503
|
+
"Build process requires more memory than allocated",
|
|
504
|
+
"Memory leak in build process",
|
|
505
|
+
"Large dependency tree"
|
|
506
|
+
],
|
|
507
|
+
suggestedFixes: [
|
|
508
|
+
"Increase memory allocation for the build",
|
|
509
|
+
"Optimize build process to use less memory",
|
|
510
|
+
"Consider splitting large bundles",
|
|
511
|
+
"Add NODE_OPTIONS=--max_old_space_size=4096"
|
|
512
|
+
]
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
patterns: [/timed? ?out/i, /deadline exceeded/i, /timeout/i],
|
|
516
|
+
category: "timeout",
|
|
517
|
+
type: "build_error",
|
|
518
|
+
possibleCauses: [
|
|
519
|
+
"Build process took too long",
|
|
520
|
+
"Network timeout during dependency install",
|
|
521
|
+
"Slow external service calls"
|
|
522
|
+
],
|
|
523
|
+
suggestedFixes: [
|
|
524
|
+
"Optimize build process for faster execution",
|
|
525
|
+
"Check for slow network requests during build",
|
|
526
|
+
"Consider caching dependencies",
|
|
527
|
+
"Increase build timeout if possible"
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
patterns: [/permission denied/i, /EACCES/i, /EPERM/i],
|
|
532
|
+
category: "permission",
|
|
533
|
+
type: "build_error",
|
|
534
|
+
possibleCauses: [
|
|
535
|
+
"File permission issues",
|
|
536
|
+
"Attempting to write to read-only locations",
|
|
537
|
+
"Docker user permission mismatch"
|
|
538
|
+
],
|
|
539
|
+
suggestedFixes: [
|
|
540
|
+
"Check file permissions in the project",
|
|
541
|
+
"Ensure Dockerfile uses correct user",
|
|
542
|
+
"Avoid writing to restricted directories"
|
|
543
|
+
]
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
patterns: [
|
|
547
|
+
/ENOTFOUND/i,
|
|
548
|
+
/ECONNREFUSED/i,
|
|
549
|
+
/network error/i,
|
|
550
|
+
/fetch failed/i,
|
|
551
|
+
/getaddrinfo/i
|
|
552
|
+
],
|
|
553
|
+
category: "network",
|
|
554
|
+
type: "build_error",
|
|
555
|
+
possibleCauses: [
|
|
556
|
+
"Network connectivity issues",
|
|
557
|
+
"DNS resolution failure",
|
|
558
|
+
"Registry unavailable",
|
|
559
|
+
"Firewall blocking connections"
|
|
560
|
+
],
|
|
561
|
+
suggestedFixes: [
|
|
562
|
+
"Check if the package registry is accessible",
|
|
563
|
+
"Verify network configuration",
|
|
564
|
+
"Try again later if registry is temporarily down",
|
|
565
|
+
"Check for any proxy configuration issues"
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
];
|
|
569
|
+
function analyzeDeploymentError(logs, errorMessage) {
|
|
570
|
+
const relevantLogLines = [];
|
|
571
|
+
let matchedPattern = null;
|
|
572
|
+
let maxMatches = 0;
|
|
573
|
+
const allLines = errorMessage ? [...logs, errorMessage] : logs;
|
|
574
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
575
|
+
let matches = 0;
|
|
576
|
+
const matchedLines = [];
|
|
577
|
+
for (const line of allLines) {
|
|
578
|
+
for (const regex of pattern.patterns) {
|
|
579
|
+
if (regex.test(line)) {
|
|
580
|
+
matches++;
|
|
581
|
+
if (!matchedLines.includes(line)) {
|
|
582
|
+
matchedLines.push(line);
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
486
585
|
}
|
|
487
|
-
} catch (err) {
|
|
488
|
-
failSpinner("Authentication failed");
|
|
489
|
-
authServer.close();
|
|
490
|
-
throw err;
|
|
491
586
|
}
|
|
492
|
-
} catch (err) {
|
|
493
|
-
handleError(err);
|
|
494
587
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
outputData({ success: true, message: "Already logged out" });
|
|
501
|
-
} else {
|
|
502
|
-
log("Already logged out.");
|
|
503
|
-
}
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
const profile = getCurrentProfile();
|
|
507
|
-
clearConfig();
|
|
508
|
-
if (isJsonMode()) {
|
|
509
|
-
outputData({ success: true, message: "Logged out successfully" });
|
|
510
|
-
} else {
|
|
511
|
-
success(`Logged out from ${profile?.userEmail || "Tarout"}`);
|
|
512
|
-
}
|
|
513
|
-
} catch (err) {
|
|
514
|
-
handleError(err);
|
|
588
|
+
if (matches > maxMatches) {
|
|
589
|
+
maxMatches = matches;
|
|
590
|
+
matchedPattern = pattern;
|
|
591
|
+
relevantLogLines.length = 0;
|
|
592
|
+
relevantLogLines.push(...matchedLines.slice(0, 10));
|
|
515
593
|
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
const profile = getCurrentProfile();
|
|
523
|
-
if (!profile) {
|
|
524
|
-
throw new AuthError();
|
|
525
|
-
}
|
|
526
|
-
if (isJsonMode()) {
|
|
527
|
-
outputData({
|
|
528
|
-
user: {
|
|
529
|
-
id: profile.userId,
|
|
530
|
-
email: profile.userEmail,
|
|
531
|
-
name: profile.userName
|
|
532
|
-
},
|
|
533
|
-
organization: {
|
|
534
|
-
id: profile.organizationId,
|
|
535
|
-
name: profile.organizationName
|
|
536
|
-
},
|
|
537
|
-
environment: {
|
|
538
|
-
id: profile.environmentId,
|
|
539
|
-
name: profile.environmentName
|
|
540
|
-
},
|
|
541
|
-
apiUrl: profile.apiUrl
|
|
542
|
-
});
|
|
543
|
-
} else {
|
|
544
|
-
log("");
|
|
545
|
-
log(`${colors.bold("User")}`);
|
|
546
|
-
log(` Email: ${colors.cyan(profile.userEmail)}`);
|
|
547
|
-
if (profile.userName) {
|
|
548
|
-
log(` Name: ${profile.userName}`);
|
|
549
|
-
}
|
|
550
|
-
log("");
|
|
551
|
-
log(`${colors.bold("Organization")}`);
|
|
552
|
-
log(` Name: ${profile.organizationName}`);
|
|
553
|
-
log(` ID: ${colors.dim(profile.organizationId)}`);
|
|
554
|
-
log("");
|
|
555
|
-
log(`${colors.bold("Environment")}`);
|
|
556
|
-
log(` Name: ${profile.environmentName}`);
|
|
557
|
-
log(` ID: ${colors.dim(profile.environmentId)}`);
|
|
558
|
-
log("");
|
|
559
|
-
}
|
|
560
|
-
} catch (err) {
|
|
561
|
-
handleError(err);
|
|
594
|
+
}
|
|
595
|
+
const errorIndicators = [/error/i, /failed/i, /fatal/i, /exception/i];
|
|
596
|
+
for (const line of allLines) {
|
|
597
|
+
if (relevantLogLines.length < 15 && !relevantLogLines.includes(line) && errorIndicators.some((indicator) => indicator.test(line))) {
|
|
598
|
+
relevantLogLines.push(line);
|
|
562
599
|
}
|
|
563
|
-
}
|
|
600
|
+
}
|
|
601
|
+
if (matchedPattern) {
|
|
602
|
+
return {
|
|
603
|
+
type: matchedPattern.type,
|
|
604
|
+
category: matchedPattern.category,
|
|
605
|
+
possibleCauses: matchedPattern.possibleCauses,
|
|
606
|
+
suggestedFixes: matchedPattern.suggestedFixes,
|
|
607
|
+
relevantLogLines
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
type: "unknown",
|
|
612
|
+
category: "unknown",
|
|
613
|
+
possibleCauses: [
|
|
614
|
+
"Unable to automatically determine the cause",
|
|
615
|
+
"Check the log output for specific error messages"
|
|
616
|
+
],
|
|
617
|
+
suggestedFixes: [
|
|
618
|
+
"Review the deployment logs for error messages",
|
|
619
|
+
"Try running the build locally to reproduce the issue",
|
|
620
|
+
"Check environment variables and configuration",
|
|
621
|
+
"Contact support if the issue persists"
|
|
622
|
+
],
|
|
623
|
+
relevantLogLines
|
|
624
|
+
};
|
|
564
625
|
}
|
|
565
626
|
|
|
566
|
-
// src/commands/apps.ts
|
|
567
|
-
import open2 from "open";
|
|
568
|
-
|
|
569
627
|
// src/lib/api.ts
|
|
570
|
-
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
|
571
|
-
import superjson from "superjson";
|
|
572
628
|
var client = null;
|
|
573
629
|
function createApiClient() {
|
|
574
630
|
if (!isLoggedIn()) {
|
|
@@ -638,7 +694,7 @@ function registerAppsCommands(program2) {
|
|
|
638
694
|
try {
|
|
639
695
|
if (!isLoggedIn()) throw new AuthError();
|
|
640
696
|
const client2 = getApiClient();
|
|
641
|
-
const
|
|
697
|
+
const _spinner = startSpinner("Fetching applications...");
|
|
642
698
|
const applications = await client2.application.allByOrganization.query();
|
|
643
699
|
succeedSpinner();
|
|
644
700
|
if (isJsonMode()) {
|
|
@@ -664,7 +720,11 @@ function registerAppsCommands(program2) {
|
|
|
664
720
|
])
|
|
665
721
|
);
|
|
666
722
|
log("");
|
|
667
|
-
log(
|
|
723
|
+
log(
|
|
724
|
+
colors.dim(
|
|
725
|
+
`${applications.length} application${applications.length === 1 ? "" : "s"}`
|
|
726
|
+
)
|
|
727
|
+
);
|
|
668
728
|
} catch (err) {
|
|
669
729
|
handleError(err);
|
|
670
730
|
}
|
|
@@ -684,7 +744,7 @@ function registerAppsCommands(program2) {
|
|
|
684
744
|
}
|
|
685
745
|
const slug = generateSlug(appName);
|
|
686
746
|
const client2 = getApiClient();
|
|
687
|
-
const
|
|
747
|
+
const _spinner = startSpinner("Creating application...");
|
|
688
748
|
const application = await client2.application.create.mutate({
|
|
689
749
|
name: appName,
|
|
690
750
|
appName: slug,
|
|
@@ -703,8 +763,12 @@ function registerAppsCommands(program2) {
|
|
|
703
763
|
`Slug: ${application.appName}`
|
|
704
764
|
]);
|
|
705
765
|
log("Next steps:");
|
|
706
|
-
log(
|
|
707
|
-
|
|
766
|
+
log(
|
|
767
|
+
` 1. Connect a source: ${colors.dim(`tarout apps info ${application.applicationId.slice(0, 8)}`)}`
|
|
768
|
+
);
|
|
769
|
+
log(
|
|
770
|
+
` 2. Deploy: ${colors.dim(`tarout deploy ${application.applicationId.slice(0, 8)}`)}`
|
|
771
|
+
);
|
|
708
772
|
log("");
|
|
709
773
|
} catch (err) {
|
|
710
774
|
handleError(err);
|
|
@@ -714,7 +778,7 @@ function registerAppsCommands(program2) {
|
|
|
714
778
|
try {
|
|
715
779
|
if (!isLoggedIn()) throw new AuthError();
|
|
716
780
|
const client2 = getApiClient();
|
|
717
|
-
const
|
|
781
|
+
const _spinner = startSpinner("Finding application...");
|
|
718
782
|
const apps2 = await client2.application.allByOrganization.query();
|
|
719
783
|
const app = findApp(apps2, appIdentifier);
|
|
720
784
|
if (!app) {
|
|
@@ -740,7 +804,7 @@ function registerAppsCommands(program2) {
|
|
|
740
804
|
return;
|
|
741
805
|
}
|
|
742
806
|
}
|
|
743
|
-
const
|
|
807
|
+
const _deleteSpinner = startSpinner("Deleting application...");
|
|
744
808
|
await client2.application.delete.mutate({
|
|
745
809
|
applicationId: app.applicationId
|
|
746
810
|
});
|
|
@@ -758,7 +822,7 @@ function registerAppsCommands(program2) {
|
|
|
758
822
|
try {
|
|
759
823
|
if (!isLoggedIn()) throw new AuthError();
|
|
760
824
|
const client2 = getApiClient();
|
|
761
|
-
const
|
|
825
|
+
const _spinner = startSpinner("Fetching application...");
|
|
762
826
|
const apps2 = await client2.application.allByOrganization.query();
|
|
763
827
|
const appSummary = findApp(apps2, appIdentifier);
|
|
764
828
|
if (!appSummary) {
|
|
@@ -835,7 +899,7 @@ function registerAppsCommands(program2) {
|
|
|
835
899
|
try {
|
|
836
900
|
if (!isLoggedIn()) throw new AuthError();
|
|
837
901
|
const client2 = getApiClient();
|
|
838
|
-
const
|
|
902
|
+
const _spinner = startSpinner("Finding application...");
|
|
839
903
|
const apps2 = await client2.application.allByOrganization.query();
|
|
840
904
|
const appSummary = findApp(apps2, appIdentifier);
|
|
841
905
|
if (!appSummary) {
|
|
@@ -857,7 +921,9 @@ function registerAppsCommands(program2) {
|
|
|
857
921
|
url = app.cloudServiceUrl;
|
|
858
922
|
}
|
|
859
923
|
if (!url) {
|
|
860
|
-
error(
|
|
924
|
+
error(
|
|
925
|
+
"No URL found for this application. Deploy first or add a domain."
|
|
926
|
+
);
|
|
861
927
|
return;
|
|
862
928
|
}
|
|
863
929
|
if (isJsonMode()) {
|
|
@@ -865,7 +931,7 @@ function registerAppsCommands(program2) {
|
|
|
865
931
|
return;
|
|
866
932
|
}
|
|
867
933
|
log(`Opening ${colors.cyan(url)}...`);
|
|
868
|
-
await
|
|
934
|
+
await open(url);
|
|
869
935
|
} catch (err) {
|
|
870
936
|
handleError(err);
|
|
871
937
|
}
|
|
@@ -888,1033 +954,1445 @@ function formatDate(date) {
|
|
|
888
954
|
);
|
|
889
955
|
if (diffDays === 0) {
|
|
890
956
|
return "Today";
|
|
891
|
-
}
|
|
957
|
+
}
|
|
958
|
+
if (diffDays === 1) {
|
|
892
959
|
return "Yesterday";
|
|
893
|
-
}
|
|
960
|
+
}
|
|
961
|
+
if (diffDays < 7) {
|
|
894
962
|
return `${diffDays}d ago`;
|
|
895
|
-
} else {
|
|
896
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
897
963
|
}
|
|
964
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
898
965
|
}
|
|
899
966
|
|
|
900
|
-
// src/commands/
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
failSpinner("Deployment failed");
|
|
952
|
-
throw new CliError("Deployment failed. Check logs for details.", ExitCode.GENERAL_ERROR);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
failSpinner("Deployment timed out");
|
|
956
|
-
throw new CliError("Deployment timed out after 10 minutes", ExitCode.GENERAL_ERROR);
|
|
957
|
-
} else {
|
|
958
|
-
succeedSpinner("Deployment started!");
|
|
959
|
-
if (isJsonMode()) {
|
|
960
|
-
outputData({
|
|
961
|
-
deploymentId: result.deploymentId,
|
|
962
|
-
status: "deploying"
|
|
963
|
-
});
|
|
964
|
-
} else {
|
|
965
|
-
quietOutput(result.deploymentId);
|
|
966
|
-
log("");
|
|
967
|
-
log(`Deployment ID: ${colors.cyan(result.deploymentId)}`);
|
|
968
|
-
log("");
|
|
969
|
-
log("Deployment is running in the background.");
|
|
970
|
-
log(`Check status: ${colors.dim(`tarout deploy:status ${app.applicationId.slice(0, 8)}`)}`);
|
|
971
|
-
log(`View logs: ${colors.dim(`tarout logs ${app.applicationId.slice(0, 8)}`)}`);
|
|
972
|
-
log("");
|
|
973
|
-
}
|
|
967
|
+
// src/commands/auth.ts
|
|
968
|
+
import open2 from "open";
|
|
969
|
+
|
|
970
|
+
// src/lib/auth-server.ts
|
|
971
|
+
import express from "express";
|
|
972
|
+
function startAuthServer() {
|
|
973
|
+
return new Promise((resolve) => {
|
|
974
|
+
const app = express();
|
|
975
|
+
let server;
|
|
976
|
+
let callbackResolver;
|
|
977
|
+
let callbackRejecter;
|
|
978
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
979
|
+
callbackResolver = res;
|
|
980
|
+
callbackRejecter = rej;
|
|
981
|
+
});
|
|
982
|
+
app.get("/callback", (req, res) => {
|
|
983
|
+
const {
|
|
984
|
+
token,
|
|
985
|
+
userId,
|
|
986
|
+
userEmail,
|
|
987
|
+
userName,
|
|
988
|
+
organizationId,
|
|
989
|
+
organizationName,
|
|
990
|
+
environmentId,
|
|
991
|
+
environmentName,
|
|
992
|
+
error: error2
|
|
993
|
+
} = req.query;
|
|
994
|
+
if (error2) {
|
|
995
|
+
res.send(`
|
|
996
|
+
<!DOCTYPE html>
|
|
997
|
+
<html>
|
|
998
|
+
<head>
|
|
999
|
+
<title>Tarout CLI - Authentication Failed</title>
|
|
1000
|
+
<style>
|
|
1001
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
1002
|
+
.container { text-align: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
1003
|
+
h1 { color: #dc2626; margin-bottom: 16px; }
|
|
1004
|
+
p { color: #666; }
|
|
1005
|
+
</style>
|
|
1006
|
+
</head>
|
|
1007
|
+
<body>
|
|
1008
|
+
<div class="container">
|
|
1009
|
+
<h1>Authentication Failed</h1>
|
|
1010
|
+
<p>${error2}</p>
|
|
1011
|
+
<p>You can close this window and try again.</p>
|
|
1012
|
+
</div>
|
|
1013
|
+
</body>
|
|
1014
|
+
</html>
|
|
1015
|
+
`);
|
|
1016
|
+
callbackRejecter(new Error(String(error2)));
|
|
1017
|
+
return;
|
|
974
1018
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
program2.command("deploy:status").argument("<app>", "Application ID or name").description("Check deployment status").action(async (appIdentifier) => {
|
|
980
|
-
try {
|
|
981
|
-
if (!isLoggedIn()) throw new AuthError();
|
|
982
|
-
const client2 = getApiClient();
|
|
983
|
-
const spinner = startSpinner("Fetching status...");
|
|
984
|
-
const apps = await client2.application.allByOrganization.query();
|
|
985
|
-
const appSummary = findApp2(apps, appIdentifier);
|
|
986
|
-
if (!appSummary) {
|
|
987
|
-
failSpinner();
|
|
988
|
-
const suggestions = findSimilar(
|
|
989
|
-
appIdentifier,
|
|
990
|
-
apps.map((a) => a.name)
|
|
1019
|
+
if (!token || !userId || !userEmail || !organizationId || !organizationName || !environmentId || !environmentName) {
|
|
1020
|
+
res.status(400).send("Missing required parameters");
|
|
1021
|
+
callbackRejecter(
|
|
1022
|
+
new Error("Missing required parameters from auth callback")
|
|
991
1023
|
);
|
|
992
|
-
|
|
1024
|
+
return;
|
|
993
1025
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1026
|
+
res.send(`
|
|
1027
|
+
<!DOCTYPE html>
|
|
1028
|
+
<html>
|
|
1029
|
+
<head>
|
|
1030
|
+
<title>Tarout CLI - Authenticated</title>
|
|
1031
|
+
<style>
|
|
1032
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
1033
|
+
.container { text-align: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
1034
|
+
h1 { color: #16a34a; margin-bottom: 16px; }
|
|
1035
|
+
p { color: #666; }
|
|
1036
|
+
.checkmark { font-size: 64px; margin-bottom: 16px; }
|
|
1037
|
+
</style>
|
|
1038
|
+
</head>
|
|
1039
|
+
<body>
|
|
1040
|
+
<div class="container">
|
|
1041
|
+
<div class="checkmark">\u2713</div>
|
|
1042
|
+
<h1>Authenticated!</h1>
|
|
1043
|
+
<p>You can close this window and return to the terminal.</p>
|
|
1044
|
+
</div>
|
|
1045
|
+
</body>
|
|
1046
|
+
</html>
|
|
1047
|
+
`);
|
|
1048
|
+
callbackResolver({
|
|
1049
|
+
token: String(token),
|
|
1050
|
+
userId: String(userId),
|
|
1051
|
+
userEmail: String(userEmail),
|
|
1052
|
+
userName: userName ? String(userName) : void 0,
|
|
1053
|
+
organizationId: String(organizationId),
|
|
1054
|
+
organizationName: String(organizationName),
|
|
1055
|
+
environmentId: String(environmentId),
|
|
1056
|
+
environmentName: String(environmentName)
|
|
999
1057
|
});
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1058
|
+
});
|
|
1059
|
+
server = app.listen(0, () => {
|
|
1060
|
+
const address = server.address();
|
|
1061
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
1062
|
+
resolve({
|
|
1063
|
+
port,
|
|
1064
|
+
waitForCallback: () => callbackPromise,
|
|
1065
|
+
close: () => server.close()
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
setTimeout(
|
|
1069
|
+
() => {
|
|
1070
|
+
callbackRejecter(
|
|
1071
|
+
new Error("Authentication timed out. Please try again.")
|
|
1072
|
+
);
|
|
1073
|
+
server.close();
|
|
1074
|
+
},
|
|
1075
|
+
5 * 60 * 1e3
|
|
1076
|
+
);
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/commands/auth.ts
|
|
1081
|
+
function registerAuthCommands(program2) {
|
|
1082
|
+
program2.command("login").description("Authenticate with Tarout via browser").option("--api-url <url>", "Custom API URL", "https://tarout.sa").action(async (options) => {
|
|
1083
|
+
try {
|
|
1084
|
+
if (isLoggedIn()) {
|
|
1085
|
+
const profile = getCurrentProfile();
|
|
1086
|
+
if (profile) {
|
|
1087
|
+
log(`Already logged in as ${colors.cyan(profile.userEmail)}`);
|
|
1088
|
+
log(`Organization: ${profile.organizationName}`);
|
|
1089
|
+
log("");
|
|
1090
|
+
log(`Run ${colors.dim("tarout logout")} to sign out first.`);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1010
1093
|
}
|
|
1094
|
+
const apiUrl = options.apiUrl;
|
|
1011
1095
|
log("");
|
|
1012
|
-
log(
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1096
|
+
log("Opening browser to authenticate...");
|
|
1097
|
+
const authServer = await startAuthServer();
|
|
1098
|
+
const callbackUrl = `http://localhost:${authServer.port}/callback`;
|
|
1099
|
+
const authUrl = `${apiUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
1100
|
+
await open2(authUrl);
|
|
1101
|
+
const _spinner = startSpinner("Waiting for authentication...");
|
|
1102
|
+
try {
|
|
1103
|
+
const authData = await authServer.waitForCallback();
|
|
1104
|
+
succeedSpinner("Authentication successful!");
|
|
1105
|
+
authServer.close();
|
|
1106
|
+
setProfile("default", {
|
|
1107
|
+
token: authData.token,
|
|
1108
|
+
apiUrl,
|
|
1109
|
+
userId: authData.userId,
|
|
1110
|
+
userEmail: authData.userEmail,
|
|
1111
|
+
userName: authData.userName,
|
|
1112
|
+
organizationId: authData.organizationId,
|
|
1113
|
+
organizationName: authData.organizationName,
|
|
1114
|
+
environmentId: authData.environmentId,
|
|
1115
|
+
environmentName: authData.environmentName
|
|
1116
|
+
});
|
|
1117
|
+
setCurrentProfile("default");
|
|
1118
|
+
if (isJsonMode()) {
|
|
1119
|
+
outputData({
|
|
1120
|
+
success: true,
|
|
1121
|
+
user: {
|
|
1122
|
+
id: authData.userId,
|
|
1123
|
+
email: authData.userEmail,
|
|
1124
|
+
name: authData.userName
|
|
1125
|
+
},
|
|
1126
|
+
organization: {
|
|
1127
|
+
id: authData.organizationId,
|
|
1128
|
+
name: authData.organizationName
|
|
1129
|
+
},
|
|
1130
|
+
environment: {
|
|
1131
|
+
id: authData.environmentId,
|
|
1132
|
+
name: authData.environmentName
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
} else {
|
|
1136
|
+
log("");
|
|
1137
|
+
success(`Logged in as ${colors.cyan(authData.userEmail)}`);
|
|
1138
|
+
box("Account", [
|
|
1139
|
+
`Organization: ${colors.bold(authData.organizationName)}`,
|
|
1140
|
+
`Environment: ${colors.bold(authData.environmentName)}`
|
|
1141
|
+
]);
|
|
1142
|
+
}
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
failSpinner("Authentication failed");
|
|
1145
|
+
authServer.close();
|
|
1146
|
+
throw err;
|
|
1022
1147
|
}
|
|
1023
|
-
log("");
|
|
1024
1148
|
} catch (err) {
|
|
1025
1149
|
handleError(err);
|
|
1026
1150
|
}
|
|
1027
1151
|
});
|
|
1028
|
-
program2.command("
|
|
1152
|
+
program2.command("logout").description("Sign out and clear stored credentials").action(async () => {
|
|
1029
1153
|
try {
|
|
1030
|
-
if (!isLoggedIn())
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const suggestions = findSimilar(
|
|
1038
|
-
appIdentifier,
|
|
1039
|
-
apps.map((a) => a.name)
|
|
1040
|
-
);
|
|
1041
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1154
|
+
if (!isLoggedIn()) {
|
|
1155
|
+
if (isJsonMode()) {
|
|
1156
|
+
outputData({ success: true, message: "Already logged out" });
|
|
1157
|
+
} else {
|
|
1158
|
+
log("Already logged out.");
|
|
1159
|
+
}
|
|
1160
|
+
return;
|
|
1042
1161
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
});
|
|
1046
|
-
succeedSpinner("Deployment cancelled");
|
|
1162
|
+
const profile = getCurrentProfile();
|
|
1163
|
+
clearConfig();
|
|
1047
1164
|
if (isJsonMode()) {
|
|
1048
|
-
outputData({
|
|
1165
|
+
outputData({ success: true, message: "Logged out successfully" });
|
|
1166
|
+
} else {
|
|
1167
|
+
success(`Logged out from ${profile?.userEmail || "Tarout"}`);
|
|
1049
1168
|
}
|
|
1050
1169
|
} catch (err) {
|
|
1051
1170
|
handleError(err);
|
|
1052
1171
|
}
|
|
1053
1172
|
});
|
|
1054
|
-
program2.command("
|
|
1173
|
+
program2.command("whoami").description("Show current authenticated user").action(async () => {
|
|
1055
1174
|
try {
|
|
1056
|
-
if (!isLoggedIn())
|
|
1057
|
-
|
|
1058
|
-
const spinner = startSpinner("Fetching deployments...");
|
|
1059
|
-
const apps = await client2.application.allByOrganization.query();
|
|
1060
|
-
const appSummary = findApp2(apps, appIdentifier);
|
|
1061
|
-
if (!appSummary) {
|
|
1062
|
-
failSpinner();
|
|
1063
|
-
const suggestions = findSimilar(
|
|
1064
|
-
appIdentifier,
|
|
1065
|
-
apps.map((a) => a.name)
|
|
1066
|
-
);
|
|
1067
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1175
|
+
if (!isLoggedIn()) {
|
|
1176
|
+
throw new AuthError();
|
|
1068
1177
|
}
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
succeedSpinner();
|
|
1073
|
-
const limitedDeployments = deployments.slice(0, parseInt(options.limit, 10));
|
|
1074
|
-
if (isJsonMode()) {
|
|
1075
|
-
outputData(limitedDeployments);
|
|
1076
|
-
return;
|
|
1178
|
+
const profile = getCurrentProfile();
|
|
1179
|
+
if (!profile) {
|
|
1180
|
+
throw new AuthError();
|
|
1077
1181
|
}
|
|
1078
|
-
if (
|
|
1182
|
+
if (isJsonMode()) {
|
|
1183
|
+
outputData({
|
|
1184
|
+
user: {
|
|
1185
|
+
id: profile.userId,
|
|
1186
|
+
email: profile.userEmail,
|
|
1187
|
+
name: profile.userName
|
|
1188
|
+
},
|
|
1189
|
+
organization: {
|
|
1190
|
+
id: profile.organizationId,
|
|
1191
|
+
name: profile.organizationName
|
|
1192
|
+
},
|
|
1193
|
+
environment: {
|
|
1194
|
+
id: profile.environmentId,
|
|
1195
|
+
name: profile.environmentName
|
|
1196
|
+
},
|
|
1197
|
+
apiUrl: profile.apiUrl
|
|
1198
|
+
});
|
|
1199
|
+
} else {
|
|
1079
1200
|
log("");
|
|
1080
|
-
log(
|
|
1201
|
+
log(`${colors.bold("User")}`);
|
|
1202
|
+
log(` Email: ${colors.cyan(profile.userEmail)}`);
|
|
1203
|
+
if (profile.userName) {
|
|
1204
|
+
log(` Name: ${profile.userName}`);
|
|
1205
|
+
}
|
|
1206
|
+
log("");
|
|
1207
|
+
log(`${colors.bold("Organization")}`);
|
|
1208
|
+
log(` Name: ${profile.organizationName}`);
|
|
1209
|
+
log(` ID: ${colors.dim(profile.organizationId)}`);
|
|
1210
|
+
log("");
|
|
1211
|
+
log(`${colors.bold("Environment")}`);
|
|
1212
|
+
log(` Name: ${profile.environmentName}`);
|
|
1213
|
+
log(` ID: ${colors.dim(profile.environmentId)}`);
|
|
1081
1214
|
log("");
|
|
1082
|
-
log(`Deploy with: ${colors.dim(`tarout deploy ${appSummary.applicationId.slice(0, 8)}`)}`);
|
|
1083
|
-
return;
|
|
1084
1215
|
}
|
|
1085
|
-
log("");
|
|
1086
|
-
table(
|
|
1087
|
-
["ID", "STATUS", "TITLE", "CREATED"],
|
|
1088
|
-
limitedDeployments.map((d) => [
|
|
1089
|
-
colors.cyan(d.deploymentId.slice(0, 8)),
|
|
1090
|
-
getStatusBadge(d.status),
|
|
1091
|
-
d.title || colors.dim("-"),
|
|
1092
|
-
formatDate2(d.createdAt)
|
|
1093
|
-
])
|
|
1094
|
-
);
|
|
1095
|
-
log("");
|
|
1096
|
-
log(colors.dim(`${limitedDeployments.length} deployment${limitedDeployments.length === 1 ? "" : "s"}`));
|
|
1097
1216
|
} catch (err) {
|
|
1098
1217
|
handleError(err);
|
|
1099
1218
|
}
|
|
1100
1219
|
});
|
|
1101
1220
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1221
|
+
|
|
1222
|
+
// src/commands/db.ts
|
|
1223
|
+
import { spawn } from "child_process";
|
|
1224
|
+
function registerDbCommands(program2) {
|
|
1225
|
+
const db = program2.command("db").description("Manage databases");
|
|
1226
|
+
db.command("list").alias("ls").description("List all databases").option("-t, --type <type>", "Filter by type (postgres, mysql, redis)").action(async (options) => {
|
|
1104
1227
|
try {
|
|
1105
1228
|
if (!isLoggedIn()) throw new AuthError();
|
|
1106
1229
|
const client2 = getApiClient();
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
appIdentifier,
|
|
1114
|
-
apps.map((a) => a.name)
|
|
1115
|
-
);
|
|
1116
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1117
|
-
}
|
|
1118
|
-
let timeRange;
|
|
1119
|
-
if (options.since) {
|
|
1120
|
-
const since = parseDuration(options.since);
|
|
1121
|
-
if (since) {
|
|
1122
|
-
timeRange = {
|
|
1123
|
-
start: new Date(Date.now() - since).toISOString(),
|
|
1124
|
-
end: null
|
|
1125
|
-
};
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
const logs = await client2.logs.getCloudRunLogs.query({
|
|
1129
|
-
applicationId: app.applicationId,
|
|
1130
|
-
level: options.level.toUpperCase(),
|
|
1131
|
-
limit: parseInt(options.limit, 10),
|
|
1132
|
-
timeRange
|
|
1133
|
-
});
|
|
1230
|
+
const _spinner = startSpinner("Fetching databases...");
|
|
1231
|
+
const [postgres, mysql, redis] = await Promise.all([
|
|
1232
|
+
client2.postgres.allByOrganization.query(),
|
|
1233
|
+
client2.mysql.allByOrganization.query(),
|
|
1234
|
+
client2.redis.allByOrganization.query()
|
|
1235
|
+
]);
|
|
1134
1236
|
succeedSpinner();
|
|
1237
|
+
let databases = [];
|
|
1238
|
+
if (!options.type || options.type === "postgres") {
|
|
1239
|
+
databases = databases.concat(
|
|
1240
|
+
postgres.map((db2) => ({
|
|
1241
|
+
id: db2.postgresId,
|
|
1242
|
+
name: db2.name,
|
|
1243
|
+
type: "postgres",
|
|
1244
|
+
status: db2.applicationStatus,
|
|
1245
|
+
created: db2.createdAt
|
|
1246
|
+
}))
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if (!options.type || options.type === "mysql") {
|
|
1250
|
+
databases = databases.concat(
|
|
1251
|
+
mysql.map((db2) => ({
|
|
1252
|
+
id: db2.mysqlId,
|
|
1253
|
+
name: db2.name,
|
|
1254
|
+
type: "mysql",
|
|
1255
|
+
status: db2.applicationStatus,
|
|
1256
|
+
created: db2.createdAt
|
|
1257
|
+
}))
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (!options.type || options.type === "redis") {
|
|
1261
|
+
databases = databases.concat(
|
|
1262
|
+
redis.map((db2) => ({
|
|
1263
|
+
id: db2.redisId,
|
|
1264
|
+
name: db2.name,
|
|
1265
|
+
type: "redis",
|
|
1266
|
+
status: db2.applicationStatus,
|
|
1267
|
+
created: db2.createdAt
|
|
1268
|
+
}))
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1135
1271
|
if (isJsonMode()) {
|
|
1136
|
-
outputData(
|
|
1272
|
+
outputData(databases);
|
|
1137
1273
|
return;
|
|
1138
1274
|
}
|
|
1139
|
-
if (
|
|
1275
|
+
if (databases.length === 0) {
|
|
1140
1276
|
log("");
|
|
1141
|
-
log("No
|
|
1277
|
+
log("No databases found.");
|
|
1278
|
+
log("");
|
|
1279
|
+
log(`Create one with: ${colors.dim("tarout db create <name>")}`);
|
|
1142
1280
|
return;
|
|
1143
1281
|
}
|
|
1144
1282
|
log("");
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
end: null
|
|
1162
|
-
}
|
|
1163
|
-
});
|
|
1164
|
-
if (newLogs.entries && newLogs.entries.length > 0) {
|
|
1165
|
-
for (const entry of newLogs.entries) {
|
|
1166
|
-
const entryTime = new Date(entry.timestamp).getTime();
|
|
1167
|
-
if (entryTime > lastTimestamp) {
|
|
1168
|
-
printLogEntry(entry);
|
|
1169
|
-
lastTimestamp = entryTime;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1283
|
+
table(
|
|
1284
|
+
["ID", "NAME", "TYPE", "STATUS", "CREATED"],
|
|
1285
|
+
databases.map((db2) => [
|
|
1286
|
+
colors.cyan(db2.id.slice(0, 8)),
|
|
1287
|
+
db2.name,
|
|
1288
|
+
getTypeLabel(db2.type),
|
|
1289
|
+
getStatusBadge(db2.status),
|
|
1290
|
+
formatDate2(db2.created)
|
|
1291
|
+
])
|
|
1292
|
+
);
|
|
1293
|
+
log("");
|
|
1294
|
+
log(
|
|
1295
|
+
colors.dim(
|
|
1296
|
+
`${databases.length} database${databases.length === 1 ? "" : "s"}`
|
|
1297
|
+
)
|
|
1298
|
+
);
|
|
1175
1299
|
} catch (err) {
|
|
1176
1300
|
handleError(err);
|
|
1177
1301
|
}
|
|
1178
1302
|
});
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
function formatDate2(date) {
|
|
1187
|
-
const d = new Date(date);
|
|
1188
|
-
return d.toLocaleString("en-US", {
|
|
1189
|
-
month: "short",
|
|
1190
|
-
day: "numeric",
|
|
1191
|
-
hour: "2-digit",
|
|
1192
|
-
minute: "2-digit"
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
function sleep(ms) {
|
|
1196
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1197
|
-
}
|
|
1198
|
-
function parseDuration(duration) {
|
|
1199
|
-
const match = duration.match(/^(\d+)(s|m|h|d)$/);
|
|
1200
|
-
if (!match) return null;
|
|
1201
|
-
const value = parseInt(match[1], 10);
|
|
1202
|
-
const unit = match[2];
|
|
1203
|
-
const multipliers = {
|
|
1204
|
-
s: 1e3,
|
|
1205
|
-
m: 60 * 1e3,
|
|
1206
|
-
h: 60 * 60 * 1e3,
|
|
1207
|
-
d: 24 * 60 * 60 * 1e3
|
|
1208
|
-
};
|
|
1209
|
-
return value * (multipliers[unit] || 0);
|
|
1210
|
-
}
|
|
1211
|
-
function printLogEntry(entry) {
|
|
1212
|
-
const time = new Date(entry.timestamp);
|
|
1213
|
-
const timeStr = time.toLocaleTimeString("en-US", {
|
|
1214
|
-
hour12: false,
|
|
1215
|
-
hour: "2-digit",
|
|
1216
|
-
minute: "2-digit",
|
|
1217
|
-
second: "2-digit"
|
|
1218
|
-
});
|
|
1219
|
-
const levelColors = {
|
|
1220
|
-
ERROR: colors.error,
|
|
1221
|
-
WARN: colors.warn,
|
|
1222
|
-
INFO: colors.info,
|
|
1223
|
-
DEBUG: colors.dim,
|
|
1224
|
-
DEFAULT: colors.dim
|
|
1225
|
-
};
|
|
1226
|
-
const colorFn = levelColors[entry.severity] || levelColors.DEFAULT;
|
|
1227
|
-
const level = entry.severity.padEnd(5);
|
|
1228
|
-
console.log(`${colors.dim(timeStr)} ${colorFn(level)} ${entry.message}`);
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// src/commands/env.ts
|
|
1232
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
1233
|
-
function registerEnvCommands(program2) {
|
|
1234
|
-
const env = program2.command("env").argument("<app>", "Application ID or name").description("Manage environment variables");
|
|
1235
|
-
env.command("list").alias("ls").description("List all environment variables").option("--reveal", "Show actual values (not masked)").action(async (options, command) => {
|
|
1303
|
+
db.command("create").argument("[name]", "Database name").description("Create a new database").option(
|
|
1304
|
+
"-t, --type <type>",
|
|
1305
|
+
"Database type (postgres, mysql, redis)",
|
|
1306
|
+
"postgres"
|
|
1307
|
+
).option("-d, --description <description>", "Database description").action(async (name, options) => {
|
|
1236
1308
|
try {
|
|
1237
1309
|
if (!isLoggedIn()) throw new AuthError();
|
|
1238
|
-
const
|
|
1310
|
+
const profile = getCurrentProfile();
|
|
1311
|
+
if (!profile) throw new AuthError();
|
|
1312
|
+
let dbName = name;
|
|
1313
|
+
let dbType = options.type;
|
|
1314
|
+
if (!dbName) {
|
|
1315
|
+
dbName = await input("Database name:");
|
|
1316
|
+
}
|
|
1317
|
+
if (!options.type && !shouldSkipConfirmation()) {
|
|
1318
|
+
dbType = await select("Database type:", [
|
|
1319
|
+
{ name: "PostgreSQL", value: "postgres" },
|
|
1320
|
+
{ name: "MySQL", value: "mysql" },
|
|
1321
|
+
{ name: "Redis", value: "redis" }
|
|
1322
|
+
]);
|
|
1323
|
+
}
|
|
1324
|
+
const slug = generateSlug2(dbName);
|
|
1239
1325
|
const client2 = getApiClient();
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1326
|
+
const _spinner = startSpinner(`Creating ${dbType} database...`);
|
|
1327
|
+
let database;
|
|
1328
|
+
switch (dbType) {
|
|
1329
|
+
case "postgres":
|
|
1330
|
+
database = await client2.postgres.create.mutate({
|
|
1331
|
+
name: dbName,
|
|
1332
|
+
appName: slug,
|
|
1333
|
+
dockerImage: "postgres:17",
|
|
1334
|
+
organizationId: profile.organizationId,
|
|
1335
|
+
description: options.description
|
|
1336
|
+
});
|
|
1337
|
+
break;
|
|
1338
|
+
case "mysql":
|
|
1339
|
+
database = await client2.mysql.create.mutate({
|
|
1340
|
+
name: dbName,
|
|
1341
|
+
appName: slug,
|
|
1342
|
+
dockerImage: "mysql:8",
|
|
1343
|
+
organizationId: profile.organizationId,
|
|
1344
|
+
description: options.description
|
|
1345
|
+
});
|
|
1346
|
+
break;
|
|
1347
|
+
case "redis":
|
|
1348
|
+
database = await client2.redis.create.mutate({
|
|
1349
|
+
name: dbName,
|
|
1350
|
+
appName: slug,
|
|
1351
|
+
dockerImage: "redis:7",
|
|
1352
|
+
organizationId: profile.organizationId,
|
|
1353
|
+
description: options.description
|
|
1354
|
+
});
|
|
1355
|
+
break;
|
|
1356
|
+
default:
|
|
1357
|
+
throw new CliError(
|
|
1358
|
+
`Unsupported database type: ${dbType}`,
|
|
1359
|
+
ExitCode.INVALID_ARGUMENTS
|
|
1360
|
+
);
|
|
1250
1361
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
includeValues: options.reveal || false
|
|
1254
|
-
});
|
|
1255
|
-
succeedSpinner();
|
|
1362
|
+
succeedSpinner("Database created!");
|
|
1363
|
+
const dbId = database.postgresId || database.mysqlId || database.redisId;
|
|
1256
1364
|
if (isJsonMode()) {
|
|
1257
|
-
outputData(
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
if (variables.length === 0) {
|
|
1261
|
-
log("");
|
|
1262
|
-
log("No environment variables found.");
|
|
1263
|
-
log("");
|
|
1264
|
-
log(`Set one with: ${colors.dim(`tarout env ${app.name} set KEY=value`)}`);
|
|
1365
|
+
outputData(database);
|
|
1265
1366
|
return;
|
|
1266
1367
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1368
|
+
quietOutput(dbId);
|
|
1369
|
+
box("Database Created", [
|
|
1370
|
+
`ID: ${colors.cyan(dbId)}`,
|
|
1371
|
+
`Name: ${database.name}`,
|
|
1372
|
+
`Type: ${getTypeLabel(dbType)}`
|
|
1373
|
+
]);
|
|
1374
|
+
log("Next steps:");
|
|
1375
|
+
log(
|
|
1376
|
+
` View connection info: ${colors.dim(`tarout db info ${dbId.slice(0, 8)}`)}`
|
|
1276
1377
|
);
|
|
1277
1378
|
log("");
|
|
1278
|
-
log(colors.dim(`${variables.length} variable${variables.length === 1 ? "" : "s"}`));
|
|
1279
1379
|
} catch (err) {
|
|
1280
1380
|
handleError(err);
|
|
1281
1381
|
}
|
|
1282
1382
|
});
|
|
1283
|
-
|
|
1383
|
+
db.command("delete").alias("rm").argument("<db>", "Database ID or name").description("Delete a database").action(async (dbIdentifier) => {
|
|
1284
1384
|
try {
|
|
1285
1385
|
if (!isLoggedIn()) throw new AuthError();
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1386
|
+
const client2 = getApiClient();
|
|
1387
|
+
const _spinner = startSpinner("Finding database...");
|
|
1388
|
+
const allDbs = await getAllDatabases(client2);
|
|
1389
|
+
const dbInfo = findDatabase(allDbs, dbIdentifier);
|
|
1390
|
+
if (!dbInfo) {
|
|
1391
|
+
failSpinner();
|
|
1392
|
+
const suggestions = findSimilar(
|
|
1393
|
+
dbIdentifier,
|
|
1394
|
+
allDbs.map((d) => d.name)
|
|
1291
1395
|
);
|
|
1396
|
+
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1292
1397
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
failSpinner();
|
|
1304
|
-
const suggestions = findSimilar(
|
|
1305
|
-
appIdentifier,
|
|
1306
|
-
apps.map((a) => a.name)
|
|
1398
|
+
succeedSpinner();
|
|
1399
|
+
if (!shouldSkipConfirmation()) {
|
|
1400
|
+
log("");
|
|
1401
|
+
log(`Database: ${colors.bold(dbInfo.name)}`);
|
|
1402
|
+
log(`Type: ${getTypeLabel(dbInfo.type)}`);
|
|
1403
|
+
log(`ID: ${colors.dim(dbInfo.id)}`);
|
|
1404
|
+
log("");
|
|
1405
|
+
const confirmed = await confirm(
|
|
1406
|
+
`Are you sure you want to delete "${dbInfo.name}"? This cannot be undone.`,
|
|
1407
|
+
false
|
|
1307
1408
|
);
|
|
1308
|
-
|
|
1409
|
+
if (!confirmed) {
|
|
1410
|
+
log("Cancelled.");
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1309
1413
|
}
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
});
|
|
1322
|
-
} else {
|
|
1323
|
-
await client2.envVariable.create.mutate({
|
|
1324
|
-
applicationId: app.applicationId,
|
|
1325
|
-
key,
|
|
1326
|
-
value,
|
|
1327
|
-
isSecret: options.secret
|
|
1328
|
-
});
|
|
1414
|
+
const _deleteSpinner = startSpinner("Deleting database...");
|
|
1415
|
+
switch (dbInfo.type) {
|
|
1416
|
+
case "postgres":
|
|
1417
|
+
await client2.postgres.remove.mutate({ postgresId: dbInfo.id });
|
|
1418
|
+
break;
|
|
1419
|
+
case "mysql":
|
|
1420
|
+
await client2.mysql.remove.mutate({ mysqlId: dbInfo.id });
|
|
1421
|
+
break;
|
|
1422
|
+
case "redis":
|
|
1423
|
+
await client2.redis.remove.mutate({ redisId: dbInfo.id });
|
|
1424
|
+
break;
|
|
1329
1425
|
}
|
|
1330
|
-
succeedSpinner(
|
|
1426
|
+
succeedSpinner("Database deleted!");
|
|
1331
1427
|
if (isJsonMode()) {
|
|
1332
|
-
outputData({
|
|
1428
|
+
outputData({ deleted: true, id: dbInfo.id });
|
|
1333
1429
|
} else {
|
|
1334
|
-
quietOutput(
|
|
1430
|
+
quietOutput(dbInfo.id);
|
|
1335
1431
|
}
|
|
1336
1432
|
} catch (err) {
|
|
1337
1433
|
handleError(err);
|
|
1338
1434
|
}
|
|
1339
1435
|
});
|
|
1340
|
-
|
|
1436
|
+
db.command("info").argument("<db>", "Database ID or name").description("Show database details and connection info").action(async (dbIdentifier) => {
|
|
1341
1437
|
try {
|
|
1342
1438
|
if (!isLoggedIn()) throw new AuthError();
|
|
1343
|
-
const appIdentifier = command.parent.parent.args[0];
|
|
1344
1439
|
const client2 = getApiClient();
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
1348
|
-
if (!
|
|
1440
|
+
const _spinner = startSpinner("Fetching database info...");
|
|
1441
|
+
const allDbs = await getAllDatabases(client2);
|
|
1442
|
+
const dbSummary = findDatabase(allDbs, dbIdentifier);
|
|
1443
|
+
if (!dbSummary) {
|
|
1349
1444
|
failSpinner();
|
|
1350
1445
|
const suggestions = findSimilar(
|
|
1351
|
-
|
|
1352
|
-
|
|
1446
|
+
dbIdentifier,
|
|
1447
|
+
allDbs.map((d) => d.name)
|
|
1353
1448
|
);
|
|
1354
|
-
throw new NotFoundError("
|
|
1449
|
+
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1355
1450
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1451
|
+
let dbDetails;
|
|
1452
|
+
switch (dbSummary.type) {
|
|
1453
|
+
case "postgres":
|
|
1454
|
+
dbDetails = await client2.postgres.one.query({
|
|
1455
|
+
postgresId: dbSummary.id
|
|
1456
|
+
});
|
|
1457
|
+
break;
|
|
1458
|
+
case "mysql":
|
|
1459
|
+
dbDetails = await client2.mysql.one.query({ mysqlId: dbSummary.id });
|
|
1460
|
+
break;
|
|
1461
|
+
case "redis":
|
|
1462
|
+
dbDetails = await client2.redis.one.query({ redisId: dbSummary.id });
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
succeedSpinner();
|
|
1361
1466
|
if (isJsonMode()) {
|
|
1362
|
-
outputData(
|
|
1467
|
+
outputData(dbDetails);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
log("");
|
|
1471
|
+
log(colors.bold(dbDetails.name));
|
|
1472
|
+
log(colors.dim(dbSummary.id));
|
|
1473
|
+
log("");
|
|
1474
|
+
log(`${colors.bold("Status")}`);
|
|
1475
|
+
log(` ${getStatusBadge(dbDetails.applicationStatus)}`);
|
|
1476
|
+
log(` Type: ${getTypeLabel(dbSummary.type)}`);
|
|
1477
|
+
log("");
|
|
1478
|
+
log(`${colors.bold("Connection")}`);
|
|
1479
|
+
if (dbSummary.type === "redis") {
|
|
1480
|
+
if (dbDetails.cloudHost) {
|
|
1481
|
+
log(` Host: ${colors.cyan(dbDetails.cloudHost)}`);
|
|
1482
|
+
log(` Port: ${dbDetails.cloudPort || 6379}`);
|
|
1483
|
+
if (dbDetails.cloudPassword) {
|
|
1484
|
+
log(` Password: ${colors.dim("********")}`);
|
|
1485
|
+
}
|
|
1486
|
+
} else {
|
|
1487
|
+
log(` ${colors.dim("Not yet deployed")}`);
|
|
1488
|
+
}
|
|
1363
1489
|
} else {
|
|
1364
|
-
|
|
1490
|
+
if (dbDetails.cloudInstanceId || dbDetails.databaseName) {
|
|
1491
|
+
log(` Host: ${colors.cyan(dbDetails.cloudHost || "localhost")}`);
|
|
1492
|
+
log(
|
|
1493
|
+
` Port: ${dbDetails.cloudPort || (dbSummary.type === "postgres" ? 5432 : 3306)}`
|
|
1494
|
+
);
|
|
1495
|
+
log(
|
|
1496
|
+
` Database: ${dbDetails.cloudDatabaseName || dbDetails.databaseName}`
|
|
1497
|
+
);
|
|
1498
|
+
log(
|
|
1499
|
+
` Username: ${dbDetails.cloudUsername || dbDetails.databaseUser}`
|
|
1500
|
+
);
|
|
1501
|
+
log(` Password: ${colors.dim("********")}`);
|
|
1502
|
+
} else {
|
|
1503
|
+
log(` ${colors.dim("Not yet deployed")}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
log("");
|
|
1507
|
+
if (dbDetails.cloudHost || dbDetails.databaseName) {
|
|
1508
|
+
log(`${colors.bold("Connection String")}`);
|
|
1509
|
+
const connStr = getConnectionString(dbSummary.type, dbDetails);
|
|
1510
|
+
log(` ${colors.cyan(connStr)}`);
|
|
1511
|
+
log("");
|
|
1365
1512
|
}
|
|
1513
|
+
log(`${colors.bold("Created")}`);
|
|
1514
|
+
log(` ${new Date(dbDetails.createdAt).toLocaleString()}`);
|
|
1515
|
+
log("");
|
|
1366
1516
|
} catch (err) {
|
|
1367
1517
|
handleError(err);
|
|
1368
1518
|
}
|
|
1369
1519
|
});
|
|
1370
|
-
|
|
1520
|
+
db.command("connect").argument("<db>", "Database ID or name").description("Open interactive database shell").action(async (dbIdentifier) => {
|
|
1371
1521
|
try {
|
|
1372
1522
|
if (!isLoggedIn()) throw new AuthError();
|
|
1373
|
-
const appIdentifier = command.parent.parent.args[0];
|
|
1374
1523
|
const client2 = getApiClient();
|
|
1375
|
-
const
|
|
1376
|
-
const
|
|
1377
|
-
const
|
|
1378
|
-
if (!
|
|
1524
|
+
const _spinner = startSpinner("Connecting to database...");
|
|
1525
|
+
const allDbs = await getAllDatabases(client2);
|
|
1526
|
+
const dbSummary = findDatabase(allDbs, dbIdentifier);
|
|
1527
|
+
if (!dbSummary) {
|
|
1379
1528
|
failSpinner();
|
|
1380
1529
|
const suggestions = findSimilar(
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
);
|
|
1384
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1385
|
-
}
|
|
1386
|
-
if (existsSync(options.output) && !shouldSkipConfirmation()) {
|
|
1387
|
-
succeedSpinner();
|
|
1388
|
-
const confirmed = await confirm(
|
|
1389
|
-
`File ${options.output} already exists. Overwrite?`,
|
|
1390
|
-
false
|
|
1530
|
+
dbIdentifier,
|
|
1531
|
+
allDbs.map((d) => d.name)
|
|
1391
1532
|
);
|
|
1392
|
-
|
|
1393
|
-
log("Cancelled.");
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
const result = await client2.envVariable.export.query({
|
|
1398
|
-
applicationId: app.applicationId,
|
|
1399
|
-
format: "dotenv",
|
|
1400
|
-
maskSecrets: !options.reveal
|
|
1401
|
-
});
|
|
1402
|
-
writeFileSync(options.output, result.content);
|
|
1403
|
-
succeedSpinner(`Saved to ${options.output}`);
|
|
1404
|
-
if (isJsonMode()) {
|
|
1405
|
-
outputData({ file: options.output, content: result.content });
|
|
1406
|
-
} else {
|
|
1407
|
-
quietOutput(options.output);
|
|
1533
|
+
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1408
1534
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1535
|
+
let dbDetails;
|
|
1536
|
+
switch (dbSummary.type) {
|
|
1537
|
+
case "postgres":
|
|
1538
|
+
dbDetails = await client2.postgres.one.query({
|
|
1539
|
+
postgresId: dbSummary.id
|
|
1540
|
+
});
|
|
1541
|
+
break;
|
|
1542
|
+
case "mysql":
|
|
1543
|
+
dbDetails = await client2.mysql.one.query({ mysqlId: dbSummary.id });
|
|
1544
|
+
break;
|
|
1545
|
+
case "redis":
|
|
1546
|
+
dbDetails = await client2.redis.one.query({ redisId: dbSummary.id });
|
|
1547
|
+
break;
|
|
1419
1548
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
if (!app) {
|
|
1426
|
-
failSpinner();
|
|
1427
|
-
const suggestions = findSimilar(
|
|
1428
|
-
appIdentifier,
|
|
1429
|
-
apps.map((a) => a.name)
|
|
1549
|
+
succeedSpinner();
|
|
1550
|
+
if (!dbDetails.cloudHost && !dbDetails.databaseName) {
|
|
1551
|
+
throw new CliError(
|
|
1552
|
+
"Database not deployed yet. Deploy first.",
|
|
1553
|
+
ExitCode.GENERAL_ERROR
|
|
1430
1554
|
);
|
|
1431
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1432
1555
|
}
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1556
|
+
const { command, args, env } = getConnectCommand(
|
|
1557
|
+
dbSummary.type,
|
|
1558
|
+
dbDetails
|
|
1559
|
+
);
|
|
1560
|
+
log("");
|
|
1561
|
+
log(`Connecting to ${colors.bold(dbDetails.name)}...`);
|
|
1562
|
+
log(colors.dim("Press Ctrl+D to exit"));
|
|
1563
|
+
log("");
|
|
1564
|
+
const child = spawn(command, args, {
|
|
1565
|
+
stdio: "inherit",
|
|
1566
|
+
env: { ...process.env, ...env }
|
|
1567
|
+
});
|
|
1568
|
+
child.on("exit", (code) => {
|
|
1569
|
+
process.exit(code || 0);
|
|
1438
1570
|
});
|
|
1439
|
-
succeedSpinner(`Imported ${result.imported} variables`);
|
|
1440
|
-
if (isJsonMode()) {
|
|
1441
|
-
outputData(result);
|
|
1442
|
-
} else {
|
|
1443
|
-
quietOutput(String(result.imported));
|
|
1444
|
-
if (result.skipped > 0) {
|
|
1445
|
-
log(colors.dim(`Skipped ${result.skipped} (already exist)`));
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
1571
|
} catch (err) {
|
|
1449
1572
|
handleError(err);
|
|
1450
1573
|
}
|
|
1451
1574
|
});
|
|
1452
1575
|
}
|
|
1453
|
-
function
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1576
|
+
async function getAllDatabases(client2) {
|
|
1577
|
+
const [postgres, mysql, redis] = await Promise.all([
|
|
1578
|
+
client2.postgres.allByOrganization.query(),
|
|
1579
|
+
client2.mysql.allByOrganization.query(),
|
|
1580
|
+
client2.redis.allByOrganization.query()
|
|
1581
|
+
]);
|
|
1582
|
+
const databases = [];
|
|
1583
|
+
for (const db of postgres) {
|
|
1584
|
+
databases.push({
|
|
1585
|
+
id: db.postgresId,
|
|
1586
|
+
name: db.name,
|
|
1587
|
+
type: "postgres",
|
|
1588
|
+
status: db.applicationStatus
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
for (const db of mysql) {
|
|
1592
|
+
databases.push({
|
|
1593
|
+
id: db.mysqlId,
|
|
1594
|
+
name: db.name,
|
|
1595
|
+
type: "mysql",
|
|
1596
|
+
status: db.applicationStatus
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
for (const db of redis) {
|
|
1600
|
+
databases.push({
|
|
1601
|
+
id: db.redisId,
|
|
1602
|
+
name: db.name,
|
|
1603
|
+
type: "redis",
|
|
1604
|
+
status: db.applicationStatus
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
return databases;
|
|
1608
|
+
}
|
|
1609
|
+
function findDatabase(databases, identifier) {
|
|
1610
|
+
const lowerIdentifier = identifier.toLowerCase();
|
|
1611
|
+
return databases.find(
|
|
1612
|
+
(db) => db.id === identifier || db.id.startsWith(identifier) || db.name.toLowerCase() === lowerIdentifier
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
function generateSlug2(name) {
|
|
1616
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
1617
|
+
}
|
|
1618
|
+
function formatDate2(date) {
|
|
1619
|
+
const d = new Date(date);
|
|
1620
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1621
|
+
}
|
|
1622
|
+
function getTypeLabel(type) {
|
|
1623
|
+
const labels = {
|
|
1624
|
+
postgres: colors.info("PostgreSQL"),
|
|
1625
|
+
mysql: colors.warn("MySQL"),
|
|
1626
|
+
redis: colors.error("Redis")
|
|
1627
|
+
};
|
|
1628
|
+
return labels[type] || type;
|
|
1629
|
+
}
|
|
1630
|
+
function getConnectionString(type, details) {
|
|
1631
|
+
const host = details.cloudHost || "localhost";
|
|
1632
|
+
const user = details.cloudUsername || details.databaseUser || "user";
|
|
1633
|
+
const dbName = details.cloudDatabaseName || details.databaseName || "db";
|
|
1634
|
+
switch (type) {
|
|
1635
|
+
case "postgres": {
|
|
1636
|
+
const pgPort = details.cloudPort || 5432;
|
|
1637
|
+
return `postgresql://${user}:****@${host}:${pgPort}/${dbName}`;
|
|
1638
|
+
}
|
|
1639
|
+
case "mysql": {
|
|
1640
|
+
const myPort = details.cloudPort || 3306;
|
|
1641
|
+
return `mysql://${user}:****@${host}:${myPort}/${dbName}`;
|
|
1642
|
+
}
|
|
1643
|
+
case "redis": {
|
|
1644
|
+
const redisPort = details.cloudPort || 6379;
|
|
1645
|
+
return `redis://:****@${host}:${redisPort}`;
|
|
1646
|
+
}
|
|
1647
|
+
default:
|
|
1648
|
+
return "";
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function getConnectCommand(type, details) {
|
|
1652
|
+
const host = details.cloudHost || "localhost";
|
|
1653
|
+
const user = details.cloudUsername || details.databaseUser || "user";
|
|
1654
|
+
const password = details.cloudPassword || details.databasePassword || "";
|
|
1655
|
+
const dbName = details.cloudDatabaseName || details.databaseName || "db";
|
|
1656
|
+
switch (type) {
|
|
1657
|
+
case "postgres":
|
|
1658
|
+
return {
|
|
1659
|
+
command: "psql",
|
|
1660
|
+
args: [
|
|
1661
|
+
"-h",
|
|
1662
|
+
host,
|
|
1663
|
+
"-p",
|
|
1664
|
+
String(details.cloudPort || 5432),
|
|
1665
|
+
"-U",
|
|
1666
|
+
user,
|
|
1667
|
+
"-d",
|
|
1668
|
+
dbName
|
|
1669
|
+
],
|
|
1670
|
+
env: { PGPASSWORD: password }
|
|
1671
|
+
};
|
|
1672
|
+
case "mysql":
|
|
1673
|
+
return {
|
|
1674
|
+
command: "mysql",
|
|
1675
|
+
args: [
|
|
1676
|
+
"-h",
|
|
1677
|
+
host,
|
|
1678
|
+
"-P",
|
|
1679
|
+
String(details.cloudPort || 3306),
|
|
1680
|
+
"-u",
|
|
1681
|
+
user,
|
|
1682
|
+
`-p${password}`,
|
|
1683
|
+
dbName
|
|
1684
|
+
],
|
|
1685
|
+
env: {}
|
|
1686
|
+
};
|
|
1687
|
+
case "redis":
|
|
1688
|
+
return {
|
|
1689
|
+
command: "redis-cli",
|
|
1690
|
+
args: [
|
|
1691
|
+
"-h",
|
|
1692
|
+
host,
|
|
1693
|
+
"-p",
|
|
1694
|
+
String(details.cloudPort || 6379),
|
|
1695
|
+
...password ? ["-a", password] : []
|
|
1696
|
+
],
|
|
1697
|
+
env: {}
|
|
1698
|
+
};
|
|
1699
|
+
default:
|
|
1700
|
+
throw new CliError(
|
|
1701
|
+
`Unsupported database type: ${type}`,
|
|
1702
|
+
ExitCode.INVALID_ARGUMENTS
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/lib/websocket.ts
|
|
1708
|
+
import WebSocket from "ws";
|
|
1709
|
+
function streamDeploymentLogs(logPath, options) {
|
|
1710
|
+
const token = getToken();
|
|
1711
|
+
const apiUrl = getApiUrl();
|
|
1712
|
+
const wsUrl = apiUrl.replace(/^http/, "ws");
|
|
1713
|
+
const fullUrl = `${wsUrl}/listen-deployment?logPath=${encodeURIComponent(logPath)}`;
|
|
1714
|
+
const ws = new WebSocket(fullUrl, {
|
|
1715
|
+
headers: {
|
|
1716
|
+
"x-api-key": token || ""
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
let buffer = "";
|
|
1720
|
+
const done = new Promise((resolve) => {
|
|
1721
|
+
ws.on("close", (code, reason) => {
|
|
1722
|
+
if (buffer.length > 0) {
|
|
1723
|
+
options.onData(buffer);
|
|
1724
|
+
}
|
|
1725
|
+
options.onClose?.(code, reason.toString());
|
|
1726
|
+
resolve({ code, reason: reason.toString() });
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
ws.on("open", () => {
|
|
1730
|
+
options.onOpen?.();
|
|
1731
|
+
});
|
|
1732
|
+
ws.on("message", (data) => {
|
|
1733
|
+
buffer += data.toString();
|
|
1734
|
+
const lines = buffer.split("\n");
|
|
1735
|
+
buffer = lines.pop() || "";
|
|
1736
|
+
for (const line of lines) {
|
|
1737
|
+
if (line.length > 0) {
|
|
1738
|
+
options.onData(line);
|
|
1495
1739
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
ws.on("error", (error2) => {
|
|
1743
|
+
options.onError?.(error2);
|
|
1744
|
+
});
|
|
1745
|
+
const cleanup = () => {
|
|
1746
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1747
|
+
ws.close();
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
return { cleanup, done };
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/commands/deploy.ts
|
|
1754
|
+
function registerDeployCommands(program2) {
|
|
1755
|
+
program2.command("deploy").argument("<app>", "Application ID or name").description("Deploy an application").option("-r, --region <region>", "Deployment region", "me-central1").option("-w, --wait", "Wait for deployment to complete and stream logs").action(async (appIdentifier, options) => {
|
|
1756
|
+
try {
|
|
1757
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1758
|
+
const client2 = getApiClient();
|
|
1759
|
+
const _spinner = startSpinner("Finding application...");
|
|
1760
|
+
const apps = await client2.application.allByOrganization.query();
|
|
1761
|
+
const app = findApp2(apps, appIdentifier);
|
|
1762
|
+
if (!app) {
|
|
1763
|
+
failSpinner();
|
|
1764
|
+
const suggestions = findSimilar(
|
|
1765
|
+
appIdentifier,
|
|
1766
|
+
apps.map((a) => a.name)
|
|
1505
1767
|
);
|
|
1768
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1506
1769
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1770
|
+
updateSpinner(`Deploying ${app.name}...`);
|
|
1771
|
+
const result = await client2.application.deployToCloud.mutate({
|
|
1772
|
+
applicationId: app.applicationId,
|
|
1773
|
+
region: options.region
|
|
1774
|
+
});
|
|
1775
|
+
if (options.wait) {
|
|
1776
|
+
await streamDeploymentWithLogs(
|
|
1777
|
+
client2,
|
|
1778
|
+
result.deploymentId,
|
|
1779
|
+
app.name,
|
|
1780
|
+
app.applicationId
|
|
1516
1781
|
);
|
|
1517
|
-
}
|
|
1518
|
-
if (isJsonMode()) {
|
|
1519
|
-
outputData(databases);
|
|
1520
1782
|
return;
|
|
1521
1783
|
}
|
|
1522
|
-
|
|
1784
|
+
succeedSpinner("Deployment started!");
|
|
1785
|
+
if (isJsonMode()) {
|
|
1786
|
+
outputData({
|
|
1787
|
+
deploymentId: result.deploymentId,
|
|
1788
|
+
status: "deploying"
|
|
1789
|
+
});
|
|
1790
|
+
} else {
|
|
1791
|
+
quietOutput(result.deploymentId);
|
|
1523
1792
|
log("");
|
|
1524
|
-
log(
|
|
1793
|
+
log(`Deployment ID: ${colors.cyan(result.deploymentId)}`);
|
|
1794
|
+
log("");
|
|
1795
|
+
log("Deployment is running in the background.");
|
|
1796
|
+
log(
|
|
1797
|
+
`Check status: ${colors.dim(`tarout deploy:status ${app.applicationId.slice(0, 8)}`)}`
|
|
1798
|
+
);
|
|
1799
|
+
log(
|
|
1800
|
+
`View logs: ${colors.dim(`tarout deploy:logs ${result.deploymentId.slice(0, 8)}`)}`
|
|
1801
|
+
);
|
|
1525
1802
|
log("");
|
|
1526
|
-
log(`Create one with: ${colors.dim("tarout db create <name>")}`);
|
|
1527
|
-
return;
|
|
1528
1803
|
}
|
|
1529
|
-
log("");
|
|
1530
|
-
table(
|
|
1531
|
-
["ID", "NAME", "TYPE", "STATUS", "CREATED"],
|
|
1532
|
-
databases.map((db2) => [
|
|
1533
|
-
colors.cyan(db2.id.slice(0, 8)),
|
|
1534
|
-
db2.name,
|
|
1535
|
-
getTypeLabel(db2.type),
|
|
1536
|
-
getStatusBadge(db2.status),
|
|
1537
|
-
formatDate4(db2.created)
|
|
1538
|
-
])
|
|
1539
|
-
);
|
|
1540
|
-
log("");
|
|
1541
|
-
log(
|
|
1542
|
-
colors.dim(`${databases.length} database${databases.length === 1 ? "" : "s"}`)
|
|
1543
|
-
);
|
|
1544
1804
|
} catch (err) {
|
|
1545
1805
|
handleError(err);
|
|
1546
1806
|
}
|
|
1547
1807
|
});
|
|
1548
|
-
|
|
1808
|
+
program2.command("deploy:status").argument("<app>", "Application ID or name").description("Check deployment status").action(async (appIdentifier) => {
|
|
1549
1809
|
try {
|
|
1550
1810
|
if (!isLoggedIn()) throw new AuthError();
|
|
1551
|
-
const profile = getCurrentProfile();
|
|
1552
|
-
if (!profile) throw new AuthError();
|
|
1553
|
-
let dbName = name;
|
|
1554
|
-
let dbType = options.type;
|
|
1555
|
-
if (!dbName) {
|
|
1556
|
-
dbName = await input("Database name:");
|
|
1557
|
-
}
|
|
1558
|
-
if (!options.type && !shouldSkipConfirmation()) {
|
|
1559
|
-
dbType = await select("Database type:", [
|
|
1560
|
-
{ name: "PostgreSQL", value: "postgres" },
|
|
1561
|
-
{ name: "MySQL", value: "mysql" },
|
|
1562
|
-
{ name: "Redis", value: "redis" }
|
|
1563
|
-
]);
|
|
1564
|
-
}
|
|
1565
|
-
const slug = generateSlug2(dbName);
|
|
1566
1811
|
const client2 = getApiClient();
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
});
|
|
1578
|
-
break;
|
|
1579
|
-
case "mysql":
|
|
1580
|
-
database = await client2.mysql.create.mutate({
|
|
1581
|
-
name: dbName,
|
|
1582
|
-
appName: slug,
|
|
1583
|
-
dockerImage: "mysql:8",
|
|
1584
|
-
organizationId: profile.organizationId,
|
|
1585
|
-
description: options.description
|
|
1586
|
-
});
|
|
1587
|
-
break;
|
|
1588
|
-
case "redis":
|
|
1589
|
-
database = await client2.redis.create.mutate({
|
|
1590
|
-
name: dbName,
|
|
1591
|
-
appName: slug,
|
|
1592
|
-
dockerImage: "redis:7",
|
|
1593
|
-
organizationId: profile.organizationId,
|
|
1594
|
-
description: options.description
|
|
1595
|
-
});
|
|
1596
|
-
break;
|
|
1597
|
-
default:
|
|
1598
|
-
throw new CliError(`Unsupported database type: ${dbType}`, ExitCode.INVALID_ARGUMENTS);
|
|
1812
|
+
const _spinner = startSpinner("Fetching status...");
|
|
1813
|
+
const apps = await client2.application.allByOrganization.query();
|
|
1814
|
+
const appSummary = findApp2(apps, appIdentifier);
|
|
1815
|
+
if (!appSummary) {
|
|
1816
|
+
failSpinner();
|
|
1817
|
+
const suggestions = findSimilar(
|
|
1818
|
+
appIdentifier,
|
|
1819
|
+
apps.map((a) => a.name)
|
|
1820
|
+
);
|
|
1821
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1599
1822
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1823
|
+
const app = await client2.application.one.query({
|
|
1824
|
+
applicationId: appSummary.applicationId
|
|
1825
|
+
});
|
|
1826
|
+
const cloudStatus = await client2.application.getCloudDeploymentStatus.query({
|
|
1827
|
+
applicationId: appSummary.applicationId
|
|
1828
|
+
});
|
|
1829
|
+
succeedSpinner();
|
|
1602
1830
|
if (isJsonMode()) {
|
|
1603
|
-
outputData(
|
|
1831
|
+
outputData({
|
|
1832
|
+
applicationId: app.applicationId,
|
|
1833
|
+
name: app.name,
|
|
1834
|
+
status: app.applicationStatus,
|
|
1835
|
+
url: app.cloudServiceUrl,
|
|
1836
|
+
cloudStatus
|
|
1837
|
+
});
|
|
1604
1838
|
return;
|
|
1605
1839
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1840
|
+
log("");
|
|
1841
|
+
log(`${colors.bold(app.name)}`);
|
|
1842
|
+
log("");
|
|
1843
|
+
log(`Status: ${getStatusBadge(app.applicationStatus)}`);
|
|
1844
|
+
if (app.cloudServiceUrl) {
|
|
1845
|
+
log(`URL: ${colors.cyan(app.cloudServiceUrl)}`);
|
|
1846
|
+
}
|
|
1847
|
+
if (cloudStatus) {
|
|
1848
|
+
log(`Provider: ${cloudStatus.provider}`);
|
|
1849
|
+
log(`Region: ${cloudStatus.region}`);
|
|
1850
|
+
log(`Updated: ${new Date(cloudStatus.updatedAt).toLocaleString()}`);
|
|
1851
|
+
}
|
|
1614
1852
|
log("");
|
|
1615
1853
|
} catch (err) {
|
|
1616
1854
|
handleError(err);
|
|
1617
1855
|
}
|
|
1618
1856
|
});
|
|
1619
|
-
|
|
1857
|
+
program2.command("deploy:cancel").argument("<app>", "Application ID or name").description("Cancel a running deployment").action(async (appIdentifier) => {
|
|
1620
1858
|
try {
|
|
1621
1859
|
if (!isLoggedIn()) throw new AuthError();
|
|
1622
1860
|
const client2 = getApiClient();
|
|
1623
|
-
const
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1626
|
-
if (!
|
|
1861
|
+
const _spinner = startSpinner("Cancelling deployment...");
|
|
1862
|
+
const apps = await client2.application.allByOrganization.query();
|
|
1863
|
+
const app = findApp2(apps, appIdentifier);
|
|
1864
|
+
if (!app) {
|
|
1627
1865
|
failSpinner();
|
|
1628
1866
|
const suggestions = findSimilar(
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
);
|
|
1632
|
-
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1633
|
-
}
|
|
1634
|
-
succeedSpinner();
|
|
1635
|
-
if (!shouldSkipConfirmation()) {
|
|
1636
|
-
log("");
|
|
1637
|
-
log(`Database: ${colors.bold(dbInfo.name)}`);
|
|
1638
|
-
log(`Type: ${getTypeLabel(dbInfo.type)}`);
|
|
1639
|
-
log(`ID: ${colors.dim(dbInfo.id)}`);
|
|
1640
|
-
log("");
|
|
1641
|
-
const confirmed = await confirm(
|
|
1642
|
-
`Are you sure you want to delete "${dbInfo.name}"? This cannot be undone.`,
|
|
1643
|
-
false
|
|
1867
|
+
appIdentifier,
|
|
1868
|
+
apps.map((a) => a.name)
|
|
1644
1869
|
);
|
|
1645
|
-
|
|
1646
|
-
log("Cancelled.");
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
const deleteSpinner = startSpinner("Deleting database...");
|
|
1651
|
-
switch (dbInfo.type) {
|
|
1652
|
-
case "postgres":
|
|
1653
|
-
await client2.postgres.remove.mutate({ postgresId: dbInfo.id });
|
|
1654
|
-
break;
|
|
1655
|
-
case "mysql":
|
|
1656
|
-
await client2.mysql.remove.mutate({ mysqlId: dbInfo.id });
|
|
1657
|
-
break;
|
|
1658
|
-
case "redis":
|
|
1659
|
-
await client2.redis.remove.mutate({ redisId: dbInfo.id });
|
|
1660
|
-
break;
|
|
1870
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1661
1871
|
}
|
|
1662
|
-
|
|
1872
|
+
await client2.application.cancelDeployment.mutate({
|
|
1873
|
+
applicationId: app.applicationId
|
|
1874
|
+
});
|
|
1875
|
+
succeedSpinner("Deployment cancelled");
|
|
1663
1876
|
if (isJsonMode()) {
|
|
1664
|
-
outputData({
|
|
1665
|
-
} else {
|
|
1666
|
-
quietOutput(dbInfo.id);
|
|
1877
|
+
outputData({ cancelled: true, applicationId: app.applicationId });
|
|
1667
1878
|
}
|
|
1668
1879
|
} catch (err) {
|
|
1669
1880
|
handleError(err);
|
|
1670
1881
|
}
|
|
1671
1882
|
});
|
|
1672
|
-
|
|
1883
|
+
program2.command("deploy:list").argument("<app>", "Application ID or name").description("List recent deployments").option("-n, --limit <number>", "Number of deployments to show", "10").action(async (appIdentifier, options) => {
|
|
1673
1884
|
try {
|
|
1674
1885
|
if (!isLoggedIn()) throw new AuthError();
|
|
1675
1886
|
const client2 = getApiClient();
|
|
1676
|
-
const
|
|
1677
|
-
const
|
|
1678
|
-
const
|
|
1679
|
-
if (!
|
|
1887
|
+
const _spinner = startSpinner("Fetching deployments...");
|
|
1888
|
+
const apps = await client2.application.allByOrganization.query();
|
|
1889
|
+
const appSummary = findApp2(apps, appIdentifier);
|
|
1890
|
+
if (!appSummary) {
|
|
1680
1891
|
failSpinner();
|
|
1681
1892
|
const suggestions = findSimilar(
|
|
1682
|
-
|
|
1683
|
-
|
|
1893
|
+
appIdentifier,
|
|
1894
|
+
apps.map((a) => a.name)
|
|
1684
1895
|
);
|
|
1685
|
-
throw new NotFoundError("
|
|
1686
|
-
}
|
|
1687
|
-
let dbDetails;
|
|
1688
|
-
switch (dbSummary.type) {
|
|
1689
|
-
case "postgres":
|
|
1690
|
-
dbDetails = await client2.postgres.one.query({ postgresId: dbSummary.id });
|
|
1691
|
-
break;
|
|
1692
|
-
case "mysql":
|
|
1693
|
-
dbDetails = await client2.mysql.one.query({ mysqlId: dbSummary.id });
|
|
1694
|
-
break;
|
|
1695
|
-
case "redis":
|
|
1696
|
-
dbDetails = await client2.redis.one.query({ redisId: dbSummary.id });
|
|
1697
|
-
break;
|
|
1896
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1698
1897
|
}
|
|
1898
|
+
const deployments = await client2.deployment.all.query({
|
|
1899
|
+
applicationId: appSummary.applicationId
|
|
1900
|
+
});
|
|
1699
1901
|
succeedSpinner();
|
|
1902
|
+
const limitedDeployments = deployments.slice(
|
|
1903
|
+
0,
|
|
1904
|
+
Number.parseInt(options.limit, 10)
|
|
1905
|
+
);
|
|
1700
1906
|
if (isJsonMode()) {
|
|
1701
|
-
outputData(
|
|
1907
|
+
outputData(limitedDeployments);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (limitedDeployments.length === 0) {
|
|
1911
|
+
log("");
|
|
1912
|
+
log("No deployments found.");
|
|
1913
|
+
log("");
|
|
1914
|
+
log(
|
|
1915
|
+
`Deploy with: ${colors.dim(`tarout deploy ${appSummary.applicationId.slice(0, 8)}`)}`
|
|
1916
|
+
);
|
|
1702
1917
|
return;
|
|
1703
1918
|
}
|
|
1704
1919
|
log("");
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1920
|
+
table(
|
|
1921
|
+
["ID", "STATUS", "TITLE", "CREATED"],
|
|
1922
|
+
limitedDeployments.map((d) => [
|
|
1923
|
+
colors.cyan(d.deploymentId.slice(0, 8)),
|
|
1924
|
+
getStatusBadge(d.status),
|
|
1925
|
+
d.title || colors.dim("-"),
|
|
1926
|
+
formatDate3(d.createdAt)
|
|
1927
|
+
])
|
|
1928
|
+
);
|
|
1711
1929
|
log("");
|
|
1712
|
-
log(
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1930
|
+
log(
|
|
1931
|
+
colors.dim(
|
|
1932
|
+
`${limitedDeployments.length} deployment${limitedDeployments.length === 1 ? "" : "s"}`
|
|
1933
|
+
)
|
|
1934
|
+
);
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
handleError(err);
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
program2.command("deploy:logs").argument("<deployment-id>", "Deployment ID").description("View deployment logs").option(
|
|
1940
|
+
"-f, --follow",
|
|
1941
|
+
"Stream logs in real-time (for running deployments)"
|
|
1942
|
+
).option("--no-stream", "Fetch logs via HTTP instead of WebSocket").action(async (deploymentId, options) => {
|
|
1943
|
+
try {
|
|
1944
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1945
|
+
const client2 = getApiClient();
|
|
1946
|
+
const _spinner = startSpinner("Fetching deployment...");
|
|
1947
|
+
let deployment;
|
|
1948
|
+
try {
|
|
1949
|
+
deployment = await client2.deployment.one.query({ deploymentId });
|
|
1950
|
+
} catch {
|
|
1951
|
+
failSpinner();
|
|
1952
|
+
throw new NotFoundError("Deployment", deploymentId);
|
|
1953
|
+
}
|
|
1954
|
+
succeedSpinner();
|
|
1955
|
+
const isRunning = deployment.status === "running";
|
|
1956
|
+
const shouldStream = options.follow || isRunning && options.stream !== false;
|
|
1957
|
+
if (shouldStream && deployment.logPath) {
|
|
1958
|
+
log("");
|
|
1959
|
+
log(
|
|
1960
|
+
colors.dim(
|
|
1961
|
+
`Streaming logs for deployment ${colors.cyan(deployment.deploymentId.slice(0, 8))}...`
|
|
1962
|
+
)
|
|
1963
|
+
);
|
|
1964
|
+
log(colors.dim("Press Ctrl+C to stop"));
|
|
1965
|
+
log("");
|
|
1966
|
+
const logLines = [];
|
|
1967
|
+
const errors = [];
|
|
1968
|
+
let finalStatus = deployment.status;
|
|
1969
|
+
const { cleanup, done } = streamDeploymentLogs(deployment.logPath, {
|
|
1970
|
+
onData: (line) => {
|
|
1971
|
+
logLines.push(line);
|
|
1972
|
+
if (isErrorLine(line)) {
|
|
1973
|
+
errors.push(line);
|
|
1974
|
+
}
|
|
1975
|
+
if (!isJsonMode()) {
|
|
1976
|
+
printLogLine(line);
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
onError: (error2) => {
|
|
1980
|
+
if (!isJsonMode()) {
|
|
1981
|
+
log(colors.error(`WebSocket error: ${error2.message}`));
|
|
1982
|
+
}
|
|
1719
1983
|
}
|
|
1984
|
+
});
|
|
1985
|
+
process.on("SIGINT", () => {
|
|
1986
|
+
cleanup();
|
|
1987
|
+
if (!isJsonMode()) {
|
|
1988
|
+
log("");
|
|
1989
|
+
log(colors.dim("Log streaming stopped"));
|
|
1990
|
+
}
|
|
1991
|
+
process.exit(0);
|
|
1992
|
+
});
|
|
1993
|
+
await done;
|
|
1994
|
+
const finalDeployment = await client2.deployment.one.query({
|
|
1995
|
+
deploymentId
|
|
1996
|
+
});
|
|
1997
|
+
finalStatus = finalDeployment.status;
|
|
1998
|
+
if (isJsonMode()) {
|
|
1999
|
+
const analysis = finalStatus === "error" ? analyzeDeploymentError(logLines, finalDeployment.errorMessage) : void 0;
|
|
2000
|
+
outputData({
|
|
2001
|
+
deploymentId: deployment.deploymentId,
|
|
2002
|
+
status: finalStatus,
|
|
2003
|
+
logs: logLines,
|
|
2004
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
2005
|
+
errorAnalysis: analysis,
|
|
2006
|
+
application: deployment.application
|
|
2007
|
+
});
|
|
1720
2008
|
} else {
|
|
1721
|
-
log(
|
|
2009
|
+
log("");
|
|
2010
|
+
log(
|
|
2011
|
+
`Final status: ${getStatusBadge(finalStatus)} ${finalStatus === "error" && finalDeployment.errorMessage ? `- ${finalDeployment.errorMessage}` : ""}`
|
|
2012
|
+
);
|
|
1722
2013
|
}
|
|
1723
2014
|
} else {
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2015
|
+
const logsResult = await client2.deployment.getDeploymentLogs.query({
|
|
2016
|
+
deploymentId,
|
|
2017
|
+
offset: 0,
|
|
2018
|
+
limit: 5e3
|
|
2019
|
+
});
|
|
2020
|
+
if (isJsonMode()) {
|
|
2021
|
+
const errors = logsResult.lines.filter(isErrorLine);
|
|
2022
|
+
const analysis = deployment.status === "error" ? analyzeDeploymentError(
|
|
2023
|
+
logsResult.lines,
|
|
2024
|
+
deployment.errorMessage
|
|
2025
|
+
) : void 0;
|
|
2026
|
+
outputData({
|
|
2027
|
+
deploymentId: deployment.deploymentId,
|
|
2028
|
+
status: deployment.status,
|
|
2029
|
+
logs: logsResult.lines,
|
|
2030
|
+
totalLines: logsResult.totalLines,
|
|
2031
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
2032
|
+
errorAnalysis: analysis,
|
|
2033
|
+
application: deployment.application
|
|
2034
|
+
});
|
|
2035
|
+
return;
|
|
1732
2036
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
log(
|
|
2037
|
+
if (logsResult.lines.length === 0) {
|
|
2038
|
+
log("");
|
|
2039
|
+
log("No logs available for this deployment.");
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
log("");
|
|
2043
|
+
log(
|
|
2044
|
+
colors.dim(
|
|
2045
|
+
`Logs for deployment ${colors.cyan(deployment.deploymentId.slice(0, 8))} (${logsResult.totalLines} lines):`
|
|
2046
|
+
)
|
|
2047
|
+
);
|
|
1739
2048
|
log("");
|
|
2049
|
+
for (const line of logsResult.lines) {
|
|
2050
|
+
printLogLine(line);
|
|
2051
|
+
}
|
|
2052
|
+
if (logsResult.hasMore) {
|
|
2053
|
+
log("");
|
|
2054
|
+
log(
|
|
2055
|
+
colors.dim(
|
|
2056
|
+
`Showing ${logsResult.lines.length} of ${logsResult.totalLines} lines`
|
|
2057
|
+
)
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
log("");
|
|
2061
|
+
log(`Status: ${getStatusBadge(deployment.status)}`);
|
|
1740
2062
|
}
|
|
1741
|
-
log(`${colors.bold("Created")}`);
|
|
1742
|
-
log(` ${new Date(dbDetails.createdAt).toLocaleString()}`);
|
|
1743
|
-
log("");
|
|
1744
2063
|
} catch (err) {
|
|
1745
2064
|
handleError(err);
|
|
1746
2065
|
}
|
|
1747
2066
|
});
|
|
1748
|
-
|
|
2067
|
+
}
|
|
2068
|
+
function registerLogsCommand(program2) {
|
|
2069
|
+
program2.command("logs").argument("<app>", "Application ID or name").description("View application logs").option(
|
|
2070
|
+
"-l, --level <level>",
|
|
2071
|
+
"Log level (ALL, ERROR, WARN, INFO, DEBUG)",
|
|
2072
|
+
"ALL"
|
|
2073
|
+
).option("-f, --follow", "Stream logs continuously").option("-n, --limit <number>", "Number of logs to show", "100").option("--since <duration>", "Show logs since (e.g., 1h, 30m, 2d)").action(async (appIdentifier, options) => {
|
|
1749
2074
|
try {
|
|
1750
2075
|
if (!isLoggedIn()) throw new AuthError();
|
|
1751
2076
|
const client2 = getApiClient();
|
|
1752
|
-
const
|
|
1753
|
-
const
|
|
1754
|
-
const
|
|
1755
|
-
if (!
|
|
2077
|
+
const _spinner = startSpinner("Fetching logs...");
|
|
2078
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2079
|
+
const app = findApp2(apps, appIdentifier);
|
|
2080
|
+
if (!app) {
|
|
1756
2081
|
failSpinner();
|
|
1757
2082
|
const suggestions = findSimilar(
|
|
1758
|
-
|
|
1759
|
-
|
|
2083
|
+
appIdentifier,
|
|
2084
|
+
apps.map((a) => a.name)
|
|
1760
2085
|
);
|
|
1761
|
-
throw new NotFoundError("
|
|
2086
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1762
2087
|
}
|
|
1763
|
-
let
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
dbDetails = await client2.redis.one.query({ redisId: dbSummary.id });
|
|
1773
|
-
break;
|
|
2088
|
+
let timeRange;
|
|
2089
|
+
if (options.since) {
|
|
2090
|
+
const since = parseDuration(options.since);
|
|
2091
|
+
if (since) {
|
|
2092
|
+
timeRange = {
|
|
2093
|
+
start: new Date(Date.now() - since).toISOString(),
|
|
2094
|
+
end: null
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
1774
2097
|
}
|
|
2098
|
+
const logs = await client2.logs.getCloudRunLogs.query({
|
|
2099
|
+
applicationId: app.applicationId,
|
|
2100
|
+
level: options.level.toUpperCase(),
|
|
2101
|
+
limit: Number.parseInt(options.limit, 10),
|
|
2102
|
+
timeRange
|
|
2103
|
+
});
|
|
1775
2104
|
succeedSpinner();
|
|
1776
|
-
if (
|
|
1777
|
-
|
|
2105
|
+
if (isJsonMode()) {
|
|
2106
|
+
outputData(logs);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
if (!logs.entries || logs.entries.length === 0) {
|
|
2110
|
+
log("");
|
|
2111
|
+
log("No logs found.");
|
|
2112
|
+
return;
|
|
1778
2113
|
}
|
|
1779
|
-
const { command, args, env } = getConnectCommand(dbSummary.type, dbDetails);
|
|
1780
|
-
log("");
|
|
1781
|
-
log(`Connecting to ${colors.bold(dbDetails.name)}...`);
|
|
1782
|
-
log(colors.dim("Press Ctrl+D to exit"));
|
|
1783
2114
|
log("");
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
2115
|
+
for (const entry of logs.entries) {
|
|
2116
|
+
printLogEntry(entry);
|
|
2117
|
+
}
|
|
2118
|
+
if (options.follow) {
|
|
2119
|
+
log("");
|
|
2120
|
+
log(colors.dim("Streaming logs... (Ctrl+C to stop)"));
|
|
2121
|
+
log("");
|
|
2122
|
+
let lastTimestamp = logs.entries.length > 0 ? new Date(
|
|
2123
|
+
logs.entries[logs.entries.length - 1].timestamp
|
|
2124
|
+
).getTime() : Date.now();
|
|
2125
|
+
while (true) {
|
|
2126
|
+
await sleep(2e3);
|
|
2127
|
+
const newLogs = await client2.logs.getCloudRunLogs.query({
|
|
2128
|
+
applicationId: app.applicationId,
|
|
2129
|
+
level: options.level.toUpperCase(),
|
|
2130
|
+
limit: 50,
|
|
2131
|
+
timeRange: {
|
|
2132
|
+
start: new Date(lastTimestamp + 1).toISOString(),
|
|
2133
|
+
end: null
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
if (newLogs.entries && newLogs.entries.length > 0) {
|
|
2137
|
+
for (const entry of newLogs.entries) {
|
|
2138
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
2139
|
+
if (entryTime > lastTimestamp) {
|
|
2140
|
+
printLogEntry(entry);
|
|
2141
|
+
lastTimestamp = entryTime;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1791
2147
|
} catch (err) {
|
|
1792
2148
|
handleError(err);
|
|
1793
2149
|
}
|
|
1794
2150
|
});
|
|
1795
2151
|
}
|
|
1796
|
-
|
|
1797
|
-
const [postgres, mysql, redis] = await Promise.all([
|
|
1798
|
-
client2.postgres.allByOrganization.query(),
|
|
1799
|
-
client2.mysql.allByOrganization.query(),
|
|
1800
|
-
client2.redis.allByOrganization.query()
|
|
1801
|
-
]);
|
|
1802
|
-
const databases = [];
|
|
1803
|
-
for (const db of postgres) {
|
|
1804
|
-
databases.push({
|
|
1805
|
-
id: db.postgresId,
|
|
1806
|
-
name: db.name,
|
|
1807
|
-
type: "postgres",
|
|
1808
|
-
status: db.applicationStatus
|
|
1809
|
-
});
|
|
1810
|
-
}
|
|
1811
|
-
for (const db of mysql) {
|
|
1812
|
-
databases.push({
|
|
1813
|
-
id: db.mysqlId,
|
|
1814
|
-
name: db.name,
|
|
1815
|
-
type: "mysql",
|
|
1816
|
-
status: db.applicationStatus
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
for (const db of redis) {
|
|
1820
|
-
databases.push({
|
|
1821
|
-
id: db.redisId,
|
|
1822
|
-
name: db.name,
|
|
1823
|
-
type: "redis",
|
|
1824
|
-
status: db.applicationStatus
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
return databases;
|
|
1828
|
-
}
|
|
1829
|
-
function findDatabase(databases, identifier) {
|
|
2152
|
+
function findApp2(apps, identifier) {
|
|
1830
2153
|
const lowerIdentifier = identifier.toLowerCase();
|
|
1831
|
-
return
|
|
1832
|
-
(
|
|
2154
|
+
return apps.find(
|
|
2155
|
+
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
1833
2156
|
);
|
|
1834
2157
|
}
|
|
1835
|
-
function
|
|
1836
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
1837
|
-
}
|
|
1838
|
-
function formatDate4(date) {
|
|
2158
|
+
function formatDate3(date) {
|
|
1839
2159
|
const d = new Date(date);
|
|
1840
|
-
return d.
|
|
2160
|
+
return d.toLocaleString("en-US", {
|
|
2161
|
+
month: "short",
|
|
2162
|
+
day: "numeric",
|
|
2163
|
+
hour: "2-digit",
|
|
2164
|
+
minute: "2-digit"
|
|
2165
|
+
});
|
|
1841
2166
|
}
|
|
1842
|
-
function
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2167
|
+
function sleep(ms) {
|
|
2168
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2169
|
+
}
|
|
2170
|
+
function parseDuration(duration) {
|
|
2171
|
+
const match = duration.match(/^(\d+)(s|m|h|d)$/);
|
|
2172
|
+
if (!match) return null;
|
|
2173
|
+
const value = Number.parseInt(match[1], 10);
|
|
2174
|
+
const unit = match[2];
|
|
2175
|
+
const multipliers = {
|
|
2176
|
+
s: 1e3,
|
|
2177
|
+
m: 60 * 1e3,
|
|
2178
|
+
h: 60 * 60 * 1e3,
|
|
2179
|
+
d: 24 * 60 * 60 * 1e3
|
|
1847
2180
|
};
|
|
1848
|
-
return
|
|
2181
|
+
return value * (multipliers[unit] || 0);
|
|
1849
2182
|
}
|
|
1850
|
-
function
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2183
|
+
function printLogEntry(entry) {
|
|
2184
|
+
const time = new Date(entry.timestamp);
|
|
2185
|
+
const timeStr = time.toLocaleTimeString("en-US", {
|
|
2186
|
+
hour12: false,
|
|
2187
|
+
hour: "2-digit",
|
|
2188
|
+
minute: "2-digit",
|
|
2189
|
+
second: "2-digit"
|
|
2190
|
+
});
|
|
2191
|
+
const levelColors = {
|
|
2192
|
+
ERROR: colors.error,
|
|
2193
|
+
WARN: colors.warn,
|
|
2194
|
+
INFO: colors.info,
|
|
2195
|
+
DEBUG: colors.dim,
|
|
2196
|
+
DEFAULT: colors.dim
|
|
2197
|
+
};
|
|
2198
|
+
const colorFn = levelColors[entry.severity] || levelColors.DEFAULT;
|
|
2199
|
+
const level = entry.severity.padEnd(5);
|
|
2200
|
+
console.log(`${colors.dim(timeStr)} ${colorFn(level)} ${entry.message}`);
|
|
1867
2201
|
}
|
|
1868
|
-
function
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2202
|
+
async function streamDeploymentWithLogs(client2, deploymentId, appName, applicationId) {
|
|
2203
|
+
stopSpinner();
|
|
2204
|
+
let deployment;
|
|
2205
|
+
try {
|
|
2206
|
+
deployment = await client2.deployment.one.query({ deploymentId });
|
|
2207
|
+
} catch {
|
|
2208
|
+
throw new NotFoundError("Deployment", deploymentId);
|
|
2209
|
+
}
|
|
2210
|
+
const logLines = [];
|
|
2211
|
+
const errors = [];
|
|
2212
|
+
const startTime = Date.now();
|
|
2213
|
+
let wsConnected = false;
|
|
2214
|
+
let lastStatus = deployment.status;
|
|
2215
|
+
if (!isJsonMode()) {
|
|
2216
|
+
log("");
|
|
2217
|
+
log(
|
|
2218
|
+
`${colors.bold(appName)} - Deployment ${colors.cyan(deploymentId.slice(0, 8))}`
|
|
2219
|
+
);
|
|
2220
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2221
|
+
log("");
|
|
2222
|
+
}
|
|
2223
|
+
let cleanup = null;
|
|
2224
|
+
if (deployment.logPath) {
|
|
2225
|
+
const stream = streamDeploymentLogs(deployment.logPath, {
|
|
2226
|
+
onData: (line) => {
|
|
2227
|
+
logLines.push(line);
|
|
2228
|
+
if (isErrorLine(line)) {
|
|
2229
|
+
errors.push(line);
|
|
2230
|
+
}
|
|
2231
|
+
if (!isJsonMode()) {
|
|
2232
|
+
printLogLine(line);
|
|
2233
|
+
}
|
|
2234
|
+
},
|
|
2235
|
+
onOpen: () => {
|
|
2236
|
+
wsConnected = true;
|
|
2237
|
+
},
|
|
2238
|
+
onError: () => {
|
|
2239
|
+
if (!isJsonMode() && wsConnected) {
|
|
2240
|
+
log(colors.dim("[WebSocket reconnecting...]"));
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
cleanup = stream.cleanup;
|
|
2245
|
+
}
|
|
2246
|
+
const maxWaitMs = 6e5;
|
|
2247
|
+
const pollIntervalMs = 3e3;
|
|
2248
|
+
try {
|
|
2249
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
2250
|
+
await sleep(pollIntervalMs);
|
|
2251
|
+
const updatedDeployment = await client2.deployment.one.query({
|
|
2252
|
+
deploymentId
|
|
2253
|
+
});
|
|
2254
|
+
lastStatus = updatedDeployment.status;
|
|
2255
|
+
if (lastStatus === "done") {
|
|
2256
|
+
await sleep(500);
|
|
2257
|
+
cleanup?.();
|
|
2258
|
+
const finalApp = await client2.application.one.query({ applicationId });
|
|
2259
|
+
const duration2 = Math.round((Date.now() - startTime) / 1e3);
|
|
2260
|
+
if (isJsonMode()) {
|
|
2261
|
+
outputData({
|
|
2262
|
+
success: true,
|
|
2263
|
+
data: {
|
|
2264
|
+
deploymentId,
|
|
2265
|
+
status: "done",
|
|
2266
|
+
url: finalApp.cloudServiceUrl,
|
|
2267
|
+
duration: duration2,
|
|
2268
|
+
logs: logLines
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
} else {
|
|
2272
|
+
log("");
|
|
2273
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2274
|
+
log(colors.success("\u2713 Deployment successful!"));
|
|
2275
|
+
log("");
|
|
2276
|
+
log(`URL: ${colors.cyan(finalApp.cloudServiceUrl || "Pending...")}`);
|
|
2277
|
+
log(`Duration: ${colors.dim(`${duration2}s`)}`);
|
|
2278
|
+
log("");
|
|
2279
|
+
}
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
if (lastStatus === "error" || lastStatus === "cancelled") {
|
|
2283
|
+
cleanup?.();
|
|
2284
|
+
const errorAnalysis = analyzeDeploymentError(
|
|
2285
|
+
logLines,
|
|
2286
|
+
updatedDeployment.errorMessage
|
|
2287
|
+
);
|
|
2288
|
+
const duration2 = Math.round((Date.now() - startTime) / 1e3);
|
|
2289
|
+
if (isJsonMode()) {
|
|
2290
|
+
outputData({
|
|
2291
|
+
success: false,
|
|
2292
|
+
error: {
|
|
2293
|
+
code: errorAnalysis.category === "build_script" || errorAnalysis.category === "npm_install" || errorAnalysis.category === "typescript" ? "BUILD_FAILED" : "DEPLOYMENT_FAILED",
|
|
2294
|
+
message: updatedDeployment.errorMessage || "Deployment failed",
|
|
2295
|
+
deploymentId,
|
|
2296
|
+
duration: duration2,
|
|
2297
|
+
logs: logLines,
|
|
2298
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
2299
|
+
errorAnalysis
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
} else {
|
|
2303
|
+
log("");
|
|
2304
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2305
|
+
log(colors.error("\u2717 Deployment failed"));
|
|
2306
|
+
log("");
|
|
2307
|
+
if (updatedDeployment.errorMessage) {
|
|
2308
|
+
log(`Error: ${colors.error(updatedDeployment.errorMessage)}`);
|
|
2309
|
+
}
|
|
2310
|
+
if (errorAnalysis.category !== "unknown") {
|
|
2311
|
+
log("");
|
|
2312
|
+
log(colors.bold("Error Analysis:"));
|
|
2313
|
+
log(` Category: ${errorAnalysis.category}`);
|
|
2314
|
+
log(` Type: ${errorAnalysis.type}`);
|
|
2315
|
+
log("");
|
|
2316
|
+
log(colors.bold("Possible Causes:"));
|
|
2317
|
+
for (const cause of errorAnalysis.possibleCauses.slice(0, 3)) {
|
|
2318
|
+
log(` \u2022 ${cause}`);
|
|
2319
|
+
}
|
|
2320
|
+
log("");
|
|
2321
|
+
log(colors.bold("Suggested Fixes:"));
|
|
2322
|
+
for (const fix of errorAnalysis.suggestedFixes.slice(0, 3)) {
|
|
2323
|
+
log(` \u2022 ${fix}`);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
log("");
|
|
2327
|
+
}
|
|
2328
|
+
if (errorAnalysis.category === "build_script" || errorAnalysis.category === "npm_install" || errorAnalysis.category === "typescript" || errorAnalysis.category === "docker_build") {
|
|
2329
|
+
throw new BuildFailedError(
|
|
2330
|
+
updatedDeployment.errorMessage || "Build failed",
|
|
2331
|
+
deploymentId,
|
|
2332
|
+
errorAnalysis
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
throw new DeploymentFailedError(
|
|
2336
|
+
updatedDeployment.errorMessage || "Deployment failed",
|
|
2337
|
+
deploymentId,
|
|
2338
|
+
errorAnalysis
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
cleanup?.();
|
|
2343
|
+
const duration = Math.round((Date.now() - startTime) / 1e3);
|
|
2344
|
+
if (isJsonMode()) {
|
|
2345
|
+
outputData({
|
|
2346
|
+
success: false,
|
|
2347
|
+
error: {
|
|
2348
|
+
code: "DEPLOYMENT_TIMEOUT",
|
|
2349
|
+
message: "Deployment timed out",
|
|
2350
|
+
deploymentId,
|
|
2351
|
+
duration,
|
|
2352
|
+
logs: logLines,
|
|
2353
|
+
errors: errors.length > 0 ? errors : void 0
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
} else {
|
|
2357
|
+
log("");
|
|
2358
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2359
|
+
log(colors.warn("\u26A0 Deployment timed out"));
|
|
2360
|
+
log("");
|
|
2361
|
+
log(
|
|
2362
|
+
`The deployment is still running. Check status with: ${colors.dim(`tarout deploy:status ${applicationId.slice(0, 8)}`)}`
|
|
2363
|
+
);
|
|
2364
|
+
log("");
|
|
2365
|
+
}
|
|
2366
|
+
throw new DeploymentTimeoutError(
|
|
2367
|
+
"Deployment timed out after 10 minutes",
|
|
2368
|
+
deploymentId
|
|
2369
|
+
);
|
|
2370
|
+
} finally {
|
|
2371
|
+
cleanup?.();
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
function isErrorLine(line) {
|
|
2375
|
+
const errorPatterns = [
|
|
2376
|
+
/error/i,
|
|
2377
|
+
/ERR!/i,
|
|
2378
|
+
/failed/i,
|
|
2379
|
+
/fatal/i,
|
|
2380
|
+
/exception/i,
|
|
2381
|
+
/ENOENT/i,
|
|
2382
|
+
/EACCES/i,
|
|
2383
|
+
/EPERM/i
|
|
2384
|
+
];
|
|
2385
|
+
return errorPatterns.some((pattern) => pattern.test(line));
|
|
2386
|
+
}
|
|
2387
|
+
function printLogLine(line) {
|
|
2388
|
+
if (isErrorLine(line)) {
|
|
2389
|
+
console.log(colors.error(line));
|
|
2390
|
+
} else if (/warn/i.test(line)) {
|
|
2391
|
+
console.log(colors.warn(line));
|
|
2392
|
+
} else if (/step|stage|building|installing|deploying/i.test(line)) {
|
|
2393
|
+
console.log(colors.info(line));
|
|
2394
|
+
} else {
|
|
2395
|
+
console.log(line);
|
|
1918
2396
|
}
|
|
1919
2397
|
}
|
|
1920
2398
|
|
|
@@ -1925,11 +2403,11 @@ function registerDomainsCommands(program2) {
|
|
|
1925
2403
|
try {
|
|
1926
2404
|
if (!isLoggedIn()) throw new AuthError();
|
|
1927
2405
|
const client2 = getApiClient();
|
|
1928
|
-
const
|
|
2406
|
+
const _spinner = startSpinner("Fetching domains...");
|
|
1929
2407
|
let domainsList;
|
|
1930
2408
|
if (appIdentifier) {
|
|
1931
2409
|
const apps = await client2.application.allByOrganization.query();
|
|
1932
|
-
const app =
|
|
2410
|
+
const app = findApp3(apps, appIdentifier);
|
|
1933
2411
|
if (!app) {
|
|
1934
2412
|
failSpinner();
|
|
1935
2413
|
const suggestions = findSimilar(
|
|
@@ -1955,7 +2433,9 @@ function registerDomainsCommands(program2) {
|
|
|
1955
2433
|
log("");
|
|
1956
2434
|
log("No domains found.");
|
|
1957
2435
|
log("");
|
|
1958
|
-
log(
|
|
2436
|
+
log(
|
|
2437
|
+
`Add one with: ${colors.dim("tarout domains add <app> <domain>")}`
|
|
2438
|
+
);
|
|
1959
2439
|
return;
|
|
1960
2440
|
}
|
|
1961
2441
|
log("");
|
|
@@ -1969,7 +2449,11 @@ function registerDomainsCommands(program2) {
|
|
|
1969
2449
|
])
|
|
1970
2450
|
);
|
|
1971
2451
|
log("");
|
|
1972
|
-
log(
|
|
2452
|
+
log(
|
|
2453
|
+
colors.dim(
|
|
2454
|
+
`${domainsList.length} domain${domainsList.length === 1 ? "" : "s"}`
|
|
2455
|
+
)
|
|
2456
|
+
);
|
|
1973
2457
|
} catch (err) {
|
|
1974
2458
|
handleError(err);
|
|
1975
2459
|
}
|
|
@@ -1983,9 +2467,9 @@ function registerDomainsCommands(program2) {
|
|
|
1983
2467
|
);
|
|
1984
2468
|
}
|
|
1985
2469
|
const client2 = getApiClient();
|
|
1986
|
-
const
|
|
2470
|
+
const _spinner = startSpinner("Finding application...");
|
|
1987
2471
|
const apps = await client2.application.allByOrganization.query();
|
|
1988
|
-
const app =
|
|
2472
|
+
const app = findApp3(apps, appIdentifier);
|
|
1989
2473
|
if (!app) {
|
|
1990
2474
|
failSpinner();
|
|
1991
2475
|
const suggestions = findSimilar(
|
|
@@ -2012,8 +2496,10 @@ function registerDomainsCommands(program2) {
|
|
|
2012
2496
|
]);
|
|
2013
2497
|
if (!domain.isVerified) {
|
|
2014
2498
|
log("Next steps:");
|
|
2015
|
-
log(
|
|
2016
|
-
log(
|
|
2499
|
+
log(" 1. Add DNS record pointing to your app");
|
|
2500
|
+
log(
|
|
2501
|
+
` 2. Verify: ${colors.dim(`tarout domains verify ${domain.domainId.slice(0, 8)}`)}`
|
|
2502
|
+
);
|
|
2017
2503
|
log("");
|
|
2018
2504
|
}
|
|
2019
2505
|
} catch (err) {
|
|
@@ -2024,8 +2510,10 @@ function registerDomainsCommands(program2) {
|
|
|
2024
2510
|
try {
|
|
2025
2511
|
if (!isLoggedIn()) throw new AuthError();
|
|
2026
2512
|
const client2 = getApiClient();
|
|
2027
|
-
const
|
|
2028
|
-
const allDomains = await client2.domain.all.query({
|
|
2513
|
+
const _spinner = startSpinner("Finding domain...");
|
|
2514
|
+
const allDomains = await client2.domain.all.query({
|
|
2515
|
+
includeUnlinked: true
|
|
2516
|
+
});
|
|
2029
2517
|
const domain = findDomain(allDomains, domainIdentifier);
|
|
2030
2518
|
if (!domain) {
|
|
2031
2519
|
failSpinner();
|
|
@@ -2052,7 +2540,7 @@ function registerDomainsCommands(program2) {
|
|
|
2052
2540
|
return;
|
|
2053
2541
|
}
|
|
2054
2542
|
}
|
|
2055
|
-
const
|
|
2543
|
+
const _deleteSpinner = startSpinner("Removing domain...");
|
|
2056
2544
|
await client2.domain.delete.mutate({
|
|
2057
2545
|
domainId: domain.domainId
|
|
2058
2546
|
});
|
|
@@ -2070,8 +2558,10 @@ function registerDomainsCommands(program2) {
|
|
|
2070
2558
|
try {
|
|
2071
2559
|
if (!isLoggedIn()) throw new AuthError();
|
|
2072
2560
|
const client2 = getApiClient();
|
|
2073
|
-
const
|
|
2074
|
-
const allDomains = await client2.domain.all.query({
|
|
2561
|
+
const _spinner = startSpinner("Finding domain...");
|
|
2562
|
+
const allDomains = await client2.domain.all.query({
|
|
2563
|
+
includeUnlinked: true
|
|
2564
|
+
});
|
|
2075
2565
|
const domain = findDomain(allDomains, domainIdentifier);
|
|
2076
2566
|
if (!domain) {
|
|
2077
2567
|
failSpinner();
|
|
@@ -2116,7 +2606,7 @@ function registerDomainsCommands(program2) {
|
|
|
2116
2606
|
}
|
|
2117
2607
|
});
|
|
2118
2608
|
}
|
|
2119
|
-
function
|
|
2609
|
+
function findApp3(apps, identifier) {
|
|
2120
2610
|
const lowerIdentifier = identifier.toLowerCase();
|
|
2121
2611
|
return apps.find(
|
|
2122
2612
|
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
@@ -2138,6 +2628,250 @@ function updateSpinner2(text) {
|
|
|
2138
2628
|
});
|
|
2139
2629
|
}
|
|
2140
2630
|
|
|
2631
|
+
// src/commands/env.ts
|
|
2632
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2633
|
+
function registerEnvCommands(program2) {
|
|
2634
|
+
const env = program2.command("env").argument("<app>", "Application ID or name").description("Manage environment variables");
|
|
2635
|
+
env.command("list").alias("ls").description("List all environment variables").option("--reveal", "Show actual values (not masked)").action(async (options, command) => {
|
|
2636
|
+
try {
|
|
2637
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2638
|
+
const appIdentifier = command.parent.args[0];
|
|
2639
|
+
const client2 = getApiClient();
|
|
2640
|
+
const _spinner = startSpinner("Fetching environment variables...");
|
|
2641
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2642
|
+
const app = findApp4(apps, appIdentifier);
|
|
2643
|
+
if (!app) {
|
|
2644
|
+
failSpinner();
|
|
2645
|
+
const suggestions = findSimilar(
|
|
2646
|
+
appIdentifier,
|
|
2647
|
+
apps.map((a) => a.name)
|
|
2648
|
+
);
|
|
2649
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2650
|
+
}
|
|
2651
|
+
const variables = await client2.envVariable.list.query({
|
|
2652
|
+
applicationId: app.applicationId,
|
|
2653
|
+
includeValues: options.reveal || false
|
|
2654
|
+
});
|
|
2655
|
+
succeedSpinner();
|
|
2656
|
+
if (isJsonMode()) {
|
|
2657
|
+
outputData(variables);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (variables.length === 0) {
|
|
2661
|
+
log("");
|
|
2662
|
+
log("No environment variables found.");
|
|
2663
|
+
log("");
|
|
2664
|
+
log(
|
|
2665
|
+
`Set one with: ${colors.dim(`tarout env ${app.name} set KEY=value`)}`
|
|
2666
|
+
);
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
log("");
|
|
2670
|
+
table(
|
|
2671
|
+
["KEY", "VALUE", "SECRET", "UPDATED"],
|
|
2672
|
+
variables.map((v) => [
|
|
2673
|
+
colors.cyan(v.key),
|
|
2674
|
+
options.reveal ? v.value || colors.dim("-") : maskValue(v.value),
|
|
2675
|
+
v.isSecret ? colors.warn("Yes") : "No",
|
|
2676
|
+
formatDate4(v.updatedAt)
|
|
2677
|
+
])
|
|
2678
|
+
);
|
|
2679
|
+
log("");
|
|
2680
|
+
log(
|
|
2681
|
+
colors.dim(
|
|
2682
|
+
`${variables.length} variable${variables.length === 1 ? "" : "s"}`
|
|
2683
|
+
)
|
|
2684
|
+
);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
handleError(err);
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
env.command("set").argument("<key=value>", "Variable to set (KEY=value format)").description("Set an environment variable").option("-s, --secret", "Mark as secret (default)", true).option("--no-secret", "Mark as non-secret").action(async (keyValue, options, command) => {
|
|
2690
|
+
try {
|
|
2691
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2692
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
2693
|
+
const eqIndex = keyValue.indexOf("=");
|
|
2694
|
+
if (eqIndex === -1) {
|
|
2695
|
+
throw new InvalidArgumentError(
|
|
2696
|
+
"Invalid format. Use KEY=value (e.g., API_KEY=secret123)"
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
const key = keyValue.slice(0, eqIndex);
|
|
2700
|
+
const value = keyValue.slice(eqIndex + 1);
|
|
2701
|
+
if (!key) {
|
|
2702
|
+
throw new InvalidArgumentError("Key cannot be empty");
|
|
2703
|
+
}
|
|
2704
|
+
const client2 = getApiClient();
|
|
2705
|
+
const _spinner = startSpinner("Setting environment variable...");
|
|
2706
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2707
|
+
const app = findApp4(apps, appIdentifier);
|
|
2708
|
+
if (!app) {
|
|
2709
|
+
failSpinner();
|
|
2710
|
+
const suggestions = findSimilar(
|
|
2711
|
+
appIdentifier,
|
|
2712
|
+
apps.map((a) => a.name)
|
|
2713
|
+
);
|
|
2714
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2715
|
+
}
|
|
2716
|
+
const existing = await client2.envVariable.list.query({
|
|
2717
|
+
applicationId: app.applicationId,
|
|
2718
|
+
includeValues: false
|
|
2719
|
+
});
|
|
2720
|
+
const existingVar = existing.find((v) => v.key === key);
|
|
2721
|
+
if (existingVar) {
|
|
2722
|
+
await client2.envVariable.update.mutate({
|
|
2723
|
+
applicationId: app.applicationId,
|
|
2724
|
+
key,
|
|
2725
|
+
value,
|
|
2726
|
+
isSecret: options.secret
|
|
2727
|
+
});
|
|
2728
|
+
} else {
|
|
2729
|
+
await client2.envVariable.create.mutate({
|
|
2730
|
+
applicationId: app.applicationId,
|
|
2731
|
+
key,
|
|
2732
|
+
value,
|
|
2733
|
+
isSecret: options.secret
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
succeedSpinner(`Set ${key}`);
|
|
2737
|
+
if (isJsonMode()) {
|
|
2738
|
+
outputData({ key, updated: !!existingVar });
|
|
2739
|
+
} else {
|
|
2740
|
+
quietOutput(key);
|
|
2741
|
+
}
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
handleError(err);
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
env.command("unset").argument("<key>", "Variable key to remove").description("Remove an environment variable").action(async (key, _options, command) => {
|
|
2747
|
+
try {
|
|
2748
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2749
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
2750
|
+
const client2 = getApiClient();
|
|
2751
|
+
const _spinner = startSpinner("Removing environment variable...");
|
|
2752
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2753
|
+
const app = findApp4(apps, appIdentifier);
|
|
2754
|
+
if (!app) {
|
|
2755
|
+
failSpinner();
|
|
2756
|
+
const suggestions = findSimilar(
|
|
2757
|
+
appIdentifier,
|
|
2758
|
+
apps.map((a) => a.name)
|
|
2759
|
+
);
|
|
2760
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2761
|
+
}
|
|
2762
|
+
await client2.envVariable.delete.mutate({
|
|
2763
|
+
applicationId: app.applicationId,
|
|
2764
|
+
key
|
|
2765
|
+
});
|
|
2766
|
+
succeedSpinner(`Removed ${key}`);
|
|
2767
|
+
if (isJsonMode()) {
|
|
2768
|
+
outputData({ key, deleted: true });
|
|
2769
|
+
} else {
|
|
2770
|
+
quietOutput(key);
|
|
2771
|
+
}
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
handleError(err);
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
env.command("pull").description("Download environment variables as .env file").option("-o, --output <file>", "Output file path", ".env").option("--reveal", "Include actual secret values").action(async (options, command) => {
|
|
2777
|
+
try {
|
|
2778
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2779
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
2780
|
+
const client2 = getApiClient();
|
|
2781
|
+
const _spinner = startSpinner("Downloading environment variables...");
|
|
2782
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2783
|
+
const app = findApp4(apps, appIdentifier);
|
|
2784
|
+
if (!app) {
|
|
2785
|
+
failSpinner();
|
|
2786
|
+
const suggestions = findSimilar(
|
|
2787
|
+
appIdentifier,
|
|
2788
|
+
apps.map((a) => a.name)
|
|
2789
|
+
);
|
|
2790
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2791
|
+
}
|
|
2792
|
+
if (existsSync(options.output) && !shouldSkipConfirmation()) {
|
|
2793
|
+
succeedSpinner();
|
|
2794
|
+
const confirmed = await confirm(
|
|
2795
|
+
`File ${options.output} already exists. Overwrite?`,
|
|
2796
|
+
false
|
|
2797
|
+
);
|
|
2798
|
+
if (!confirmed) {
|
|
2799
|
+
log("Cancelled.");
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
const result = await client2.envVariable.export.query({
|
|
2804
|
+
applicationId: app.applicationId,
|
|
2805
|
+
format: "dotenv",
|
|
2806
|
+
maskSecrets: !options.reveal
|
|
2807
|
+
});
|
|
2808
|
+
writeFileSync(options.output, result.content);
|
|
2809
|
+
succeedSpinner(`Saved to ${options.output}`);
|
|
2810
|
+
if (isJsonMode()) {
|
|
2811
|
+
outputData({ file: options.output, content: result.content });
|
|
2812
|
+
} else {
|
|
2813
|
+
quietOutput(options.output);
|
|
2814
|
+
}
|
|
2815
|
+
} catch (err) {
|
|
2816
|
+
handleError(err);
|
|
2817
|
+
}
|
|
2818
|
+
});
|
|
2819
|
+
env.command("push").description("Upload environment variables from .env file").option("-i, --input <file>", "Input file path", ".env").option("--replace", "Replace all existing variables (default: merge)").action(async (options, command) => {
|
|
2820
|
+
try {
|
|
2821
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2822
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
2823
|
+
if (!existsSync(options.input)) {
|
|
2824
|
+
throw new InvalidArgumentError(`File not found: ${options.input}`);
|
|
2825
|
+
}
|
|
2826
|
+
const content = readFileSync(options.input, "utf-8");
|
|
2827
|
+
const client2 = getApiClient();
|
|
2828
|
+
const _spinner = startSpinner("Uploading environment variables...");
|
|
2829
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2830
|
+
const app = findApp4(apps, appIdentifier);
|
|
2831
|
+
if (!app) {
|
|
2832
|
+
failSpinner();
|
|
2833
|
+
const suggestions = findSimilar(
|
|
2834
|
+
appIdentifier,
|
|
2835
|
+
apps.map((a) => a.name)
|
|
2836
|
+
);
|
|
2837
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2838
|
+
}
|
|
2839
|
+
const result = await client2.envVariable.import.mutate({
|
|
2840
|
+
applicationId: app.applicationId,
|
|
2841
|
+
content,
|
|
2842
|
+
format: "dotenv",
|
|
2843
|
+
merge: !options.replace
|
|
2844
|
+
});
|
|
2845
|
+
succeedSpinner(`Imported ${result.imported} variables`);
|
|
2846
|
+
if (isJsonMode()) {
|
|
2847
|
+
outputData(result);
|
|
2848
|
+
} else {
|
|
2849
|
+
quietOutput(String(result.imported));
|
|
2850
|
+
if (result.skipped > 0) {
|
|
2851
|
+
log(colors.dim(`Skipped ${result.skipped} (already exist)`));
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
} catch (err) {
|
|
2855
|
+
handleError(err);
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
function findApp4(apps, identifier) {
|
|
2860
|
+
const lowerIdentifier = identifier.toLowerCase();
|
|
2861
|
+
return apps.find(
|
|
2862
|
+
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
function maskValue(value) {
|
|
2866
|
+
if (!value) return colors.dim("-");
|
|
2867
|
+
if (value.length <= 4) return "****";
|
|
2868
|
+
return `${value.slice(0, 2)}****${value.slice(-2)}`;
|
|
2869
|
+
}
|
|
2870
|
+
function formatDate4(date) {
|
|
2871
|
+
const d = new Date(date);
|
|
2872
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2141
2875
|
// src/commands/orgs.ts
|
|
2142
2876
|
function registerOrgsCommands(program2) {
|
|
2143
2877
|
const orgs = program2.command("orgs").description("Manage organizations");
|
|
@@ -2146,7 +2880,7 @@ function registerOrgsCommands(program2) {
|
|
|
2146
2880
|
if (!isLoggedIn()) throw new AuthError();
|
|
2147
2881
|
const client2 = getApiClient();
|
|
2148
2882
|
const profile = getCurrentProfile();
|
|
2149
|
-
const
|
|
2883
|
+
const _spinner = startSpinner("Fetching organizations...");
|
|
2150
2884
|
const organizations = await client2.organization.all.query();
|
|
2151
2885
|
succeedSpinner();
|
|
2152
2886
|
if (isJsonMode()) {
|
|
@@ -2168,7 +2902,11 @@ function registerOrgsCommands(program2) {
|
|
|
2168
2902
|
])
|
|
2169
2903
|
);
|
|
2170
2904
|
log("");
|
|
2171
|
-
log(
|
|
2905
|
+
log(
|
|
2906
|
+
colors.dim(
|
|
2907
|
+
`${organizations.length} organization${organizations.length === 1 ? "" : "s"}`
|
|
2908
|
+
)
|
|
2909
|
+
);
|
|
2172
2910
|
log("");
|
|
2173
2911
|
log(`Current: ${colors.bold(profile?.organizationName || "None")}`);
|
|
2174
2912
|
} catch (err) {
|
|
@@ -2179,7 +2917,7 @@ function registerOrgsCommands(program2) {
|
|
|
2179
2917
|
try {
|
|
2180
2918
|
if (!isLoggedIn()) throw new AuthError();
|
|
2181
2919
|
const client2 = getApiClient();
|
|
2182
|
-
const
|
|
2920
|
+
const _spinner = startSpinner("Switching organization...");
|
|
2183
2921
|
const organizations = await client2.organization.all.query();
|
|
2184
2922
|
const org = findOrg(organizations, orgIdentifier);
|
|
2185
2923
|
if (!org) {
|
|
@@ -2225,7 +2963,7 @@ function registerEnvsCommands(program2) {
|
|
|
2225
2963
|
if (!isLoggedIn()) throw new AuthError();
|
|
2226
2964
|
const client2 = getApiClient();
|
|
2227
2965
|
const profile = getCurrentProfile();
|
|
2228
|
-
const
|
|
2966
|
+
const _spinner = startSpinner("Fetching environments...");
|
|
2229
2967
|
const environments = await client2.environment.all.query();
|
|
2230
2968
|
succeedSpinner();
|
|
2231
2969
|
if (isJsonMode()) {
|
|
@@ -2256,7 +2994,7 @@ function registerEnvsCommands(program2) {
|
|
|
2256
2994
|
try {
|
|
2257
2995
|
if (!isLoggedIn()) throw new AuthError();
|
|
2258
2996
|
const client2 = getApiClient();
|
|
2259
|
-
const
|
|
2997
|
+
const _spinner = startSpinner("Switching environment...");
|
|
2260
2998
|
const environments = await client2.environment.all.query();
|
|
2261
2999
|
const env = findEnv(environments, envIdentifier);
|
|
2262
3000
|
if (!env) {
|
|
@@ -2303,7 +3041,7 @@ function findEnv(envs, identifier) {
|
|
|
2303
3041
|
|
|
2304
3042
|
// src/index.ts
|
|
2305
3043
|
var program = new Command();
|
|
2306
|
-
program.name("tarout").description("Tarout PaaS Command Line Interface").version("0.1.
|
|
3044
|
+
program.name("tarout").description("Tarout PaaS Command Line Interface").version("0.1.3").option("--json", "Output as JSON (machine-readable)").option("-y, --yes", "Skip all confirmation prompts").option("-q, --quiet", "Minimal output").option("-v, --verbose", "Extra debug information").option("--no-color", "Disable colored output").hook("preAction", (thisCommand) => {
|
|
2307
3045
|
const opts = thisCommand.opts();
|
|
2308
3046
|
setGlobalOptions({
|
|
2309
3047
|
json: opts.json || false,
|