@tarout/cli 0.1.3 → 0.2.0
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/chunk-VO4OYJW3.js +53 -0
- package/dist/index.js +2835 -1162
- package/dist/prompts-B53LIJLG.js +12 -0
- package/package.json +62 -60
package/dist/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
confirm,
|
|
4
|
+
input,
|
|
5
|
+
select
|
|
6
|
+
} from "./chunk-VO4OYJW3.js";
|
|
2
7
|
import {
|
|
3
8
|
failSpinner,
|
|
4
9
|
startSpinner,
|
|
10
|
+
stopSpinner,
|
|
5
11
|
succeedSpinner,
|
|
6
12
|
updateSpinner
|
|
7
13
|
} from "./chunk-GSKD67K4.js";
|
|
@@ -9,9 +15,138 @@ import {
|
|
|
9
15
|
// src/index.ts
|
|
10
16
|
import { Command } from "commander";
|
|
11
17
|
|
|
12
|
-
// src/
|
|
13
|
-
import
|
|
14
|
-
|
|
18
|
+
// src/commands/apps.ts
|
|
19
|
+
import open from "open";
|
|
20
|
+
|
|
21
|
+
// src/lib/api.ts
|
|
22
|
+
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
|
23
|
+
import superjson from "superjson";
|
|
24
|
+
|
|
25
|
+
// src/lib/config.ts
|
|
26
|
+
import Conf from "conf";
|
|
27
|
+
import {
|
|
28
|
+
existsSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
writeFileSync
|
|
33
|
+
} from "fs";
|
|
34
|
+
import { join } from "path";
|
|
35
|
+
var config = new Conf({
|
|
36
|
+
projectName: "tarout",
|
|
37
|
+
defaults: {
|
|
38
|
+
currentProfile: "default",
|
|
39
|
+
profiles: {}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
function getConfig() {
|
|
43
|
+
return config.store;
|
|
44
|
+
}
|
|
45
|
+
function getCurrentProfile() {
|
|
46
|
+
const cfg = getConfig();
|
|
47
|
+
return cfg.profiles[cfg.currentProfile] || null;
|
|
48
|
+
}
|
|
49
|
+
function setProfile(name, profile) {
|
|
50
|
+
config.set(`profiles.${name}`, profile);
|
|
51
|
+
}
|
|
52
|
+
function setCurrentProfile(name) {
|
|
53
|
+
config.set("currentProfile", name);
|
|
54
|
+
}
|
|
55
|
+
function clearConfig() {
|
|
56
|
+
config.clear();
|
|
57
|
+
}
|
|
58
|
+
function isLoggedIn() {
|
|
59
|
+
const profile = getCurrentProfile();
|
|
60
|
+
return profile !== null && !!profile.token;
|
|
61
|
+
}
|
|
62
|
+
function getToken() {
|
|
63
|
+
const profile = getCurrentProfile();
|
|
64
|
+
return profile?.token || null;
|
|
65
|
+
}
|
|
66
|
+
function getApiUrl() {
|
|
67
|
+
const profile = getCurrentProfile();
|
|
68
|
+
return profile?.apiUrl || "https://tarout.sa";
|
|
69
|
+
}
|
|
70
|
+
function updateProfile(updates) {
|
|
71
|
+
const cfg = getConfig();
|
|
72
|
+
const currentProfileName = cfg.currentProfile;
|
|
73
|
+
const currentProfile = cfg.profiles[currentProfileName];
|
|
74
|
+
if (currentProfile) {
|
|
75
|
+
config.set(`profiles.${currentProfileName}`, {
|
|
76
|
+
...currentProfile,
|
|
77
|
+
...updates
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
var PROJECT_CONFIG_DIR = ".tarout";
|
|
82
|
+
var PROJECT_CONFIG_FILE = "project.json";
|
|
83
|
+
function getProjectConfigDir(basePath) {
|
|
84
|
+
const base = basePath || process.cwd();
|
|
85
|
+
return join(base, PROJECT_CONFIG_DIR);
|
|
86
|
+
}
|
|
87
|
+
function getProjectConfigPath(basePath) {
|
|
88
|
+
return join(getProjectConfigDir(basePath), PROJECT_CONFIG_FILE);
|
|
89
|
+
}
|
|
90
|
+
function isProjectLinked(basePath) {
|
|
91
|
+
return existsSync(getProjectConfigPath(basePath));
|
|
92
|
+
}
|
|
93
|
+
function getProjectConfig(basePath) {
|
|
94
|
+
const configPath = getProjectConfigPath(basePath);
|
|
95
|
+
if (!existsSync(configPath)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const content = readFileSync(configPath, "utf-8");
|
|
100
|
+
return JSON.parse(content);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function setProjectConfig(config2, basePath) {
|
|
106
|
+
const configDir = getProjectConfigDir(basePath);
|
|
107
|
+
const configPath = getProjectConfigPath(basePath);
|
|
108
|
+
if (!existsSync(configDir)) {
|
|
109
|
+
mkdirSync(configDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf-8");
|
|
112
|
+
const gitignorePath = join(configDir, ".gitignore");
|
|
113
|
+
if (!existsSync(gitignorePath)) {
|
|
114
|
+
writeFileSync(
|
|
115
|
+
gitignorePath,
|
|
116
|
+
"# Ignore local tarout config\n*\n!.gitignore\n",
|
|
117
|
+
"utf-8"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function removeProjectConfig(basePath) {
|
|
122
|
+
const configDir = getProjectConfigDir(basePath);
|
|
123
|
+
if (!existsSync(configDir)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
rmSync(configDir, { recursive: true, force: true });
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/utils/exit-codes.ts
|
|
135
|
+
var ExitCode = {
|
|
136
|
+
SUCCESS: 0,
|
|
137
|
+
GENERAL_ERROR: 1,
|
|
138
|
+
INVALID_ARGUMENTS: 2,
|
|
139
|
+
AUTH_ERROR: 3,
|
|
140
|
+
NOT_FOUND: 4,
|
|
141
|
+
PERMISSION_DENIED: 5,
|
|
142
|
+
// Deployment-specific exit codes
|
|
143
|
+
DEPLOYMENT_FAILED: 10,
|
|
144
|
+
DEPLOYMENT_TIMEOUT: 11,
|
|
145
|
+
BUILD_FAILED: 12
|
|
146
|
+
};
|
|
147
|
+
function exit(code) {
|
|
148
|
+
process.exit(code);
|
|
149
|
+
}
|
|
15
150
|
|
|
16
151
|
// src/utils/json.ts
|
|
17
152
|
function jsonSuccess(data, meta) {
|
|
@@ -36,6 +171,8 @@ function outputJson(response) {
|
|
|
36
171
|
}
|
|
37
172
|
|
|
38
173
|
// src/lib/output.ts
|
|
174
|
+
import chalk from "chalk";
|
|
175
|
+
import Table from "cli-table3";
|
|
39
176
|
var globalOptions = {
|
|
40
177
|
json: false,
|
|
41
178
|
quiet: false,
|
|
@@ -152,174 +289,6 @@ function box(title, content) {
|
|
|
152
289
|
console.log("");
|
|
153
290
|
}
|
|
154
291
|
|
|
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
292
|
// src/lib/errors.ts
|
|
324
293
|
var CliError = class extends Error {
|
|
325
294
|
constructor(message, code = ExitCode.GENERAL_ERROR, suggestions) {
|
|
@@ -344,15 +313,31 @@ var InvalidArgumentError = class extends CliError {
|
|
|
344
313
|
super(message, ExitCode.INVALID_ARGUMENTS);
|
|
345
314
|
}
|
|
346
315
|
};
|
|
316
|
+
var DeploymentFailedError = class extends CliError {
|
|
317
|
+
constructor(message, deploymentId, errorAnalysis) {
|
|
318
|
+
super(message, ExitCode.DEPLOYMENT_FAILED);
|
|
319
|
+
this.deploymentId = deploymentId;
|
|
320
|
+
this.errorAnalysis = errorAnalysis;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var DeploymentTimeoutError = class extends CliError {
|
|
324
|
+
constructor(message = "Deployment timed out", deploymentId) {
|
|
325
|
+
super(message, ExitCode.DEPLOYMENT_TIMEOUT);
|
|
326
|
+
this.deploymentId = deploymentId;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
var BuildFailedError = class extends CliError {
|
|
330
|
+
constructor(message, deploymentId, errorAnalysis) {
|
|
331
|
+
super(message, ExitCode.BUILD_FAILED);
|
|
332
|
+
this.deploymentId = deploymentId;
|
|
333
|
+
this.errorAnalysis = errorAnalysis;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
347
336
|
function handleError(err) {
|
|
348
337
|
if (err instanceof CliError) {
|
|
349
338
|
if (isJsonMode()) {
|
|
350
339
|
outputJson(
|
|
351
|
-
jsonError(
|
|
352
|
-
getErrorCode(err.code),
|
|
353
|
-
err.message,
|
|
354
|
-
err.suggestions
|
|
355
|
-
)
|
|
340
|
+
jsonError(getErrorCode(err.code), err.message, err.suggestions)
|
|
356
341
|
);
|
|
357
342
|
} else {
|
|
358
343
|
error(err.message, err.suggestions);
|
|
@@ -384,7 +369,10 @@ function getErrorCode(exitCode) {
|
|
|
384
369
|
[ExitCode.INVALID_ARGUMENTS]: "INVALID_ARGUMENTS",
|
|
385
370
|
[ExitCode.AUTH_ERROR]: "AUTH_ERROR",
|
|
386
371
|
[ExitCode.NOT_FOUND]: "NOT_FOUND",
|
|
387
|
-
[ExitCode.PERMISSION_DENIED]: "PERMISSION_DENIED"
|
|
372
|
+
[ExitCode.PERMISSION_DENIED]: "PERMISSION_DENIED",
|
|
373
|
+
[ExitCode.DEPLOYMENT_FAILED]: "DEPLOYMENT_FAILED",
|
|
374
|
+
[ExitCode.DEPLOYMENT_TIMEOUT]: "DEPLOYMENT_TIMEOUT",
|
|
375
|
+
[ExitCode.BUILD_FAILED]: "BUILD_FAILED"
|
|
388
376
|
};
|
|
389
377
|
return codes[exitCode] || "ERROR";
|
|
390
378
|
}
|
|
@@ -420,155 +408,288 @@ function similarity(s1, s2) {
|
|
|
420
408
|
}
|
|
421
409
|
return 2 * intersection / (s1.length - 1 + s2.length - 1);
|
|
422
410
|
}
|
|
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
|
-
|
|
411
|
+
var ERROR_PATTERNS = [
|
|
412
|
+
{
|
|
413
|
+
patterns: [
|
|
414
|
+
/npm ERR!/i,
|
|
415
|
+
/npm error/i,
|
|
416
|
+
/ERESOLVE/i,
|
|
417
|
+
/Could not resolve dependency/i,
|
|
418
|
+
/peer dep missing/i
|
|
419
|
+
],
|
|
420
|
+
category: "npm_install",
|
|
421
|
+
type: "build_error",
|
|
422
|
+
possibleCauses: [
|
|
423
|
+
"Package version conflicts",
|
|
424
|
+
"Missing peer dependencies",
|
|
425
|
+
"Invalid package.json",
|
|
426
|
+
"Network issues during npm install",
|
|
427
|
+
"Private package without authentication"
|
|
428
|
+
],
|
|
429
|
+
suggestedFixes: [
|
|
430
|
+
"Run `npm install` locally to reproduce the issue",
|
|
431
|
+
"Check for conflicting package versions in package.json",
|
|
432
|
+
"Try deleting package-lock.json and node_modules, then reinstall",
|
|
433
|
+
"Add missing peer dependencies explicitly",
|
|
434
|
+
"Check if private packages are properly authenticated"
|
|
435
|
+
]
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
patterns: [
|
|
439
|
+
/yarn error/i,
|
|
440
|
+
/YN\d{4}/i,
|
|
441
|
+
/error An unexpected error occurred/i
|
|
442
|
+
],
|
|
443
|
+
category: "yarn_install",
|
|
444
|
+
type: "build_error",
|
|
445
|
+
possibleCauses: [
|
|
446
|
+
"Package version conflicts",
|
|
447
|
+
"Corrupted yarn.lock file",
|
|
448
|
+
"Network issues during install",
|
|
449
|
+
"Incompatible yarn version"
|
|
450
|
+
],
|
|
451
|
+
suggestedFixes: [
|
|
452
|
+
"Run `yarn install` locally to reproduce the issue",
|
|
453
|
+
"Delete yarn.lock and node_modules, then reinstall",
|
|
454
|
+
"Check for conflicting resolutions in package.json",
|
|
455
|
+
"Ensure yarn version matches the project requirements"
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
patterns: [/pnpm ERR/i, /ERR_PNPM/i],
|
|
460
|
+
category: "pnpm_install",
|
|
461
|
+
type: "build_error",
|
|
462
|
+
possibleCauses: [
|
|
463
|
+
"Package version conflicts",
|
|
464
|
+
"Incompatible pnpm version",
|
|
465
|
+
"Corrupted pnpm-lock.yaml"
|
|
466
|
+
],
|
|
467
|
+
suggestedFixes: [
|
|
468
|
+
"Run `pnpm install` locally to reproduce the issue",
|
|
469
|
+
"Delete pnpm-lock.yaml and node_modules, then reinstall",
|
|
470
|
+
"Check pnpm version compatibility"
|
|
471
|
+
]
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
patterns: [/bun install/i, /error: .* failed to resolve/i],
|
|
475
|
+
category: "bun_install",
|
|
476
|
+
type: "build_error",
|
|
477
|
+
possibleCauses: [
|
|
478
|
+
"Package resolution failure",
|
|
479
|
+
"Incompatible bun version",
|
|
480
|
+
"Missing dependencies"
|
|
481
|
+
],
|
|
482
|
+
suggestedFixes: [
|
|
483
|
+
"Run `bun install` locally to reproduce the issue",
|
|
484
|
+
"Delete bun.lockb and node_modules, then reinstall",
|
|
485
|
+
"Check bun version compatibility"
|
|
486
|
+
]
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
patterns: [
|
|
490
|
+
/error TS\d+/i,
|
|
491
|
+
/TypeScript error/i,
|
|
492
|
+
/tsc.*error/i,
|
|
493
|
+
/Type .* is not assignable to/i,
|
|
494
|
+
/Cannot find module/i,
|
|
495
|
+
/Property .* does not exist/i
|
|
496
|
+
],
|
|
497
|
+
category: "typescript",
|
|
498
|
+
type: "build_error",
|
|
499
|
+
possibleCauses: [
|
|
500
|
+
"TypeScript compilation errors in source code",
|
|
501
|
+
"Missing type definitions",
|
|
502
|
+
"Incompatible TypeScript version",
|
|
503
|
+
"Strict mode type errors"
|
|
504
|
+
],
|
|
505
|
+
suggestedFixes: [
|
|
506
|
+
"Run `npx tsc --noEmit` locally to see all type errors",
|
|
507
|
+
"Fix the TypeScript errors in the indicated files",
|
|
508
|
+
"Install missing @types/* packages",
|
|
509
|
+
"Check tsconfig.json for strict mode settings"
|
|
510
|
+
]
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
patterns: [
|
|
514
|
+
/COPY failed/i,
|
|
515
|
+
/RUN.*failed/i,
|
|
516
|
+
/failed to build/i,
|
|
517
|
+
/Error building image/i,
|
|
518
|
+
/Dockerfile/i
|
|
519
|
+
],
|
|
520
|
+
category: "docker_build",
|
|
521
|
+
type: "build_error",
|
|
522
|
+
possibleCauses: [
|
|
523
|
+
"Invalid Dockerfile syntax",
|
|
524
|
+
"Missing files in build context",
|
|
525
|
+
"Failed RUN commands",
|
|
526
|
+
"Base image not found"
|
|
527
|
+
],
|
|
528
|
+
suggestedFixes: [
|
|
529
|
+
"Test Docker build locally with `docker build .`",
|
|
530
|
+
"Check that all required files are in the build context",
|
|
531
|
+
"Verify Dockerfile syntax and commands",
|
|
532
|
+
"Ensure base image exists and is accessible"
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
patterns: [
|
|
537
|
+
/build.*failed/i,
|
|
538
|
+
/Build failed/i,
|
|
539
|
+
/exit code 1/i,
|
|
540
|
+
/Command failed/i,
|
|
541
|
+
/Script.*failed/i
|
|
542
|
+
],
|
|
543
|
+
category: "build_script",
|
|
544
|
+
type: "build_error",
|
|
545
|
+
possibleCauses: [
|
|
546
|
+
"Build script error in package.json",
|
|
547
|
+
"Missing build dependencies",
|
|
548
|
+
"Environment variable issues",
|
|
549
|
+
"Build command not found"
|
|
550
|
+
],
|
|
551
|
+
suggestedFixes: [
|
|
552
|
+
"Run `npm run build` locally to reproduce the error",
|
|
553
|
+
"Check package.json build script for errors",
|
|
554
|
+
"Ensure all required environment variables are set",
|
|
555
|
+
"Verify build dependencies are installed"
|
|
556
|
+
]
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
patterns: [
|
|
560
|
+
/out of memory/i,
|
|
561
|
+
/JavaScript heap out of memory/i,
|
|
562
|
+
/FATAL ERROR: .* allocation failed/i,
|
|
563
|
+
/OOMKilled/i
|
|
564
|
+
],
|
|
565
|
+
category: "memory_limit",
|
|
566
|
+
type: "build_error",
|
|
567
|
+
possibleCauses: [
|
|
568
|
+
"Build process requires more memory than allocated",
|
|
569
|
+
"Memory leak in build process",
|
|
570
|
+
"Large dependency tree"
|
|
571
|
+
],
|
|
572
|
+
suggestedFixes: [
|
|
573
|
+
"Increase memory allocation for the build",
|
|
574
|
+
"Optimize build process to use less memory",
|
|
575
|
+
"Consider splitting large bundles",
|
|
576
|
+
"Add NODE_OPTIONS=--max_old_space_size=4096"
|
|
577
|
+
]
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
patterns: [/timed? ?out/i, /deadline exceeded/i, /timeout/i],
|
|
581
|
+
category: "timeout",
|
|
582
|
+
type: "build_error",
|
|
583
|
+
possibleCauses: [
|
|
584
|
+
"Build process took too long",
|
|
585
|
+
"Network timeout during dependency install",
|
|
586
|
+
"Slow external service calls"
|
|
587
|
+
],
|
|
588
|
+
suggestedFixes: [
|
|
589
|
+
"Optimize build process for faster execution",
|
|
590
|
+
"Check for slow network requests during build",
|
|
591
|
+
"Consider caching dependencies",
|
|
592
|
+
"Increase build timeout if possible"
|
|
593
|
+
]
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
patterns: [/permission denied/i, /EACCES/i, /EPERM/i],
|
|
597
|
+
category: "permission",
|
|
598
|
+
type: "build_error",
|
|
599
|
+
possibleCauses: [
|
|
600
|
+
"File permission issues",
|
|
601
|
+
"Attempting to write to read-only locations",
|
|
602
|
+
"Docker user permission mismatch"
|
|
603
|
+
],
|
|
604
|
+
suggestedFixes: [
|
|
605
|
+
"Check file permissions in the project",
|
|
606
|
+
"Ensure Dockerfile uses correct user",
|
|
607
|
+
"Avoid writing to restricted directories"
|
|
608
|
+
]
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
patterns: [
|
|
612
|
+
/ENOTFOUND/i,
|
|
613
|
+
/ECONNREFUSED/i,
|
|
614
|
+
/network error/i,
|
|
615
|
+
/fetch failed/i,
|
|
616
|
+
/getaddrinfo/i
|
|
617
|
+
],
|
|
618
|
+
category: "network",
|
|
619
|
+
type: "build_error",
|
|
620
|
+
possibleCauses: [
|
|
621
|
+
"Network connectivity issues",
|
|
622
|
+
"DNS resolution failure",
|
|
623
|
+
"Registry unavailable",
|
|
624
|
+
"Firewall blocking connections"
|
|
625
|
+
],
|
|
626
|
+
suggestedFixes: [
|
|
627
|
+
"Check if the package registry is accessible",
|
|
628
|
+
"Verify network configuration",
|
|
629
|
+
"Try again later if registry is temporarily down",
|
|
630
|
+
"Check for any proxy configuration issues"
|
|
631
|
+
]
|
|
632
|
+
}
|
|
633
|
+
];
|
|
634
|
+
function analyzeDeploymentError(logs, errorMessage) {
|
|
635
|
+
const relevantLogLines = [];
|
|
636
|
+
let matchedPattern = null;
|
|
637
|
+
let maxMatches = 0;
|
|
638
|
+
const allLines = errorMessage ? [...logs, errorMessage] : logs;
|
|
639
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
640
|
+
let matches = 0;
|
|
641
|
+
const matchedLines = [];
|
|
642
|
+
for (const line of allLines) {
|
|
643
|
+
for (const regex of pattern.patterns) {
|
|
644
|
+
if (regex.test(line)) {
|
|
645
|
+
matches++;
|
|
646
|
+
if (!matchedLines.includes(line)) {
|
|
647
|
+
matchedLines.push(line);
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
486
650
|
}
|
|
487
|
-
} catch (err) {
|
|
488
|
-
failSpinner("Authentication failed");
|
|
489
|
-
authServer.close();
|
|
490
|
-
throw err;
|
|
491
651
|
}
|
|
492
|
-
} catch (err) {
|
|
493
|
-
handleError(err);
|
|
494
652
|
}
|
|
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);
|
|
653
|
+
if (matches > maxMatches) {
|
|
654
|
+
maxMatches = matches;
|
|
655
|
+
matchedPattern = pattern;
|
|
656
|
+
relevantLogLines.length = 0;
|
|
657
|
+
relevantLogLines.push(...matchedLines.slice(0, 10));
|
|
515
658
|
}
|
|
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);
|
|
659
|
+
}
|
|
660
|
+
const errorIndicators = [/error/i, /failed/i, /fatal/i, /exception/i];
|
|
661
|
+
for (const line of allLines) {
|
|
662
|
+
if (relevantLogLines.length < 15 && !relevantLogLines.includes(line) && errorIndicators.some((indicator) => indicator.test(line))) {
|
|
663
|
+
relevantLogLines.push(line);
|
|
562
664
|
}
|
|
563
|
-
}
|
|
665
|
+
}
|
|
666
|
+
if (matchedPattern) {
|
|
667
|
+
return {
|
|
668
|
+
type: matchedPattern.type,
|
|
669
|
+
category: matchedPattern.category,
|
|
670
|
+
possibleCauses: matchedPattern.possibleCauses,
|
|
671
|
+
suggestedFixes: matchedPattern.suggestedFixes,
|
|
672
|
+
relevantLogLines
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
type: "unknown",
|
|
677
|
+
category: "unknown",
|
|
678
|
+
possibleCauses: [
|
|
679
|
+
"Unable to automatically determine the cause",
|
|
680
|
+
"Check the log output for specific error messages"
|
|
681
|
+
],
|
|
682
|
+
suggestedFixes: [
|
|
683
|
+
"Review the deployment logs for error messages",
|
|
684
|
+
"Try running the build locally to reproduce the issue",
|
|
685
|
+
"Check environment variables and configuration",
|
|
686
|
+
"Contact support if the issue persists"
|
|
687
|
+
],
|
|
688
|
+
relevantLogLines
|
|
689
|
+
};
|
|
564
690
|
}
|
|
565
691
|
|
|
566
|
-
// src/commands/apps.ts
|
|
567
|
-
import open2 from "open";
|
|
568
|
-
|
|
569
692
|
// src/lib/api.ts
|
|
570
|
-
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
|
571
|
-
import superjson from "superjson";
|
|
572
693
|
var client = null;
|
|
573
694
|
function createApiClient() {
|
|
574
695
|
if (!isLoggedIn()) {
|
|
@@ -595,42 +716,6 @@ function getApiClient() {
|
|
|
595
716
|
return client;
|
|
596
717
|
}
|
|
597
718
|
|
|
598
|
-
// src/utils/prompts.ts
|
|
599
|
-
import inquirer from "inquirer";
|
|
600
|
-
async function confirm(message, defaultValue = false) {
|
|
601
|
-
const { confirmed } = await inquirer.prompt([
|
|
602
|
-
{
|
|
603
|
-
type: "confirm",
|
|
604
|
-
name: "confirmed",
|
|
605
|
-
message,
|
|
606
|
-
default: defaultValue
|
|
607
|
-
}
|
|
608
|
-
]);
|
|
609
|
-
return confirmed;
|
|
610
|
-
}
|
|
611
|
-
async function input(message, defaultValue) {
|
|
612
|
-
const { value } = await inquirer.prompt([
|
|
613
|
-
{
|
|
614
|
-
type: "input",
|
|
615
|
-
name: "value",
|
|
616
|
-
message,
|
|
617
|
-
default: defaultValue
|
|
618
|
-
}
|
|
619
|
-
]);
|
|
620
|
-
return value;
|
|
621
|
-
}
|
|
622
|
-
async function select(message, choices) {
|
|
623
|
-
const { value } = await inquirer.prompt([
|
|
624
|
-
{
|
|
625
|
-
type: "list",
|
|
626
|
-
name: "value",
|
|
627
|
-
message,
|
|
628
|
-
choices
|
|
629
|
-
}
|
|
630
|
-
]);
|
|
631
|
-
return value;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
719
|
// src/commands/apps.ts
|
|
635
720
|
function registerAppsCommands(program2) {
|
|
636
721
|
const apps = program2.command("apps").description("Manage applications");
|
|
@@ -638,7 +723,7 @@ function registerAppsCommands(program2) {
|
|
|
638
723
|
try {
|
|
639
724
|
if (!isLoggedIn()) throw new AuthError();
|
|
640
725
|
const client2 = getApiClient();
|
|
641
|
-
const
|
|
726
|
+
const _spinner = startSpinner("Fetching applications...");
|
|
642
727
|
const applications = await client2.application.allByOrganization.query();
|
|
643
728
|
succeedSpinner();
|
|
644
729
|
if (isJsonMode()) {
|
|
@@ -664,7 +749,11 @@ function registerAppsCommands(program2) {
|
|
|
664
749
|
])
|
|
665
750
|
);
|
|
666
751
|
log("");
|
|
667
|
-
log(
|
|
752
|
+
log(
|
|
753
|
+
colors.dim(
|
|
754
|
+
`${applications.length} application${applications.length === 1 ? "" : "s"}`
|
|
755
|
+
)
|
|
756
|
+
);
|
|
668
757
|
} catch (err) {
|
|
669
758
|
handleError(err);
|
|
670
759
|
}
|
|
@@ -684,7 +773,7 @@ function registerAppsCommands(program2) {
|
|
|
684
773
|
}
|
|
685
774
|
const slug = generateSlug(appName);
|
|
686
775
|
const client2 = getApiClient();
|
|
687
|
-
const
|
|
776
|
+
const _spinner = startSpinner("Creating application...");
|
|
688
777
|
const application = await client2.application.create.mutate({
|
|
689
778
|
name: appName,
|
|
690
779
|
appName: slug,
|
|
@@ -703,8 +792,12 @@ function registerAppsCommands(program2) {
|
|
|
703
792
|
`Slug: ${application.appName}`
|
|
704
793
|
]);
|
|
705
794
|
log("Next steps:");
|
|
706
|
-
log(
|
|
707
|
-
|
|
795
|
+
log(
|
|
796
|
+
` 1. Connect a source: ${colors.dim(`tarout apps info ${application.applicationId.slice(0, 8)}`)}`
|
|
797
|
+
);
|
|
798
|
+
log(
|
|
799
|
+
` 2. Deploy: ${colors.dim(`tarout deploy ${application.applicationId.slice(0, 8)}`)}`
|
|
800
|
+
);
|
|
708
801
|
log("");
|
|
709
802
|
} catch (err) {
|
|
710
803
|
handleError(err);
|
|
@@ -714,7 +807,7 @@ function registerAppsCommands(program2) {
|
|
|
714
807
|
try {
|
|
715
808
|
if (!isLoggedIn()) throw new AuthError();
|
|
716
809
|
const client2 = getApiClient();
|
|
717
|
-
const
|
|
810
|
+
const _spinner = startSpinner("Finding application...");
|
|
718
811
|
const apps2 = await client2.application.allByOrganization.query();
|
|
719
812
|
const app = findApp(apps2, appIdentifier);
|
|
720
813
|
if (!app) {
|
|
@@ -740,7 +833,7 @@ function registerAppsCommands(program2) {
|
|
|
740
833
|
return;
|
|
741
834
|
}
|
|
742
835
|
}
|
|
743
|
-
const
|
|
836
|
+
const _deleteSpinner = startSpinner("Deleting application...");
|
|
744
837
|
await client2.application.delete.mutate({
|
|
745
838
|
applicationId: app.applicationId
|
|
746
839
|
});
|
|
@@ -758,7 +851,7 @@ function registerAppsCommands(program2) {
|
|
|
758
851
|
try {
|
|
759
852
|
if (!isLoggedIn()) throw new AuthError();
|
|
760
853
|
const client2 = getApiClient();
|
|
761
|
-
const
|
|
854
|
+
const _spinner = startSpinner("Fetching application...");
|
|
762
855
|
const apps2 = await client2.application.allByOrganization.query();
|
|
763
856
|
const appSummary = findApp(apps2, appIdentifier);
|
|
764
857
|
if (!appSummary) {
|
|
@@ -835,7 +928,7 @@ function registerAppsCommands(program2) {
|
|
|
835
928
|
try {
|
|
836
929
|
if (!isLoggedIn()) throw new AuthError();
|
|
837
930
|
const client2 = getApiClient();
|
|
838
|
-
const
|
|
931
|
+
const _spinner = startSpinner("Finding application...");
|
|
839
932
|
const apps2 = await client2.application.allByOrganization.query();
|
|
840
933
|
const appSummary = findApp(apps2, appIdentifier);
|
|
841
934
|
if (!appSummary) {
|
|
@@ -857,7 +950,9 @@ function registerAppsCommands(program2) {
|
|
|
857
950
|
url = app.cloudServiceUrl;
|
|
858
951
|
}
|
|
859
952
|
if (!url) {
|
|
860
|
-
error(
|
|
953
|
+
error(
|
|
954
|
+
"No URL found for this application. Deploy first or add a domain."
|
|
955
|
+
);
|
|
861
956
|
return;
|
|
862
957
|
}
|
|
863
958
|
if (isJsonMode()) {
|
|
@@ -865,7 +960,7 @@ function registerAppsCommands(program2) {
|
|
|
865
960
|
return;
|
|
866
961
|
}
|
|
867
962
|
log(`Opening ${colors.cyan(url)}...`);
|
|
868
|
-
await
|
|
963
|
+
await open(url);
|
|
869
964
|
} catch (err) {
|
|
870
965
|
handleError(err);
|
|
871
966
|
}
|
|
@@ -888,89 +983,1268 @@ function formatDate(date) {
|
|
|
888
983
|
);
|
|
889
984
|
if (diffDays === 0) {
|
|
890
985
|
return "Today";
|
|
891
|
-
}
|
|
986
|
+
}
|
|
987
|
+
if (diffDays === 1) {
|
|
892
988
|
return "Yesterday";
|
|
893
|
-
}
|
|
989
|
+
}
|
|
990
|
+
if (diffDays < 7) {
|
|
894
991
|
return `${diffDays}d ago`;
|
|
895
|
-
} else {
|
|
896
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
897
992
|
}
|
|
993
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
898
994
|
}
|
|
899
995
|
|
|
900
|
-
// src/commands/
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
996
|
+
// src/commands/auth.ts
|
|
997
|
+
import open2 from "open";
|
|
998
|
+
|
|
999
|
+
// src/lib/auth-server.ts
|
|
1000
|
+
import express from "express";
|
|
1001
|
+
function startAuthServer() {
|
|
1002
|
+
return new Promise((resolve) => {
|
|
1003
|
+
const app = express();
|
|
1004
|
+
let server;
|
|
1005
|
+
let callbackResolver;
|
|
1006
|
+
let callbackRejecter;
|
|
1007
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
1008
|
+
callbackResolver = res;
|
|
1009
|
+
callbackRejecter = rej;
|
|
1010
|
+
});
|
|
1011
|
+
app.get("/callback", (req, res) => {
|
|
1012
|
+
const {
|
|
1013
|
+
token,
|
|
1014
|
+
userId,
|
|
1015
|
+
userEmail,
|
|
1016
|
+
userName,
|
|
1017
|
+
organizationId,
|
|
1018
|
+
organizationName,
|
|
1019
|
+
environmentId,
|
|
1020
|
+
environmentName,
|
|
1021
|
+
error: error2
|
|
1022
|
+
} = req.query;
|
|
1023
|
+
if (error2) {
|
|
1024
|
+
res.send(`
|
|
1025
|
+
<!DOCTYPE html>
|
|
1026
|
+
<html>
|
|
1027
|
+
<head>
|
|
1028
|
+
<title>Tarout CLI - Authentication Failed</title>
|
|
1029
|
+
<style>
|
|
1030
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
1031
|
+
.container { text-align: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
1032
|
+
h1 { color: #dc2626; margin-bottom: 16px; }
|
|
1033
|
+
p { color: #666; }
|
|
1034
|
+
</style>
|
|
1035
|
+
</head>
|
|
1036
|
+
<body>
|
|
1037
|
+
<div class="container">
|
|
1038
|
+
<h1>Authentication Failed</h1>
|
|
1039
|
+
<p>${error2}</p>
|
|
1040
|
+
<p>You can close this window and try again.</p>
|
|
1041
|
+
</div>
|
|
1042
|
+
</body>
|
|
1043
|
+
</html>
|
|
1044
|
+
`);
|
|
1045
|
+
callbackRejecter(new Error(String(error2)));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (!token || !userId || !userEmail || !organizationId || !organizationName || !environmentId || !environmentName) {
|
|
1049
|
+
res.status(400).send("Missing required parameters");
|
|
1050
|
+
callbackRejecter(
|
|
1051
|
+
new Error("Missing required parameters from auth callback")
|
|
914
1052
|
);
|
|
915
|
-
|
|
1053
|
+
return;
|
|
916
1054
|
}
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
1055
|
+
res.send(`
|
|
1056
|
+
<!DOCTYPE html>
|
|
1057
|
+
<html>
|
|
1058
|
+
<head>
|
|
1059
|
+
<title>Tarout CLI - Authenticated</title>
|
|
1060
|
+
<style>
|
|
1061
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
1062
|
+
.container { text-align: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
1063
|
+
h1 { color: #16a34a; margin-bottom: 16px; }
|
|
1064
|
+
p { color: #666; }
|
|
1065
|
+
.checkmark { font-size: 64px; margin-bottom: 16px; }
|
|
1066
|
+
</style>
|
|
1067
|
+
</head>
|
|
1068
|
+
<body>
|
|
1069
|
+
<div class="container">
|
|
1070
|
+
<div class="checkmark">\u2713</div>
|
|
1071
|
+
<h1>Authenticated!</h1>
|
|
1072
|
+
<p>You can close this window and return to the terminal.</p>
|
|
1073
|
+
</div>
|
|
1074
|
+
</body>
|
|
1075
|
+
</html>
|
|
1076
|
+
`);
|
|
1077
|
+
callbackResolver({
|
|
1078
|
+
token: String(token),
|
|
1079
|
+
userId: String(userId),
|
|
1080
|
+
userEmail: String(userEmail),
|
|
1081
|
+
userName: userName ? String(userName) : void 0,
|
|
1082
|
+
organizationId: String(organizationId),
|
|
1083
|
+
organizationName: String(organizationName),
|
|
1084
|
+
environmentId: String(environmentId),
|
|
1085
|
+
environmentName: String(environmentName)
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
server = app.listen(0, () => {
|
|
1089
|
+
const address = server.address();
|
|
1090
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
1091
|
+
resolve({
|
|
1092
|
+
port,
|
|
1093
|
+
waitForCallback: () => callbackPromise,
|
|
1094
|
+
close: () => server.close()
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
setTimeout(
|
|
1098
|
+
() => {
|
|
1099
|
+
callbackRejecter(
|
|
1100
|
+
new Error("Authentication timed out. Please try again.")
|
|
1101
|
+
);
|
|
1102
|
+
server.close();
|
|
1103
|
+
},
|
|
1104
|
+
5 * 60 * 1e3
|
|
1105
|
+
);
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/commands/auth.ts
|
|
1110
|
+
function registerAuthCommands(program2) {
|
|
1111
|
+
program2.command("login").description("Authenticate with Tarout via browser").option("--api-url <url>", "Custom API URL", "https://tarout.sa").action(async (options) => {
|
|
1112
|
+
try {
|
|
1113
|
+
if (isLoggedIn()) {
|
|
1114
|
+
const profile = getCurrentProfile();
|
|
1115
|
+
if (profile) {
|
|
1116
|
+
log(`Already logged in as ${colors.cyan(profile.userEmail)}`);
|
|
1117
|
+
log(`Organization: ${profile.organizationName}`);
|
|
1118
|
+
log("");
|
|
1119
|
+
log(`Run ${colors.dim("tarout logout")} to sign out first.`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const apiUrl = options.apiUrl;
|
|
1124
|
+
log("");
|
|
1125
|
+
log("Opening browser to authenticate...");
|
|
1126
|
+
const authServer = await startAuthServer();
|
|
1127
|
+
const callbackUrl = `http://localhost:${authServer.port}/callback`;
|
|
1128
|
+
const authUrl = `${apiUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
1129
|
+
await open2(authUrl);
|
|
1130
|
+
const _spinner = startSpinner("Waiting for authentication...");
|
|
1131
|
+
try {
|
|
1132
|
+
const authData = await authServer.waitForCallback();
|
|
1133
|
+
succeedSpinner("Authentication successful!");
|
|
1134
|
+
authServer.close();
|
|
1135
|
+
setProfile("default", {
|
|
1136
|
+
token: authData.token,
|
|
1137
|
+
apiUrl,
|
|
1138
|
+
userId: authData.userId,
|
|
1139
|
+
userEmail: authData.userEmail,
|
|
1140
|
+
userName: authData.userName,
|
|
1141
|
+
organizationId: authData.organizationId,
|
|
1142
|
+
organizationName: authData.organizationName,
|
|
1143
|
+
environmentId: authData.environmentId,
|
|
1144
|
+
environmentName: authData.environmentName
|
|
1145
|
+
});
|
|
1146
|
+
setCurrentProfile("default");
|
|
1147
|
+
if (isJsonMode()) {
|
|
960
1148
|
outputData({
|
|
961
|
-
|
|
962
|
-
|
|
1149
|
+
success: true,
|
|
1150
|
+
user: {
|
|
1151
|
+
id: authData.userId,
|
|
1152
|
+
email: authData.userEmail,
|
|
1153
|
+
name: authData.userName
|
|
1154
|
+
},
|
|
1155
|
+
organization: {
|
|
1156
|
+
id: authData.organizationId,
|
|
1157
|
+
name: authData.organizationName
|
|
1158
|
+
},
|
|
1159
|
+
environment: {
|
|
1160
|
+
id: authData.environmentId,
|
|
1161
|
+
name: authData.environmentName
|
|
1162
|
+
}
|
|
963
1163
|
});
|
|
964
1164
|
} 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
1165
|
log("");
|
|
1166
|
+
success(`Logged in as ${colors.cyan(authData.userEmail)}`);
|
|
1167
|
+
box("Account", [
|
|
1168
|
+
`Organization: ${colors.bold(authData.organizationName)}`,
|
|
1169
|
+
`Environment: ${colors.bold(authData.environmentName)}`
|
|
1170
|
+
]);
|
|
1171
|
+
}
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
failSpinner("Authentication failed");
|
|
1174
|
+
authServer.close();
|
|
1175
|
+
throw err;
|
|
1176
|
+
}
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
handleError(err);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
program2.command("logout").description("Sign out and clear stored credentials").action(async () => {
|
|
1182
|
+
try {
|
|
1183
|
+
if (!isLoggedIn()) {
|
|
1184
|
+
if (isJsonMode()) {
|
|
1185
|
+
outputData({ success: true, message: "Already logged out" });
|
|
1186
|
+
} else {
|
|
1187
|
+
log("Already logged out.");
|
|
1188
|
+
}
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const profile = getCurrentProfile();
|
|
1192
|
+
clearConfig();
|
|
1193
|
+
if (isJsonMode()) {
|
|
1194
|
+
outputData({ success: true, message: "Logged out successfully" });
|
|
1195
|
+
} else {
|
|
1196
|
+
success(`Logged out from ${profile?.userEmail || "Tarout"}`);
|
|
1197
|
+
}
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
handleError(err);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
program2.command("whoami").description("Show current authenticated user").action(async () => {
|
|
1203
|
+
try {
|
|
1204
|
+
if (!isLoggedIn()) {
|
|
1205
|
+
throw new AuthError();
|
|
1206
|
+
}
|
|
1207
|
+
const profile = getCurrentProfile();
|
|
1208
|
+
if (!profile) {
|
|
1209
|
+
throw new AuthError();
|
|
1210
|
+
}
|
|
1211
|
+
if (isJsonMode()) {
|
|
1212
|
+
outputData({
|
|
1213
|
+
user: {
|
|
1214
|
+
id: profile.userId,
|
|
1215
|
+
email: profile.userEmail,
|
|
1216
|
+
name: profile.userName
|
|
1217
|
+
},
|
|
1218
|
+
organization: {
|
|
1219
|
+
id: profile.organizationId,
|
|
1220
|
+
name: profile.organizationName
|
|
1221
|
+
},
|
|
1222
|
+
environment: {
|
|
1223
|
+
id: profile.environmentId,
|
|
1224
|
+
name: profile.environmentName
|
|
1225
|
+
},
|
|
1226
|
+
apiUrl: profile.apiUrl
|
|
1227
|
+
});
|
|
1228
|
+
} else {
|
|
1229
|
+
log("");
|
|
1230
|
+
log(`${colors.bold("User")}`);
|
|
1231
|
+
log(` Email: ${colors.cyan(profile.userEmail)}`);
|
|
1232
|
+
if (profile.userName) {
|
|
1233
|
+
log(` Name: ${profile.userName}`);
|
|
1234
|
+
}
|
|
1235
|
+
log("");
|
|
1236
|
+
log(`${colors.bold("Organization")}`);
|
|
1237
|
+
log(` Name: ${profile.organizationName}`);
|
|
1238
|
+
log(` ID: ${colors.dim(profile.organizationId)}`);
|
|
1239
|
+
log("");
|
|
1240
|
+
log(`${colors.bold("Environment")}`);
|
|
1241
|
+
log(` Name: ${profile.environmentName}`);
|
|
1242
|
+
log(` ID: ${colors.dim(profile.environmentId)}`);
|
|
1243
|
+
log("");
|
|
1244
|
+
}
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
handleError(err);
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/lib/process.ts
|
|
1252
|
+
import { spawn } from "child_process";
|
|
1253
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1254
|
+
import { join as join2 } from "path";
|
|
1255
|
+
function readPackageJson(basePath) {
|
|
1256
|
+
const base = basePath || process.cwd();
|
|
1257
|
+
const packagePath = join2(base, "package.json");
|
|
1258
|
+
if (!existsSync2(packagePath)) {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
const content = readFileSync2(packagePath, "utf-8");
|
|
1263
|
+
return JSON.parse(content);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function detectPackageManager(basePath) {
|
|
1269
|
+
const base = basePath || process.cwd();
|
|
1270
|
+
if (existsSync2(join2(base, "bun.lockb")) || existsSync2(join2(base, "bun.lock"))) {
|
|
1271
|
+
return "bun";
|
|
1272
|
+
}
|
|
1273
|
+
if (existsSync2(join2(base, "pnpm-lock.yaml"))) {
|
|
1274
|
+
return "pnpm";
|
|
1275
|
+
}
|
|
1276
|
+
if (existsSync2(join2(base, "yarn.lock"))) {
|
|
1277
|
+
return "yarn";
|
|
1278
|
+
}
|
|
1279
|
+
if (existsSync2(join2(base, "package-lock.json"))) {
|
|
1280
|
+
return "npm";
|
|
1281
|
+
}
|
|
1282
|
+
const pkg = readPackageJson(basePath);
|
|
1283
|
+
if (pkg?.packageManager) {
|
|
1284
|
+
if (pkg.packageManager.startsWith("bun")) return "bun";
|
|
1285
|
+
if (pkg.packageManager.startsWith("pnpm")) return "pnpm";
|
|
1286
|
+
if (pkg.packageManager.startsWith("yarn")) return "yarn";
|
|
1287
|
+
if (pkg.packageManager.startsWith("npm")) return "npm";
|
|
1288
|
+
}
|
|
1289
|
+
return "npm";
|
|
1290
|
+
}
|
|
1291
|
+
var FRAMEWORKS = [
|
|
1292
|
+
{
|
|
1293
|
+
dependencies: ["next"],
|
|
1294
|
+
info: {
|
|
1295
|
+
name: "Next.js",
|
|
1296
|
+
devCommand: "next dev",
|
|
1297
|
+
buildCommand: "next build",
|
|
1298
|
+
defaultPort: 3e3
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
dependencies: ["vite"],
|
|
1303
|
+
info: {
|
|
1304
|
+
name: "Vite",
|
|
1305
|
+
devCommand: "vite",
|
|
1306
|
+
buildCommand: "vite build",
|
|
1307
|
+
defaultPort: 5173
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
dependencies: ["@remix-run/dev"],
|
|
1312
|
+
info: {
|
|
1313
|
+
name: "Remix",
|
|
1314
|
+
devCommand: "remix dev",
|
|
1315
|
+
buildCommand: "remix build",
|
|
1316
|
+
defaultPort: 3e3
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
dependencies: ["nuxt"],
|
|
1321
|
+
info: {
|
|
1322
|
+
name: "Nuxt",
|
|
1323
|
+
devCommand: "nuxt dev",
|
|
1324
|
+
buildCommand: "nuxt build",
|
|
1325
|
+
defaultPort: 3e3
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
dependencies: ["astro"],
|
|
1330
|
+
info: {
|
|
1331
|
+
name: "Astro",
|
|
1332
|
+
devCommand: "astro dev",
|
|
1333
|
+
buildCommand: "astro build",
|
|
1334
|
+
defaultPort: 4321
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
dependencies: ["@angular/core"],
|
|
1339
|
+
info: {
|
|
1340
|
+
name: "Angular",
|
|
1341
|
+
devCommand: "ng serve",
|
|
1342
|
+
buildCommand: "ng build",
|
|
1343
|
+
defaultPort: 4200
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
dependencies: ["svelte-kit", "@sveltejs/kit"],
|
|
1348
|
+
info: {
|
|
1349
|
+
name: "SvelteKit",
|
|
1350
|
+
devCommand: "svelte-kit dev",
|
|
1351
|
+
buildCommand: "svelte-kit build",
|
|
1352
|
+
defaultPort: 5173
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
dependencies: ["svelte"],
|
|
1357
|
+
info: {
|
|
1358
|
+
name: "Svelte",
|
|
1359
|
+
devCommand: "vite",
|
|
1360
|
+
buildCommand: "vite build",
|
|
1361
|
+
defaultPort: 5173
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
dependencies: ["react-scripts"],
|
|
1366
|
+
info: {
|
|
1367
|
+
name: "Create React App",
|
|
1368
|
+
devCommand: "react-scripts start",
|
|
1369
|
+
buildCommand: "react-scripts build",
|
|
1370
|
+
defaultPort: 3e3
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
dependencies: ["gatsby"],
|
|
1375
|
+
info: {
|
|
1376
|
+
name: "Gatsby",
|
|
1377
|
+
devCommand: "gatsby develop",
|
|
1378
|
+
buildCommand: "gatsby build",
|
|
1379
|
+
defaultPort: 8e3
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
dependencies: ["express"],
|
|
1384
|
+
info: {
|
|
1385
|
+
name: "Express",
|
|
1386
|
+
devCommand: "node",
|
|
1387
|
+
buildCommand: "echo 'No build required'",
|
|
1388
|
+
defaultPort: 3e3
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
];
|
|
1392
|
+
function detectFramework(pkg) {
|
|
1393
|
+
const allDeps = {
|
|
1394
|
+
...pkg.dependencies,
|
|
1395
|
+
...pkg.devDependencies
|
|
1396
|
+
};
|
|
1397
|
+
for (const framework of FRAMEWORKS) {
|
|
1398
|
+
for (const dep of framework.dependencies) {
|
|
1399
|
+
if (dep in allDeps) {
|
|
1400
|
+
return framework.info;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
function getDevCommand(pkg, pm) {
|
|
1407
|
+
const devScripts = ["dev", "start:dev", "serve", "start"];
|
|
1408
|
+
for (const script of devScripts) {
|
|
1409
|
+
if (pkg.scripts?.[script]) {
|
|
1410
|
+
return `${pm} run ${script}`;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const framework = detectFramework(pkg);
|
|
1414
|
+
if (framework) {
|
|
1415
|
+
return `${pm === "npm" ? "npx" : pm} ${framework.devCommand}`;
|
|
1416
|
+
}
|
|
1417
|
+
return `${pm} run dev`;
|
|
1418
|
+
}
|
|
1419
|
+
function getBuildCommand(pkg, pm) {
|
|
1420
|
+
if (pkg.scripts?.build) {
|
|
1421
|
+
return `${pm} run build`;
|
|
1422
|
+
}
|
|
1423
|
+
const framework = detectFramework(pkg);
|
|
1424
|
+
if (framework) {
|
|
1425
|
+
return `${pm === "npm" ? "npx" : pm} ${framework.buildCommand}`;
|
|
1426
|
+
}
|
|
1427
|
+
return `${pm} run build`;
|
|
1428
|
+
}
|
|
1429
|
+
function getDefaultPort(pkg) {
|
|
1430
|
+
const framework = detectFramework(pkg);
|
|
1431
|
+
return framework?.defaultPort || 3e3;
|
|
1432
|
+
}
|
|
1433
|
+
function runCommand(command, env, options = {}) {
|
|
1434
|
+
return new Promise((resolve) => {
|
|
1435
|
+
const [cmd, ...args] = parseCommand(command);
|
|
1436
|
+
const spawnOptions = {
|
|
1437
|
+
cwd: options.cwd || process.cwd(),
|
|
1438
|
+
env: {
|
|
1439
|
+
...process.env,
|
|
1440
|
+
...env
|
|
1441
|
+
},
|
|
1442
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1443
|
+
shell: true
|
|
1444
|
+
};
|
|
1445
|
+
const child = spawn(cmd, args, spawnOptions);
|
|
1446
|
+
child.stdout?.on("data", (data) => {
|
|
1447
|
+
const str = data.toString();
|
|
1448
|
+
if (options.onStdout) {
|
|
1449
|
+
options.onStdout(str);
|
|
1450
|
+
} else {
|
|
1451
|
+
process.stdout.write(str);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
child.stderr?.on("data", (data) => {
|
|
1455
|
+
const str = data.toString();
|
|
1456
|
+
if (options.onStderr) {
|
|
1457
|
+
options.onStderr(str);
|
|
1458
|
+
} else {
|
|
1459
|
+
process.stderr.write(str);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
const handleSignal = () => {
|
|
1463
|
+
child.kill("SIGINT");
|
|
1464
|
+
};
|
|
1465
|
+
process.on("SIGINT", handleSignal);
|
|
1466
|
+
process.on("SIGTERM", handleSignal);
|
|
1467
|
+
child.on("close", (code, signal) => {
|
|
1468
|
+
process.off("SIGINT", handleSignal);
|
|
1469
|
+
process.off("SIGTERM", handleSignal);
|
|
1470
|
+
resolve({
|
|
1471
|
+
exitCode: code ?? 1,
|
|
1472
|
+
signal
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
child.on("error", (err) => {
|
|
1476
|
+
process.off("SIGINT", handleSignal);
|
|
1477
|
+
process.off("SIGTERM", handleSignal);
|
|
1478
|
+
console.error(`Failed to start process: ${err.message}`);
|
|
1479
|
+
resolve({
|
|
1480
|
+
exitCode: 1,
|
|
1481
|
+
signal: null
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
function parseCommand(command) {
|
|
1487
|
+
const parts = [];
|
|
1488
|
+
let current = "";
|
|
1489
|
+
let inQuote = false;
|
|
1490
|
+
let quoteChar = "";
|
|
1491
|
+
for (const char of command) {
|
|
1492
|
+
if ((char === '"' || char === "'") && !inQuote) {
|
|
1493
|
+
inQuote = true;
|
|
1494
|
+
quoteChar = char;
|
|
1495
|
+
} else if (char === quoteChar && inQuote) {
|
|
1496
|
+
inQuote = false;
|
|
1497
|
+
quoteChar = "";
|
|
1498
|
+
} else if (char === " " && !inQuote) {
|
|
1499
|
+
if (current) {
|
|
1500
|
+
parts.push(current);
|
|
1501
|
+
current = "";
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
current += char;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
if (current) {
|
|
1508
|
+
parts.push(current);
|
|
1509
|
+
}
|
|
1510
|
+
return parts;
|
|
1511
|
+
}
|
|
1512
|
+
function envVarsToObject(variables) {
|
|
1513
|
+
const result = {};
|
|
1514
|
+
for (const v of variables) {
|
|
1515
|
+
if (v.value !== null) {
|
|
1516
|
+
result[v.key] = v.value;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return result;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/commands/build.ts
|
|
1523
|
+
function registerBuildCommand(program2) {
|
|
1524
|
+
program2.command("build").description("Build locally with cloud environment variables").option("-a, --app <app>", "Application ID or name (overrides linked app)").option("-c, --command <command>", "Custom build command to run").action(async (options) => {
|
|
1525
|
+
try {
|
|
1526
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1527
|
+
const profile = getCurrentProfile();
|
|
1528
|
+
if (!profile) throw new AuthError();
|
|
1529
|
+
const client2 = getApiClient();
|
|
1530
|
+
let applicationId;
|
|
1531
|
+
let appName;
|
|
1532
|
+
if (options.app) {
|
|
1533
|
+
const _spinner = startSpinner("Finding application...");
|
|
1534
|
+
const apps = await client2.application.allByOrganization.query();
|
|
1535
|
+
const app = findApp2(apps, options.app);
|
|
1536
|
+
if (!app) {
|
|
1537
|
+
failSpinner();
|
|
1538
|
+
const suggestions = findSimilar(
|
|
1539
|
+
options.app,
|
|
1540
|
+
apps.map((a) => a.name)
|
|
1541
|
+
);
|
|
1542
|
+
throw new NotFoundError("Application", options.app, suggestions);
|
|
973
1543
|
}
|
|
1544
|
+
applicationId = app.applicationId;
|
|
1545
|
+
appName = app.name;
|
|
1546
|
+
succeedSpinner();
|
|
1547
|
+
} else if (isProjectLinked()) {
|
|
1548
|
+
const config2 = getProjectConfig();
|
|
1549
|
+
if (!config2) {
|
|
1550
|
+
throw new CliError(
|
|
1551
|
+
"Project config is corrupted. Run 'tarout link' to relink."
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
applicationId = config2.applicationId;
|
|
1555
|
+
appName = config2.name;
|
|
1556
|
+
} else {
|
|
1557
|
+
throw new InvalidArgumentError(
|
|
1558
|
+
"No linked application. Run 'tarout link' first or use --app flag."
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
const pkg = readPackageJson();
|
|
1562
|
+
if (!pkg) {
|
|
1563
|
+
throw new CliError(
|
|
1564
|
+
"No package.json found in current directory. Make sure you're in a Node.js project."
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
const pm = detectPackageManager();
|
|
1568
|
+
let buildCommand = options.command;
|
|
1569
|
+
if (!buildCommand) {
|
|
1570
|
+
buildCommand = getBuildCommand(pkg, pm);
|
|
1571
|
+
}
|
|
1572
|
+
const _envSpinner = startSpinner(
|
|
1573
|
+
`Fetching environment variables for ${appName}...`
|
|
1574
|
+
);
|
|
1575
|
+
let envVars = {};
|
|
1576
|
+
try {
|
|
1577
|
+
const variables = await client2.envVariable.list.query({
|
|
1578
|
+
applicationId,
|
|
1579
|
+
includeValues: true
|
|
1580
|
+
});
|
|
1581
|
+
envVars = envVarsToObject(variables);
|
|
1582
|
+
succeedSpinner(
|
|
1583
|
+
`Loaded ${Object.keys(envVars).length} environment variables`
|
|
1584
|
+
);
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
failSpinner();
|
|
1587
|
+
throw new CliError(
|
|
1588
|
+
`Failed to fetch environment variables: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
envVars.NODE_ENV = envVars.NODE_ENV || "production";
|
|
1592
|
+
const framework = detectFramework(pkg);
|
|
1593
|
+
if (isJsonMode()) {
|
|
1594
|
+
const startTime2 = Date.now();
|
|
1595
|
+
const result2 = await runCommand(buildCommand, envVars);
|
|
1596
|
+
const duration2 = Math.round((Date.now() - startTime2) / 1e3);
|
|
1597
|
+
outputData({
|
|
1598
|
+
success: result2.exitCode === 0,
|
|
1599
|
+
applicationId,
|
|
1600
|
+
appName,
|
|
1601
|
+
command: buildCommand,
|
|
1602
|
+
framework: framework?.name || "Unknown",
|
|
1603
|
+
envVarCount: Object.keys(envVars).length,
|
|
1604
|
+
packageManager: pm,
|
|
1605
|
+
exitCode: result2.exitCode,
|
|
1606
|
+
duration: duration2
|
|
1607
|
+
});
|
|
1608
|
+
if (result2.exitCode !== 0) {
|
|
1609
|
+
process.exit(result2.exitCode);
|
|
1610
|
+
}
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
log("");
|
|
1614
|
+
log(colors.bold(`Building ${colors.cyan(appName)}`));
|
|
1615
|
+
log("");
|
|
1616
|
+
log(` Framework: ${colors.dim(framework?.name || "Unknown")}`);
|
|
1617
|
+
log(` Package Manager: ${colors.dim(pm)}`);
|
|
1618
|
+
log(` Command: ${colors.dim(buildCommand)}`);
|
|
1619
|
+
log(
|
|
1620
|
+
` Env Variables: ${colors.dim(String(Object.keys(envVars).length))}`
|
|
1621
|
+
);
|
|
1622
|
+
log("");
|
|
1623
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
1624
|
+
log("");
|
|
1625
|
+
const startTime = Date.now();
|
|
1626
|
+
const result = await runCommand(buildCommand, envVars);
|
|
1627
|
+
const duration = Math.round((Date.now() - startTime) / 1e3);
|
|
1628
|
+
log("");
|
|
1629
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
1630
|
+
log("");
|
|
1631
|
+
if (result.exitCode === 0) {
|
|
1632
|
+
log(colors.success(`Build completed successfully in ${duration}s`));
|
|
1633
|
+
log("");
|
|
1634
|
+
log("Next steps:");
|
|
1635
|
+
log(` ${colors.dim("tarout deploy")} - Deploy to cloud`);
|
|
1636
|
+
log("");
|
|
1637
|
+
} else {
|
|
1638
|
+
log(
|
|
1639
|
+
colors.error(
|
|
1640
|
+
`Build failed with exit code ${result.exitCode} (${duration}s)`
|
|
1641
|
+
)
|
|
1642
|
+
);
|
|
1643
|
+
log("");
|
|
1644
|
+
log("Troubleshooting:");
|
|
1645
|
+
log(` ${colors.dim("1.")} Check the build output above for errors`);
|
|
1646
|
+
log(` ${colors.dim("2.")} Verify all dependencies are installed`);
|
|
1647
|
+
log(
|
|
1648
|
+
` ${colors.dim("3.")} Make sure environment variables are correct`
|
|
1649
|
+
);
|
|
1650
|
+
log("");
|
|
1651
|
+
throw new BuildFailedError(
|
|
1652
|
+
`Build failed with exit code ${result.exitCode}`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
handleError(err);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
function findApp2(apps, identifier) {
|
|
1661
|
+
const lowerIdentifier = identifier.toLowerCase();
|
|
1662
|
+
return apps.find(
|
|
1663
|
+
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/commands/db.ts
|
|
1668
|
+
import { spawn as spawn2 } from "child_process";
|
|
1669
|
+
function registerDbCommands(program2) {
|
|
1670
|
+
const db = program2.command("db").description("Manage databases");
|
|
1671
|
+
db.command("list").alias("ls").description("List all databases").option("-t, --type <type>", "Filter by type (postgres, mysql, redis)").action(async (options) => {
|
|
1672
|
+
try {
|
|
1673
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1674
|
+
const client2 = getApiClient();
|
|
1675
|
+
const _spinner = startSpinner("Fetching databases...");
|
|
1676
|
+
const [postgres, mysql, redis] = await Promise.all([
|
|
1677
|
+
client2.postgres.allByOrganization.query(),
|
|
1678
|
+
client2.mysql.allByOrganization.query(),
|
|
1679
|
+
client2.redis.allByOrganization.query()
|
|
1680
|
+
]);
|
|
1681
|
+
succeedSpinner();
|
|
1682
|
+
let databases = [];
|
|
1683
|
+
if (!options.type || options.type === "postgres") {
|
|
1684
|
+
databases = databases.concat(
|
|
1685
|
+
postgres.map((db2) => ({
|
|
1686
|
+
id: db2.postgresId,
|
|
1687
|
+
name: db2.name,
|
|
1688
|
+
type: "postgres",
|
|
1689
|
+
status: db2.applicationStatus,
|
|
1690
|
+
created: db2.createdAt
|
|
1691
|
+
}))
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
if (!options.type || options.type === "mysql") {
|
|
1695
|
+
databases = databases.concat(
|
|
1696
|
+
mysql.map((db2) => ({
|
|
1697
|
+
id: db2.mysqlId,
|
|
1698
|
+
name: db2.name,
|
|
1699
|
+
type: "mysql",
|
|
1700
|
+
status: db2.applicationStatus,
|
|
1701
|
+
created: db2.createdAt
|
|
1702
|
+
}))
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (!options.type || options.type === "redis") {
|
|
1706
|
+
databases = databases.concat(
|
|
1707
|
+
redis.map((db2) => ({
|
|
1708
|
+
id: db2.redisId,
|
|
1709
|
+
name: db2.name,
|
|
1710
|
+
type: "redis",
|
|
1711
|
+
status: db2.applicationStatus,
|
|
1712
|
+
created: db2.createdAt
|
|
1713
|
+
}))
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
if (isJsonMode()) {
|
|
1717
|
+
outputData(databases);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
if (databases.length === 0) {
|
|
1721
|
+
log("");
|
|
1722
|
+
log("No databases found.");
|
|
1723
|
+
log("");
|
|
1724
|
+
log(`Create one with: ${colors.dim("tarout db create <name>")}`);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
log("");
|
|
1728
|
+
table(
|
|
1729
|
+
["ID", "NAME", "TYPE", "STATUS", "CREATED"],
|
|
1730
|
+
databases.map((db2) => [
|
|
1731
|
+
colors.cyan(db2.id.slice(0, 8)),
|
|
1732
|
+
db2.name,
|
|
1733
|
+
getTypeLabel(db2.type),
|
|
1734
|
+
getStatusBadge(db2.status),
|
|
1735
|
+
formatDate2(db2.created)
|
|
1736
|
+
])
|
|
1737
|
+
);
|
|
1738
|
+
log("");
|
|
1739
|
+
log(
|
|
1740
|
+
colors.dim(
|
|
1741
|
+
`${databases.length} database${databases.length === 1 ? "" : "s"}`
|
|
1742
|
+
)
|
|
1743
|
+
);
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
handleError(err);
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
db.command("create").argument("[name]", "Database name").description("Create a new database").option(
|
|
1749
|
+
"-t, --type <type>",
|
|
1750
|
+
"Database type (postgres, mysql, redis)",
|
|
1751
|
+
"postgres"
|
|
1752
|
+
).option("-d, --description <description>", "Database description").action(async (name, options) => {
|
|
1753
|
+
try {
|
|
1754
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1755
|
+
const profile = getCurrentProfile();
|
|
1756
|
+
if (!profile) throw new AuthError();
|
|
1757
|
+
let dbName = name;
|
|
1758
|
+
let dbType = options.type;
|
|
1759
|
+
if (!dbName) {
|
|
1760
|
+
dbName = await input("Database name:");
|
|
1761
|
+
}
|
|
1762
|
+
if (!options.type && !shouldSkipConfirmation()) {
|
|
1763
|
+
dbType = await select("Database type:", [
|
|
1764
|
+
{ name: "PostgreSQL", value: "postgres" },
|
|
1765
|
+
{ name: "MySQL", value: "mysql" },
|
|
1766
|
+
{ name: "Redis", value: "redis" }
|
|
1767
|
+
]);
|
|
1768
|
+
}
|
|
1769
|
+
const slug = generateSlug2(dbName);
|
|
1770
|
+
const client2 = getApiClient();
|
|
1771
|
+
const _spinner = startSpinner(`Creating ${dbType} database...`);
|
|
1772
|
+
let database;
|
|
1773
|
+
switch (dbType) {
|
|
1774
|
+
case "postgres":
|
|
1775
|
+
database = await client2.postgres.create.mutate({
|
|
1776
|
+
name: dbName,
|
|
1777
|
+
appName: slug,
|
|
1778
|
+
dockerImage: "postgres:17",
|
|
1779
|
+
organizationId: profile.organizationId,
|
|
1780
|
+
description: options.description
|
|
1781
|
+
});
|
|
1782
|
+
break;
|
|
1783
|
+
case "mysql":
|
|
1784
|
+
database = await client2.mysql.create.mutate({
|
|
1785
|
+
name: dbName,
|
|
1786
|
+
appName: slug,
|
|
1787
|
+
dockerImage: "mysql:8",
|
|
1788
|
+
organizationId: profile.organizationId,
|
|
1789
|
+
description: options.description
|
|
1790
|
+
});
|
|
1791
|
+
break;
|
|
1792
|
+
case "redis":
|
|
1793
|
+
database = await client2.redis.create.mutate({
|
|
1794
|
+
name: dbName,
|
|
1795
|
+
appName: slug,
|
|
1796
|
+
dockerImage: "redis:7",
|
|
1797
|
+
organizationId: profile.organizationId,
|
|
1798
|
+
description: options.description
|
|
1799
|
+
});
|
|
1800
|
+
break;
|
|
1801
|
+
default:
|
|
1802
|
+
throw new CliError(
|
|
1803
|
+
`Unsupported database type: ${dbType}`,
|
|
1804
|
+
ExitCode.INVALID_ARGUMENTS
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
succeedSpinner("Database created!");
|
|
1808
|
+
const dbId = database.postgresId || database.mysqlId || database.redisId;
|
|
1809
|
+
if (isJsonMode()) {
|
|
1810
|
+
outputData(database);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
quietOutput(dbId);
|
|
1814
|
+
box("Database Created", [
|
|
1815
|
+
`ID: ${colors.cyan(dbId)}`,
|
|
1816
|
+
`Name: ${database.name}`,
|
|
1817
|
+
`Type: ${getTypeLabel(dbType)}`
|
|
1818
|
+
]);
|
|
1819
|
+
log("Next steps:");
|
|
1820
|
+
log(
|
|
1821
|
+
` View connection info: ${colors.dim(`tarout db info ${dbId.slice(0, 8)}`)}`
|
|
1822
|
+
);
|
|
1823
|
+
log("");
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
handleError(err);
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
db.command("delete").alias("rm").argument("<db>", "Database ID or name").description("Delete a database").action(async (dbIdentifier) => {
|
|
1829
|
+
try {
|
|
1830
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1831
|
+
const client2 = getApiClient();
|
|
1832
|
+
const _spinner = startSpinner("Finding database...");
|
|
1833
|
+
const allDbs = await getAllDatabases(client2);
|
|
1834
|
+
const dbInfo = findDatabase(allDbs, dbIdentifier);
|
|
1835
|
+
if (!dbInfo) {
|
|
1836
|
+
failSpinner();
|
|
1837
|
+
const suggestions = findSimilar(
|
|
1838
|
+
dbIdentifier,
|
|
1839
|
+
allDbs.map((d) => d.name)
|
|
1840
|
+
);
|
|
1841
|
+
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1842
|
+
}
|
|
1843
|
+
succeedSpinner();
|
|
1844
|
+
if (!shouldSkipConfirmation()) {
|
|
1845
|
+
log("");
|
|
1846
|
+
log(`Database: ${colors.bold(dbInfo.name)}`);
|
|
1847
|
+
log(`Type: ${getTypeLabel(dbInfo.type)}`);
|
|
1848
|
+
log(`ID: ${colors.dim(dbInfo.id)}`);
|
|
1849
|
+
log("");
|
|
1850
|
+
const confirmed = await confirm(
|
|
1851
|
+
`Are you sure you want to delete "${dbInfo.name}"? This cannot be undone.`,
|
|
1852
|
+
false
|
|
1853
|
+
);
|
|
1854
|
+
if (!confirmed) {
|
|
1855
|
+
log("Cancelled.");
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const _deleteSpinner = startSpinner("Deleting database...");
|
|
1860
|
+
switch (dbInfo.type) {
|
|
1861
|
+
case "postgres":
|
|
1862
|
+
await client2.postgres.remove.mutate({ postgresId: dbInfo.id });
|
|
1863
|
+
break;
|
|
1864
|
+
case "mysql":
|
|
1865
|
+
await client2.mysql.remove.mutate({ mysqlId: dbInfo.id });
|
|
1866
|
+
break;
|
|
1867
|
+
case "redis":
|
|
1868
|
+
await client2.redis.remove.mutate({ redisId: dbInfo.id });
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
succeedSpinner("Database deleted!");
|
|
1872
|
+
if (isJsonMode()) {
|
|
1873
|
+
outputData({ deleted: true, id: dbInfo.id });
|
|
1874
|
+
} else {
|
|
1875
|
+
quietOutput(dbInfo.id);
|
|
1876
|
+
}
|
|
1877
|
+
} catch (err) {
|
|
1878
|
+
handleError(err);
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
db.command("info").argument("<db>", "Database ID or name").description("Show database details and connection info").action(async (dbIdentifier) => {
|
|
1882
|
+
try {
|
|
1883
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1884
|
+
const client2 = getApiClient();
|
|
1885
|
+
const _spinner = startSpinner("Fetching database info...");
|
|
1886
|
+
const allDbs = await getAllDatabases(client2);
|
|
1887
|
+
const dbSummary = findDatabase(allDbs, dbIdentifier);
|
|
1888
|
+
if (!dbSummary) {
|
|
1889
|
+
failSpinner();
|
|
1890
|
+
const suggestions = findSimilar(
|
|
1891
|
+
dbIdentifier,
|
|
1892
|
+
allDbs.map((d) => d.name)
|
|
1893
|
+
);
|
|
1894
|
+
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1895
|
+
}
|
|
1896
|
+
let dbDetails;
|
|
1897
|
+
switch (dbSummary.type) {
|
|
1898
|
+
case "postgres":
|
|
1899
|
+
dbDetails = await client2.postgres.one.query({
|
|
1900
|
+
postgresId: dbSummary.id
|
|
1901
|
+
});
|
|
1902
|
+
break;
|
|
1903
|
+
case "mysql":
|
|
1904
|
+
dbDetails = await client2.mysql.one.query({ mysqlId: dbSummary.id });
|
|
1905
|
+
break;
|
|
1906
|
+
case "redis":
|
|
1907
|
+
dbDetails = await client2.redis.one.query({ redisId: dbSummary.id });
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
succeedSpinner();
|
|
1911
|
+
if (isJsonMode()) {
|
|
1912
|
+
outputData(dbDetails);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
log("");
|
|
1916
|
+
log(colors.bold(dbDetails.name));
|
|
1917
|
+
log(colors.dim(dbSummary.id));
|
|
1918
|
+
log("");
|
|
1919
|
+
log(`${colors.bold("Status")}`);
|
|
1920
|
+
log(` ${getStatusBadge(dbDetails.applicationStatus)}`);
|
|
1921
|
+
log(` Type: ${getTypeLabel(dbSummary.type)}`);
|
|
1922
|
+
log("");
|
|
1923
|
+
log(`${colors.bold("Connection")}`);
|
|
1924
|
+
if (dbSummary.type === "redis") {
|
|
1925
|
+
if (dbDetails.cloudHost) {
|
|
1926
|
+
log(` Host: ${colors.cyan(dbDetails.cloudHost)}`);
|
|
1927
|
+
log(` Port: ${dbDetails.cloudPort || 6379}`);
|
|
1928
|
+
if (dbDetails.cloudPassword) {
|
|
1929
|
+
log(` Password: ${colors.dim("********")}`);
|
|
1930
|
+
}
|
|
1931
|
+
} else {
|
|
1932
|
+
log(` ${colors.dim("Not yet deployed")}`);
|
|
1933
|
+
}
|
|
1934
|
+
} else {
|
|
1935
|
+
if (dbDetails.cloudInstanceId || dbDetails.databaseName) {
|
|
1936
|
+
log(` Host: ${colors.cyan(dbDetails.cloudHost || "localhost")}`);
|
|
1937
|
+
log(
|
|
1938
|
+
` Port: ${dbDetails.cloudPort || (dbSummary.type === "postgres" ? 5432 : 3306)}`
|
|
1939
|
+
);
|
|
1940
|
+
log(
|
|
1941
|
+
` Database: ${dbDetails.cloudDatabaseName || dbDetails.databaseName}`
|
|
1942
|
+
);
|
|
1943
|
+
log(
|
|
1944
|
+
` Username: ${dbDetails.cloudUsername || dbDetails.databaseUser}`
|
|
1945
|
+
);
|
|
1946
|
+
log(` Password: ${colors.dim("********")}`);
|
|
1947
|
+
} else {
|
|
1948
|
+
log(` ${colors.dim("Not yet deployed")}`);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
log("");
|
|
1952
|
+
if (dbDetails.cloudHost || dbDetails.databaseName) {
|
|
1953
|
+
log(`${colors.bold("Connection String")}`);
|
|
1954
|
+
const connStr = getConnectionString(dbSummary.type, dbDetails);
|
|
1955
|
+
log(` ${colors.cyan(connStr)}`);
|
|
1956
|
+
log("");
|
|
1957
|
+
}
|
|
1958
|
+
log(`${colors.bold("Created")}`);
|
|
1959
|
+
log(` ${new Date(dbDetails.createdAt).toLocaleString()}`);
|
|
1960
|
+
log("");
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
handleError(err);
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
db.command("connect").argument("<db>", "Database ID or name").description("Open interactive database shell").action(async (dbIdentifier) => {
|
|
1966
|
+
try {
|
|
1967
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
1968
|
+
const client2 = getApiClient();
|
|
1969
|
+
const _spinner = startSpinner("Connecting to database...");
|
|
1970
|
+
const allDbs = await getAllDatabases(client2);
|
|
1971
|
+
const dbSummary = findDatabase(allDbs, dbIdentifier);
|
|
1972
|
+
if (!dbSummary) {
|
|
1973
|
+
failSpinner();
|
|
1974
|
+
const suggestions = findSimilar(
|
|
1975
|
+
dbIdentifier,
|
|
1976
|
+
allDbs.map((d) => d.name)
|
|
1977
|
+
);
|
|
1978
|
+
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1979
|
+
}
|
|
1980
|
+
let dbDetails;
|
|
1981
|
+
switch (dbSummary.type) {
|
|
1982
|
+
case "postgres":
|
|
1983
|
+
dbDetails = await client2.postgres.one.query({
|
|
1984
|
+
postgresId: dbSummary.id
|
|
1985
|
+
});
|
|
1986
|
+
break;
|
|
1987
|
+
case "mysql":
|
|
1988
|
+
dbDetails = await client2.mysql.one.query({ mysqlId: dbSummary.id });
|
|
1989
|
+
break;
|
|
1990
|
+
case "redis":
|
|
1991
|
+
dbDetails = await client2.redis.one.query({ redisId: dbSummary.id });
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1994
|
+
succeedSpinner();
|
|
1995
|
+
if (!dbDetails.cloudHost && !dbDetails.databaseName) {
|
|
1996
|
+
throw new CliError(
|
|
1997
|
+
"Database not deployed yet. Deploy first.",
|
|
1998
|
+
ExitCode.GENERAL_ERROR
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
const { command, args, env } = getConnectCommand(
|
|
2002
|
+
dbSummary.type,
|
|
2003
|
+
dbDetails
|
|
2004
|
+
);
|
|
2005
|
+
log("");
|
|
2006
|
+
log(`Connecting to ${colors.bold(dbDetails.name)}...`);
|
|
2007
|
+
log(colors.dim("Press Ctrl+D to exit"));
|
|
2008
|
+
log("");
|
|
2009
|
+
const child = spawn2(command, args, {
|
|
2010
|
+
stdio: "inherit",
|
|
2011
|
+
env: { ...process.env, ...env }
|
|
2012
|
+
});
|
|
2013
|
+
child.on("exit", (code) => {
|
|
2014
|
+
process.exit(code || 0);
|
|
2015
|
+
});
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
handleError(err);
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
async function getAllDatabases(client2) {
|
|
2022
|
+
const [postgres, mysql, redis] = await Promise.all([
|
|
2023
|
+
client2.postgres.allByOrganization.query(),
|
|
2024
|
+
client2.mysql.allByOrganization.query(),
|
|
2025
|
+
client2.redis.allByOrganization.query()
|
|
2026
|
+
]);
|
|
2027
|
+
const databases = [];
|
|
2028
|
+
for (const db of postgres) {
|
|
2029
|
+
databases.push({
|
|
2030
|
+
id: db.postgresId,
|
|
2031
|
+
name: db.name,
|
|
2032
|
+
type: "postgres",
|
|
2033
|
+
status: db.applicationStatus
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
for (const db of mysql) {
|
|
2037
|
+
databases.push({
|
|
2038
|
+
id: db.mysqlId,
|
|
2039
|
+
name: db.name,
|
|
2040
|
+
type: "mysql",
|
|
2041
|
+
status: db.applicationStatus
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
for (const db of redis) {
|
|
2045
|
+
databases.push({
|
|
2046
|
+
id: db.redisId,
|
|
2047
|
+
name: db.name,
|
|
2048
|
+
type: "redis",
|
|
2049
|
+
status: db.applicationStatus
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
return databases;
|
|
2053
|
+
}
|
|
2054
|
+
function findDatabase(databases, identifier) {
|
|
2055
|
+
const lowerIdentifier = identifier.toLowerCase();
|
|
2056
|
+
return databases.find(
|
|
2057
|
+
(db) => db.id === identifier || db.id.startsWith(identifier) || db.name.toLowerCase() === lowerIdentifier
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
function generateSlug2(name) {
|
|
2061
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
2062
|
+
}
|
|
2063
|
+
function formatDate2(date) {
|
|
2064
|
+
const d = new Date(date);
|
|
2065
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
2066
|
+
}
|
|
2067
|
+
function getTypeLabel(type) {
|
|
2068
|
+
const labels = {
|
|
2069
|
+
postgres: colors.info("PostgreSQL"),
|
|
2070
|
+
mysql: colors.warn("MySQL"),
|
|
2071
|
+
redis: colors.error("Redis")
|
|
2072
|
+
};
|
|
2073
|
+
return labels[type] || type;
|
|
2074
|
+
}
|
|
2075
|
+
function getConnectionString(type, details) {
|
|
2076
|
+
const host = details.cloudHost || "localhost";
|
|
2077
|
+
const user = details.cloudUsername || details.databaseUser || "user";
|
|
2078
|
+
const dbName = details.cloudDatabaseName || details.databaseName || "db";
|
|
2079
|
+
switch (type) {
|
|
2080
|
+
case "postgres": {
|
|
2081
|
+
const pgPort = details.cloudPort || 5432;
|
|
2082
|
+
return `postgresql://${user}:****@${host}:${pgPort}/${dbName}`;
|
|
2083
|
+
}
|
|
2084
|
+
case "mysql": {
|
|
2085
|
+
const myPort = details.cloudPort || 3306;
|
|
2086
|
+
return `mysql://${user}:****@${host}:${myPort}/${dbName}`;
|
|
2087
|
+
}
|
|
2088
|
+
case "redis": {
|
|
2089
|
+
const redisPort = details.cloudPort || 6379;
|
|
2090
|
+
return `redis://:****@${host}:${redisPort}`;
|
|
2091
|
+
}
|
|
2092
|
+
default:
|
|
2093
|
+
return "";
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function getConnectCommand(type, details) {
|
|
2097
|
+
const host = details.cloudHost || "localhost";
|
|
2098
|
+
const user = details.cloudUsername || details.databaseUser || "user";
|
|
2099
|
+
const password = details.cloudPassword || details.databasePassword || "";
|
|
2100
|
+
const dbName = details.cloudDatabaseName || details.databaseName || "db";
|
|
2101
|
+
switch (type) {
|
|
2102
|
+
case "postgres":
|
|
2103
|
+
return {
|
|
2104
|
+
command: "psql",
|
|
2105
|
+
args: [
|
|
2106
|
+
"-h",
|
|
2107
|
+
host,
|
|
2108
|
+
"-p",
|
|
2109
|
+
String(details.cloudPort || 5432),
|
|
2110
|
+
"-U",
|
|
2111
|
+
user,
|
|
2112
|
+
"-d",
|
|
2113
|
+
dbName
|
|
2114
|
+
],
|
|
2115
|
+
env: { PGPASSWORD: password }
|
|
2116
|
+
};
|
|
2117
|
+
case "mysql":
|
|
2118
|
+
return {
|
|
2119
|
+
command: "mysql",
|
|
2120
|
+
args: [
|
|
2121
|
+
"-h",
|
|
2122
|
+
host,
|
|
2123
|
+
"-P",
|
|
2124
|
+
String(details.cloudPort || 3306),
|
|
2125
|
+
"-u",
|
|
2126
|
+
user,
|
|
2127
|
+
`-p${password}`,
|
|
2128
|
+
dbName
|
|
2129
|
+
],
|
|
2130
|
+
env: {}
|
|
2131
|
+
};
|
|
2132
|
+
case "redis":
|
|
2133
|
+
return {
|
|
2134
|
+
command: "redis-cli",
|
|
2135
|
+
args: [
|
|
2136
|
+
"-h",
|
|
2137
|
+
host,
|
|
2138
|
+
"-p",
|
|
2139
|
+
String(details.cloudPort || 6379),
|
|
2140
|
+
...password ? ["-a", password] : []
|
|
2141
|
+
],
|
|
2142
|
+
env: {}
|
|
2143
|
+
};
|
|
2144
|
+
default:
|
|
2145
|
+
throw new CliError(
|
|
2146
|
+
`Unsupported database type: ${type}`,
|
|
2147
|
+
ExitCode.INVALID_ARGUMENTS
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/lib/websocket.ts
|
|
2153
|
+
import WebSocket from "ws";
|
|
2154
|
+
function streamDeploymentLogs(logPath, options) {
|
|
2155
|
+
const token = getToken();
|
|
2156
|
+
const apiUrl = getApiUrl();
|
|
2157
|
+
const wsUrl = apiUrl.replace(/^http/, "ws");
|
|
2158
|
+
const fullUrl = `${wsUrl}/listen-deployment?logPath=${encodeURIComponent(logPath)}`;
|
|
2159
|
+
const ws = new WebSocket(fullUrl, {
|
|
2160
|
+
headers: {
|
|
2161
|
+
"x-api-key": token || ""
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
let buffer = "";
|
|
2165
|
+
const done = new Promise((resolve) => {
|
|
2166
|
+
ws.on("close", (code, reason) => {
|
|
2167
|
+
if (buffer.length > 0) {
|
|
2168
|
+
options.onData(buffer);
|
|
2169
|
+
}
|
|
2170
|
+
options.onClose?.(code, reason.toString());
|
|
2171
|
+
resolve({ code, reason: reason.toString() });
|
|
2172
|
+
});
|
|
2173
|
+
});
|
|
2174
|
+
ws.on("open", () => {
|
|
2175
|
+
options.onOpen?.();
|
|
2176
|
+
});
|
|
2177
|
+
ws.on("message", (data) => {
|
|
2178
|
+
buffer += data.toString();
|
|
2179
|
+
const lines = buffer.split("\n");
|
|
2180
|
+
buffer = lines.pop() || "";
|
|
2181
|
+
for (const line of lines) {
|
|
2182
|
+
if (line.length > 0) {
|
|
2183
|
+
options.onData(line);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
ws.on("error", (error2) => {
|
|
2188
|
+
options.onError?.(error2);
|
|
2189
|
+
});
|
|
2190
|
+
const cleanup = () => {
|
|
2191
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2192
|
+
ws.close();
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
return { cleanup, done };
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// src/commands/deploy.ts
|
|
2199
|
+
function registerDeployCommands(program2) {
|
|
2200
|
+
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) => {
|
|
2201
|
+
try {
|
|
2202
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2203
|
+
const client2 = getApiClient();
|
|
2204
|
+
const _spinner = startSpinner("Finding application...");
|
|
2205
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2206
|
+
const app = findApp3(apps, appIdentifier);
|
|
2207
|
+
if (!app) {
|
|
2208
|
+
failSpinner();
|
|
2209
|
+
const suggestions = findSimilar(
|
|
2210
|
+
appIdentifier,
|
|
2211
|
+
apps.map((a) => a.name)
|
|
2212
|
+
);
|
|
2213
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2214
|
+
}
|
|
2215
|
+
updateSpinner(`Deploying ${app.name}...`);
|
|
2216
|
+
const result = await client2.application.deployToCloud.mutate({
|
|
2217
|
+
applicationId: app.applicationId,
|
|
2218
|
+
region: options.region
|
|
2219
|
+
});
|
|
2220
|
+
if (options.wait) {
|
|
2221
|
+
await streamDeploymentWithLogs(
|
|
2222
|
+
client2,
|
|
2223
|
+
result.deploymentId,
|
|
2224
|
+
app.name,
|
|
2225
|
+
app.applicationId
|
|
2226
|
+
);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
succeedSpinner("Deployment started!");
|
|
2230
|
+
if (isJsonMode()) {
|
|
2231
|
+
outputData({
|
|
2232
|
+
deploymentId: result.deploymentId,
|
|
2233
|
+
status: "deploying"
|
|
2234
|
+
});
|
|
2235
|
+
} else {
|
|
2236
|
+
quietOutput(result.deploymentId);
|
|
2237
|
+
log("");
|
|
2238
|
+
log(`Deployment ID: ${colors.cyan(result.deploymentId)}`);
|
|
2239
|
+
log("");
|
|
2240
|
+
log("Deployment is running in the background.");
|
|
2241
|
+
log(
|
|
2242
|
+
`Check status: ${colors.dim(`tarout deploy:status ${app.applicationId.slice(0, 8)}`)}`
|
|
2243
|
+
);
|
|
2244
|
+
log(
|
|
2245
|
+
`View logs: ${colors.dim(`tarout deploy:logs ${result.deploymentId.slice(0, 8)}`)}`
|
|
2246
|
+
);
|
|
2247
|
+
log("");
|
|
974
2248
|
}
|
|
975
2249
|
} catch (err) {
|
|
976
2250
|
handleError(err);
|
|
@@ -980,9 +2254,9 @@ function registerDeployCommands(program2) {
|
|
|
980
2254
|
try {
|
|
981
2255
|
if (!isLoggedIn()) throw new AuthError();
|
|
982
2256
|
const client2 = getApiClient();
|
|
983
|
-
const
|
|
2257
|
+
const _spinner = startSpinner("Fetching status...");
|
|
984
2258
|
const apps = await client2.application.allByOrganization.query();
|
|
985
|
-
const appSummary =
|
|
2259
|
+
const appSummary = findApp3(apps, appIdentifier);
|
|
986
2260
|
if (!appSummary) {
|
|
987
2261
|
failSpinner();
|
|
988
2262
|
const suggestions = findSimilar(
|
|
@@ -1029,9 +2303,9 @@ function registerDeployCommands(program2) {
|
|
|
1029
2303
|
try {
|
|
1030
2304
|
if (!isLoggedIn()) throw new AuthError();
|
|
1031
2305
|
const client2 = getApiClient();
|
|
1032
|
-
const
|
|
2306
|
+
const _spinner = startSpinner("Cancelling deployment...");
|
|
1033
2307
|
const apps = await client2.application.allByOrganization.query();
|
|
1034
|
-
const app =
|
|
2308
|
+
const app = findApp3(apps, appIdentifier);
|
|
1035
2309
|
if (!app) {
|
|
1036
2310
|
failSpinner();
|
|
1037
2311
|
const suggestions = findSimilar(
|
|
@@ -1055,9 +2329,9 @@ function registerDeployCommands(program2) {
|
|
|
1055
2329
|
try {
|
|
1056
2330
|
if (!isLoggedIn()) throw new AuthError();
|
|
1057
2331
|
const client2 = getApiClient();
|
|
1058
|
-
const
|
|
2332
|
+
const _spinner = startSpinner("Fetching deployments...");
|
|
1059
2333
|
const apps = await client2.application.allByOrganization.query();
|
|
1060
|
-
const appSummary =
|
|
2334
|
+
const appSummary = findApp3(apps, appIdentifier);
|
|
1061
2335
|
if (!appSummary) {
|
|
1062
2336
|
failSpinner();
|
|
1063
2337
|
const suggestions = findSimilar(
|
|
@@ -1070,7 +2344,10 @@ function registerDeployCommands(program2) {
|
|
|
1070
2344
|
applicationId: appSummary.applicationId
|
|
1071
2345
|
});
|
|
1072
2346
|
succeedSpinner();
|
|
1073
|
-
const limitedDeployments = deployments.slice(
|
|
2347
|
+
const limitedDeployments = deployments.slice(
|
|
2348
|
+
0,
|
|
2349
|
+
Number.parseInt(options.limit, 10)
|
|
2350
|
+
);
|
|
1074
2351
|
if (isJsonMode()) {
|
|
1075
2352
|
outputData(limitedDeployments);
|
|
1076
2353
|
return;
|
|
@@ -1079,7 +2356,9 @@ function registerDeployCommands(program2) {
|
|
|
1079
2356
|
log("");
|
|
1080
2357
|
log("No deployments found.");
|
|
1081
2358
|
log("");
|
|
1082
|
-
log(
|
|
2359
|
+
log(
|
|
2360
|
+
`Deploy with: ${colors.dim(`tarout deploy ${appSummary.applicationId.slice(0, 8)}`)}`
|
|
2361
|
+
);
|
|
1083
2362
|
return;
|
|
1084
2363
|
}
|
|
1085
2364
|
log("");
|
|
@@ -1089,24 +2368,277 @@ function registerDeployCommands(program2) {
|
|
|
1089
2368
|
colors.cyan(d.deploymentId.slice(0, 8)),
|
|
1090
2369
|
getStatusBadge(d.status),
|
|
1091
2370
|
d.title || colors.dim("-"),
|
|
1092
|
-
|
|
2371
|
+
formatDate3(d.createdAt)
|
|
1093
2372
|
])
|
|
1094
2373
|
);
|
|
1095
2374
|
log("");
|
|
1096
|
-
log(
|
|
2375
|
+
log(
|
|
2376
|
+
colors.dim(
|
|
2377
|
+
`${limitedDeployments.length} deployment${limitedDeployments.length === 1 ? "" : "s"}`
|
|
2378
|
+
)
|
|
2379
|
+
);
|
|
2380
|
+
} catch (err) {
|
|
2381
|
+
handleError(err);
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
program2.command("deploy:logs").argument("<deployment-id>", "Deployment ID").description("View deployment logs").option(
|
|
2385
|
+
"-f, --follow",
|
|
2386
|
+
"Stream logs in real-time (for running deployments)"
|
|
2387
|
+
).option("--no-stream", "Fetch logs via HTTP instead of WebSocket").action(async (deploymentId, options) => {
|
|
2388
|
+
try {
|
|
2389
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2390
|
+
const client2 = getApiClient();
|
|
2391
|
+
const _spinner = startSpinner("Fetching deployment...");
|
|
2392
|
+
let deployment;
|
|
2393
|
+
try {
|
|
2394
|
+
deployment = await client2.deployment.one.query({ deploymentId });
|
|
2395
|
+
} catch {
|
|
2396
|
+
failSpinner();
|
|
2397
|
+
throw new NotFoundError("Deployment", deploymentId);
|
|
2398
|
+
}
|
|
2399
|
+
succeedSpinner();
|
|
2400
|
+
const isRunning = deployment.status === "running";
|
|
2401
|
+
const shouldStream = options.follow || isRunning && options.stream !== false;
|
|
2402
|
+
if (shouldStream && deployment.logPath) {
|
|
2403
|
+
log("");
|
|
2404
|
+
log(
|
|
2405
|
+
colors.dim(
|
|
2406
|
+
`Streaming logs for deployment ${colors.cyan(deployment.deploymentId.slice(0, 8))}...`
|
|
2407
|
+
)
|
|
2408
|
+
);
|
|
2409
|
+
log(colors.dim("Press Ctrl+C to stop"));
|
|
2410
|
+
log("");
|
|
2411
|
+
const logLines = [];
|
|
2412
|
+
const errors = [];
|
|
2413
|
+
let finalStatus = deployment.status;
|
|
2414
|
+
const { cleanup, done } = streamDeploymentLogs(deployment.logPath, {
|
|
2415
|
+
onData: (line) => {
|
|
2416
|
+
logLines.push(line);
|
|
2417
|
+
if (isErrorLine(line)) {
|
|
2418
|
+
errors.push(line);
|
|
2419
|
+
}
|
|
2420
|
+
if (!isJsonMode()) {
|
|
2421
|
+
printLogLine(line);
|
|
2422
|
+
}
|
|
2423
|
+
},
|
|
2424
|
+
onError: (error2) => {
|
|
2425
|
+
if (!isJsonMode()) {
|
|
2426
|
+
log(colors.error(`WebSocket error: ${error2.message}`));
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
process.on("SIGINT", () => {
|
|
2431
|
+
cleanup();
|
|
2432
|
+
if (!isJsonMode()) {
|
|
2433
|
+
log("");
|
|
2434
|
+
log(colors.dim("Log streaming stopped"));
|
|
2435
|
+
}
|
|
2436
|
+
process.exit(0);
|
|
2437
|
+
});
|
|
2438
|
+
await done;
|
|
2439
|
+
const finalDeployment = await client2.deployment.one.query({
|
|
2440
|
+
deploymentId
|
|
2441
|
+
});
|
|
2442
|
+
finalStatus = finalDeployment.status;
|
|
2443
|
+
if (isJsonMode()) {
|
|
2444
|
+
const analysis = finalStatus === "error" ? analyzeDeploymentError(logLines, finalDeployment.errorMessage) : void 0;
|
|
2445
|
+
outputData({
|
|
2446
|
+
deploymentId: deployment.deploymentId,
|
|
2447
|
+
status: finalStatus,
|
|
2448
|
+
logs: logLines,
|
|
2449
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
2450
|
+
errorAnalysis: analysis,
|
|
2451
|
+
application: deployment.application
|
|
2452
|
+
});
|
|
2453
|
+
} else {
|
|
2454
|
+
log("");
|
|
2455
|
+
log(
|
|
2456
|
+
`Final status: ${getStatusBadge(finalStatus)} ${finalStatus === "error" && finalDeployment.errorMessage ? `- ${finalDeployment.errorMessage}` : ""}`
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
} else {
|
|
2460
|
+
const logsResult = await client2.deployment.getDeploymentLogs.query({
|
|
2461
|
+
deploymentId,
|
|
2462
|
+
offset: 0,
|
|
2463
|
+
limit: 5e3
|
|
2464
|
+
});
|
|
2465
|
+
if (isJsonMode()) {
|
|
2466
|
+
const errors = logsResult.lines.filter(isErrorLine);
|
|
2467
|
+
const analysis = deployment.status === "error" ? analyzeDeploymentError(
|
|
2468
|
+
logsResult.lines,
|
|
2469
|
+
deployment.errorMessage
|
|
2470
|
+
) : void 0;
|
|
2471
|
+
outputData({
|
|
2472
|
+
deploymentId: deployment.deploymentId,
|
|
2473
|
+
status: deployment.status,
|
|
2474
|
+
logs: logsResult.lines,
|
|
2475
|
+
totalLines: logsResult.totalLines,
|
|
2476
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
2477
|
+
errorAnalysis: analysis,
|
|
2478
|
+
application: deployment.application
|
|
2479
|
+
});
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
if (logsResult.lines.length === 0) {
|
|
2483
|
+
log("");
|
|
2484
|
+
log("No logs available for this deployment.");
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
log("");
|
|
2488
|
+
log(
|
|
2489
|
+
colors.dim(
|
|
2490
|
+
`Logs for deployment ${colors.cyan(deployment.deploymentId.slice(0, 8))} (${logsResult.totalLines} lines):`
|
|
2491
|
+
)
|
|
2492
|
+
);
|
|
2493
|
+
log("");
|
|
2494
|
+
for (const line of logsResult.lines) {
|
|
2495
|
+
printLogLine(line);
|
|
2496
|
+
}
|
|
2497
|
+
if (logsResult.hasMore) {
|
|
2498
|
+
log("");
|
|
2499
|
+
log(
|
|
2500
|
+
colors.dim(
|
|
2501
|
+
`Showing ${logsResult.lines.length} of ${logsResult.totalLines} lines`
|
|
2502
|
+
)
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
log("");
|
|
2506
|
+
log(`Status: ${getStatusBadge(deployment.status)}`);
|
|
2507
|
+
}
|
|
2508
|
+
} catch (err) {
|
|
2509
|
+
handleError(err);
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
2512
|
+
program2.command("deploy:rollback").argument("<app>", "Application ID or name").description("Rollback to a previous deployment").option("--to <deployment-id>", "Specific deployment ID to rollback to").option("--previous", "Rollback to the immediately previous deployment").option("-w, --wait", "Wait for rollback to complete").action(async (appIdentifier, options) => {
|
|
2513
|
+
try {
|
|
2514
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
2515
|
+
const client2 = getApiClient();
|
|
2516
|
+
const _spinner = startSpinner("Finding application...");
|
|
2517
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2518
|
+
const appSummary = findApp3(apps, appIdentifier);
|
|
2519
|
+
if (!appSummary) {
|
|
2520
|
+
failSpinner();
|
|
2521
|
+
const suggestions = findSimilar(
|
|
2522
|
+
appIdentifier,
|
|
2523
|
+
apps.map((a) => a.name)
|
|
2524
|
+
);
|
|
2525
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2526
|
+
}
|
|
2527
|
+
const deployments = await client2.deployment.all.query({
|
|
2528
|
+
applicationId: appSummary.applicationId
|
|
2529
|
+
});
|
|
2530
|
+
succeedSpinner();
|
|
2531
|
+
const successfulDeployments = deployments.filter(
|
|
2532
|
+
(d) => d.status === "done"
|
|
2533
|
+
);
|
|
2534
|
+
if (successfulDeployments.length === 0) {
|
|
2535
|
+
log("");
|
|
2536
|
+
log("No successful deployments found to rollback to.");
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
let targetDeploymentId;
|
|
2540
|
+
if (options.to) {
|
|
2541
|
+
const targetDeployment = successfulDeployments.find(
|
|
2542
|
+
(d) => d.deploymentId === options.to || d.deploymentId.startsWith(options.to)
|
|
2543
|
+
);
|
|
2544
|
+
if (!targetDeployment) {
|
|
2545
|
+
throw new NotFoundError("Deployment", options.to);
|
|
2546
|
+
}
|
|
2547
|
+
targetDeploymentId = targetDeployment.deploymentId;
|
|
2548
|
+
} else if (options.previous) {
|
|
2549
|
+
if (successfulDeployments.length < 2) {
|
|
2550
|
+
log("");
|
|
2551
|
+
log("No previous deployment to rollback to.");
|
|
2552
|
+
log("There must be at least 2 successful deployments.");
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
targetDeploymentId = successfulDeployments[1].deploymentId;
|
|
2556
|
+
} else {
|
|
2557
|
+
log("");
|
|
2558
|
+
log(
|
|
2559
|
+
`Select a deployment to rollback to for ${colors.cyan(appSummary.name)}:`
|
|
2560
|
+
);
|
|
2561
|
+
log("");
|
|
2562
|
+
const choices = successfulDeployments.slice(0, 10).map((d, index) => ({
|
|
2563
|
+
name: `${colors.cyan(d.deploymentId.slice(0, 8))} - ${d.title || "Deployment"} (${formatDate3(d.createdAt)})${index === 0 ? colors.dim(" [current]") : ""}`,
|
|
2564
|
+
value: d.deploymentId
|
|
2565
|
+
}));
|
|
2566
|
+
const { select: select2 } = await import("./prompts-B53LIJLG.js");
|
|
2567
|
+
targetDeploymentId = await select2("Select deployment:", choices);
|
|
2568
|
+
}
|
|
2569
|
+
if (!options.yes) {
|
|
2570
|
+
const targetDeployment = successfulDeployments.find(
|
|
2571
|
+
(d) => d.deploymentId === targetDeploymentId
|
|
2572
|
+
);
|
|
2573
|
+
log("");
|
|
2574
|
+
log(`Rolling back ${colors.cyan(appSummary.name)} to:`);
|
|
2575
|
+
log(` Deployment: ${colors.cyan(targetDeploymentId.slice(0, 8))}`);
|
|
2576
|
+
log(` Title: ${targetDeployment?.title || "Deployment"}`);
|
|
2577
|
+
log(` Created: ${formatDate3(targetDeployment?.createdAt)}`);
|
|
2578
|
+
log("");
|
|
2579
|
+
const { confirm: confirm2 } = await import("./prompts-B53LIJLG.js");
|
|
2580
|
+
const confirmed = await confirm2(
|
|
2581
|
+
"Are you sure you want to rollback?",
|
|
2582
|
+
false
|
|
2583
|
+
);
|
|
2584
|
+
if (!confirmed) {
|
|
2585
|
+
log("Cancelled.");
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
const _rollbackSpinner = startSpinner("Initiating rollback...");
|
|
2590
|
+
const result = await client2.deployment.rollback.mutate({
|
|
2591
|
+
applicationId: appSummary.applicationId,
|
|
2592
|
+
targetDeploymentId
|
|
2593
|
+
});
|
|
2594
|
+
if (options.wait) {
|
|
2595
|
+
await streamDeploymentWithLogs(
|
|
2596
|
+
client2,
|
|
2597
|
+
result.deploymentId,
|
|
2598
|
+
appSummary.name,
|
|
2599
|
+
appSummary.applicationId
|
|
2600
|
+
);
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
succeedSpinner("Rollback initiated!");
|
|
2604
|
+
if (isJsonMode()) {
|
|
2605
|
+
outputData({
|
|
2606
|
+
deploymentId: result.deploymentId,
|
|
2607
|
+
rollbackFrom: targetDeploymentId,
|
|
2608
|
+
status: "running"
|
|
2609
|
+
});
|
|
2610
|
+
} else {
|
|
2611
|
+
quietOutput(result.deploymentId);
|
|
2612
|
+
log("");
|
|
2613
|
+
log(`Deployment ID: ${colors.cyan(result.deploymentId)}`);
|
|
2614
|
+
log(`Rolling back to: ${colors.dim(targetDeploymentId.slice(0, 8))}`);
|
|
2615
|
+
log("");
|
|
2616
|
+
log("Rollback is running in the background.");
|
|
2617
|
+
log(
|
|
2618
|
+
`Check status: ${colors.dim(`tarout deploy:status ${appSummary.applicationId.slice(0, 8)}`)}`
|
|
2619
|
+
);
|
|
2620
|
+
log(
|
|
2621
|
+
`View logs: ${colors.dim(`tarout deploy:logs ${result.deploymentId.slice(0, 8)}`)}`
|
|
2622
|
+
);
|
|
2623
|
+
log("");
|
|
2624
|
+
}
|
|
1097
2625
|
} catch (err) {
|
|
1098
2626
|
handleError(err);
|
|
1099
2627
|
}
|
|
1100
2628
|
});
|
|
1101
2629
|
}
|
|
1102
2630
|
function registerLogsCommand(program2) {
|
|
1103
|
-
program2.command("logs").argument("<app>", "Application ID or name").description("View application logs").option(
|
|
2631
|
+
program2.command("logs").argument("<app>", "Application ID or name").description("View application logs").option(
|
|
2632
|
+
"-l, --level <level>",
|
|
2633
|
+
"Log level (ALL, ERROR, WARN, INFO, DEBUG)",
|
|
2634
|
+
"ALL"
|
|
2635
|
+
).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) => {
|
|
1104
2636
|
try {
|
|
1105
2637
|
if (!isLoggedIn()) throw new AuthError();
|
|
1106
2638
|
const client2 = getApiClient();
|
|
1107
|
-
const
|
|
2639
|
+
const _spinner = startSpinner("Fetching logs...");
|
|
1108
2640
|
const apps = await client2.application.allByOrganization.query();
|
|
1109
|
-
const app =
|
|
2641
|
+
const app = findApp3(apps, appIdentifier);
|
|
1110
2642
|
if (!app) {
|
|
1111
2643
|
failSpinner();
|
|
1112
2644
|
const suggestions = findSimilar(
|
|
@@ -1128,7 +2660,7 @@ function registerLogsCommand(program2) {
|
|
|
1128
2660
|
const logs = await client2.logs.getCloudRunLogs.query({
|
|
1129
2661
|
applicationId: app.applicationId,
|
|
1130
2662
|
level: options.level.toUpperCase(),
|
|
1131
|
-
limit: parseInt(options.limit, 10),
|
|
2663
|
+
limit: Number.parseInt(options.limit, 10),
|
|
1132
2664
|
timeRange
|
|
1133
2665
|
});
|
|
1134
2666
|
succeedSpinner();
|
|
@@ -1149,7 +2681,9 @@ function registerLogsCommand(program2) {
|
|
|
1149
2681
|
log("");
|
|
1150
2682
|
log(colors.dim("Streaming logs... (Ctrl+C to stop)"));
|
|
1151
2683
|
log("");
|
|
1152
|
-
let lastTimestamp = logs.entries.length > 0 ? new Date(
|
|
2684
|
+
let lastTimestamp = logs.entries.length > 0 ? new Date(
|
|
2685
|
+
logs.entries[logs.entries.length - 1].timestamp
|
|
2686
|
+
).getTime() : Date.now();
|
|
1153
2687
|
while (true) {
|
|
1154
2688
|
await sleep(2e3);
|
|
1155
2689
|
const newLogs = await client2.logs.getCloudRunLogs.query({
|
|
@@ -1177,13 +2711,13 @@ function registerLogsCommand(program2) {
|
|
|
1177
2711
|
}
|
|
1178
2712
|
});
|
|
1179
2713
|
}
|
|
1180
|
-
function
|
|
2714
|
+
function findApp3(apps, identifier) {
|
|
1181
2715
|
const lowerIdentifier = identifier.toLowerCase();
|
|
1182
2716
|
return apps.find(
|
|
1183
2717
|
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
1184
2718
|
);
|
|
1185
2719
|
}
|
|
1186
|
-
function
|
|
2720
|
+
function formatDate3(date) {
|
|
1187
2721
|
const d = new Date(date);
|
|
1188
2722
|
return d.toLocaleString("en-US", {
|
|
1189
2723
|
month: "short",
|
|
@@ -1198,7 +2732,7 @@ function sleep(ms) {
|
|
|
1198
2732
|
function parseDuration(duration) {
|
|
1199
2733
|
const match = duration.match(/^(\d+)(s|m|h|d)$/);
|
|
1200
2734
|
if (!match) return null;
|
|
1201
|
-
const value = parseInt(match[1], 10);
|
|
2735
|
+
const value = Number.parseInt(match[1], 10);
|
|
1202
2736
|
const unit = match[2];
|
|
1203
2737
|
const multipliers = {
|
|
1204
2738
|
s: 1e3,
|
|
@@ -1221,425 +2755,469 @@ function printLogEntry(entry) {
|
|
|
1221
2755
|
WARN: colors.warn,
|
|
1222
2756
|
INFO: colors.info,
|
|
1223
2757
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
variables.map((v) => [
|
|
1271
|
-
colors.cyan(v.key),
|
|
1272
|
-
options.reveal ? v.value || colors.dim("-") : maskValue(v.value),
|
|
1273
|
-
v.isSecret ? colors.warn("Yes") : "No",
|
|
1274
|
-
formatDate3(v.updatedAt)
|
|
1275
|
-
])
|
|
1276
|
-
);
|
|
1277
|
-
log("");
|
|
1278
|
-
log(colors.dim(`${variables.length} variable${variables.length === 1 ? "" : "s"}`));
|
|
1279
|
-
} catch (err) {
|
|
1280
|
-
handleError(err);
|
|
1281
|
-
}
|
|
1282
|
-
});
|
|
1283
|
-
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) => {
|
|
1284
|
-
try {
|
|
1285
|
-
if (!isLoggedIn()) throw new AuthError();
|
|
1286
|
-
const appIdentifier = command.parent.parent.args[0];
|
|
1287
|
-
const eqIndex = keyValue.indexOf("=");
|
|
1288
|
-
if (eqIndex === -1) {
|
|
1289
|
-
throw new InvalidArgumentError(
|
|
1290
|
-
"Invalid format. Use KEY=value (e.g., API_KEY=secret123)"
|
|
1291
|
-
);
|
|
1292
|
-
}
|
|
1293
|
-
const key = keyValue.slice(0, eqIndex);
|
|
1294
|
-
const value = keyValue.slice(eqIndex + 1);
|
|
1295
|
-
if (!key) {
|
|
1296
|
-
throw new InvalidArgumentError("Key cannot be empty");
|
|
1297
|
-
}
|
|
1298
|
-
const client2 = getApiClient();
|
|
1299
|
-
const spinner = startSpinner("Setting environment variable...");
|
|
1300
|
-
const apps = await client2.application.allByOrganization.query();
|
|
1301
|
-
const app = findApp3(apps, appIdentifier);
|
|
1302
|
-
if (!app) {
|
|
1303
|
-
failSpinner();
|
|
1304
|
-
const suggestions = findSimilar(
|
|
1305
|
-
appIdentifier,
|
|
1306
|
-
apps.map((a) => a.name)
|
|
1307
|
-
);
|
|
1308
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1309
|
-
}
|
|
1310
|
-
const existing = await client2.envVariable.list.query({
|
|
1311
|
-
applicationId: app.applicationId,
|
|
1312
|
-
includeValues: false
|
|
1313
|
-
});
|
|
1314
|
-
const existingVar = existing.find((v) => v.key === key);
|
|
1315
|
-
if (existingVar) {
|
|
1316
|
-
await client2.envVariable.update.mutate({
|
|
1317
|
-
applicationId: app.applicationId,
|
|
1318
|
-
key,
|
|
1319
|
-
value,
|
|
1320
|
-
isSecret: options.secret
|
|
1321
|
-
});
|
|
1322
|
-
} else {
|
|
1323
|
-
await client2.envVariable.create.mutate({
|
|
1324
|
-
applicationId: app.applicationId,
|
|
1325
|
-
key,
|
|
1326
|
-
value,
|
|
1327
|
-
isSecret: options.secret
|
|
1328
|
-
});
|
|
2758
|
+
DEFAULT: colors.dim
|
|
2759
|
+
};
|
|
2760
|
+
const colorFn = levelColors[entry.severity] || levelColors.DEFAULT;
|
|
2761
|
+
const level = entry.severity.padEnd(5);
|
|
2762
|
+
console.log(`${colors.dim(timeStr)} ${colorFn(level)} ${entry.message}`);
|
|
2763
|
+
}
|
|
2764
|
+
async function streamDeploymentWithLogs(client2, deploymentId, appName, applicationId) {
|
|
2765
|
+
stopSpinner();
|
|
2766
|
+
let deployment;
|
|
2767
|
+
try {
|
|
2768
|
+
deployment = await client2.deployment.one.query({ deploymentId });
|
|
2769
|
+
} catch {
|
|
2770
|
+
throw new NotFoundError("Deployment", deploymentId);
|
|
2771
|
+
}
|
|
2772
|
+
const logLines = [];
|
|
2773
|
+
const errors = [];
|
|
2774
|
+
const startTime = Date.now();
|
|
2775
|
+
let wsConnected = false;
|
|
2776
|
+
let lastStatus = deployment.status;
|
|
2777
|
+
if (!isJsonMode()) {
|
|
2778
|
+
log("");
|
|
2779
|
+
log(
|
|
2780
|
+
`${colors.bold(appName)} - Deployment ${colors.cyan(deploymentId.slice(0, 8))}`
|
|
2781
|
+
);
|
|
2782
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2783
|
+
log("");
|
|
2784
|
+
}
|
|
2785
|
+
let cleanup = null;
|
|
2786
|
+
if (deployment.logPath) {
|
|
2787
|
+
const stream = streamDeploymentLogs(deployment.logPath, {
|
|
2788
|
+
onData: (line) => {
|
|
2789
|
+
logLines.push(line);
|
|
2790
|
+
if (isErrorLine(line)) {
|
|
2791
|
+
errors.push(line);
|
|
2792
|
+
}
|
|
2793
|
+
if (!isJsonMode()) {
|
|
2794
|
+
printLogLine(line);
|
|
2795
|
+
}
|
|
2796
|
+
},
|
|
2797
|
+
onOpen: () => {
|
|
2798
|
+
wsConnected = true;
|
|
2799
|
+
},
|
|
2800
|
+
onError: () => {
|
|
2801
|
+
if (!isJsonMode() && wsConnected) {
|
|
2802
|
+
log(colors.dim("[WebSocket reconnecting...]"));
|
|
2803
|
+
}
|
|
1329
2804
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
2805
|
+
});
|
|
2806
|
+
cleanup = stream.cleanup;
|
|
2807
|
+
}
|
|
2808
|
+
const maxWaitMs = 6e5;
|
|
2809
|
+
const pollIntervalMs = 3e3;
|
|
2810
|
+
try {
|
|
2811
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
2812
|
+
await sleep(pollIntervalMs);
|
|
2813
|
+
const updatedDeployment = await client2.deployment.one.query({
|
|
2814
|
+
deploymentId
|
|
2815
|
+
});
|
|
2816
|
+
lastStatus = updatedDeployment.status;
|
|
2817
|
+
if (lastStatus === "done") {
|
|
2818
|
+
await sleep(500);
|
|
2819
|
+
cleanup?.();
|
|
2820
|
+
const finalApp = await client2.application.one.query({ applicationId });
|
|
2821
|
+
const duration2 = Math.round((Date.now() - startTime) / 1e3);
|
|
2822
|
+
if (isJsonMode()) {
|
|
2823
|
+
outputData({
|
|
2824
|
+
success: true,
|
|
2825
|
+
data: {
|
|
2826
|
+
deploymentId,
|
|
2827
|
+
status: "done",
|
|
2828
|
+
url: finalApp.cloudServiceUrl,
|
|
2829
|
+
duration: duration2,
|
|
2830
|
+
logs: logLines
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
} else {
|
|
2834
|
+
log("");
|
|
2835
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2836
|
+
log(colors.success("\u2713 Deployment successful!"));
|
|
2837
|
+
log("");
|
|
2838
|
+
log(`URL: ${colors.cyan(finalApp.cloudServiceUrl || "Pending...")}`);
|
|
2839
|
+
log(`Duration: ${colors.dim(`${duration2}s`)}`);
|
|
2840
|
+
log("");
|
|
2841
|
+
}
|
|
2842
|
+
return;
|
|
1335
2843
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
2844
|
+
if (lastStatus === "error" || lastStatus === "cancelled") {
|
|
2845
|
+
cleanup?.();
|
|
2846
|
+
const errorAnalysis = analyzeDeploymentError(
|
|
2847
|
+
logLines,
|
|
2848
|
+
updatedDeployment.errorMessage
|
|
2849
|
+
);
|
|
2850
|
+
const duration2 = Math.round((Date.now() - startTime) / 1e3);
|
|
2851
|
+
if (isJsonMode()) {
|
|
2852
|
+
outputData({
|
|
2853
|
+
success: false,
|
|
2854
|
+
error: {
|
|
2855
|
+
code: errorAnalysis.category === "build_script" || errorAnalysis.category === "npm_install" || errorAnalysis.category === "typescript" ? "BUILD_FAILED" : "DEPLOYMENT_FAILED",
|
|
2856
|
+
message: updatedDeployment.errorMessage || "Deployment failed",
|
|
2857
|
+
deploymentId,
|
|
2858
|
+
duration: duration2,
|
|
2859
|
+
logs: logLines,
|
|
2860
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
2861
|
+
errorAnalysis
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
} else {
|
|
2865
|
+
log("");
|
|
2866
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2867
|
+
log(colors.error("\u2717 Deployment failed"));
|
|
2868
|
+
log("");
|
|
2869
|
+
if (updatedDeployment.errorMessage) {
|
|
2870
|
+
log(`Error: ${colors.error(updatedDeployment.errorMessage)}`);
|
|
2871
|
+
}
|
|
2872
|
+
if (errorAnalysis.category !== "unknown") {
|
|
2873
|
+
log("");
|
|
2874
|
+
log(colors.bold("Error Analysis:"));
|
|
2875
|
+
log(` Category: ${errorAnalysis.category}`);
|
|
2876
|
+
log(` Type: ${errorAnalysis.type}`);
|
|
2877
|
+
log("");
|
|
2878
|
+
log(colors.bold("Possible Causes:"));
|
|
2879
|
+
for (const cause of errorAnalysis.possibleCauses.slice(0, 3)) {
|
|
2880
|
+
log(` \u2022 ${cause}`);
|
|
2881
|
+
}
|
|
2882
|
+
log("");
|
|
2883
|
+
log(colors.bold("Suggested Fixes:"));
|
|
2884
|
+
for (const fix of errorAnalysis.suggestedFixes.slice(0, 3)) {
|
|
2885
|
+
log(` \u2022 ${fix}`);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
log("");
|
|
2889
|
+
}
|
|
2890
|
+
if (errorAnalysis.category === "build_script" || errorAnalysis.category === "npm_install" || errorAnalysis.category === "typescript" || errorAnalysis.category === "docker_build") {
|
|
2891
|
+
throw new BuildFailedError(
|
|
2892
|
+
updatedDeployment.errorMessage || "Build failed",
|
|
2893
|
+
deploymentId,
|
|
2894
|
+
errorAnalysis
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
throw new DeploymentFailedError(
|
|
2898
|
+
updatedDeployment.errorMessage || "Deployment failed",
|
|
2899
|
+
deploymentId,
|
|
2900
|
+
errorAnalysis
|
|
1353
2901
|
);
|
|
1354
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1355
2902
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
2903
|
+
}
|
|
2904
|
+
cleanup?.();
|
|
2905
|
+
const duration = Math.round((Date.now() - startTime) / 1e3);
|
|
2906
|
+
if (isJsonMode()) {
|
|
2907
|
+
outputData({
|
|
2908
|
+
success: false,
|
|
2909
|
+
error: {
|
|
2910
|
+
code: "DEPLOYMENT_TIMEOUT",
|
|
2911
|
+
message: "Deployment timed out",
|
|
2912
|
+
deploymentId,
|
|
2913
|
+
duration,
|
|
2914
|
+
logs: logLines,
|
|
2915
|
+
errors: errors.length > 0 ? errors : void 0
|
|
2916
|
+
}
|
|
1359
2917
|
});
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
2918
|
+
} else {
|
|
2919
|
+
log("");
|
|
2920
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
2921
|
+
log(colors.warn("\u26A0 Deployment timed out"));
|
|
2922
|
+
log("");
|
|
2923
|
+
log(
|
|
2924
|
+
`The deployment is still running. Check status with: ${colors.dim(`tarout deploy:status ${applicationId.slice(0, 8)}`)}`
|
|
2925
|
+
);
|
|
2926
|
+
log("");
|
|
1368
2927
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
2928
|
+
throw new DeploymentTimeoutError(
|
|
2929
|
+
"Deployment timed out after 10 minutes",
|
|
2930
|
+
deploymentId
|
|
2931
|
+
);
|
|
2932
|
+
} finally {
|
|
2933
|
+
cleanup?.();
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
function isErrorLine(line) {
|
|
2937
|
+
const errorPatterns = [
|
|
2938
|
+
/error/i,
|
|
2939
|
+
/ERR!/i,
|
|
2940
|
+
/failed/i,
|
|
2941
|
+
/fatal/i,
|
|
2942
|
+
/exception/i,
|
|
2943
|
+
/ENOENT/i,
|
|
2944
|
+
/EACCES/i,
|
|
2945
|
+
/EPERM/i
|
|
2946
|
+
];
|
|
2947
|
+
return errorPatterns.some((pattern) => pattern.test(line));
|
|
2948
|
+
}
|
|
2949
|
+
function printLogLine(line) {
|
|
2950
|
+
if (isErrorLine(line)) {
|
|
2951
|
+
console.log(colors.error(line));
|
|
2952
|
+
} else if (/warn/i.test(line)) {
|
|
2953
|
+
console.log(colors.warn(line));
|
|
2954
|
+
} else if (/step|stage|building|installing|deploying/i.test(line)) {
|
|
2955
|
+
console.log(colors.info(line));
|
|
2956
|
+
} else {
|
|
2957
|
+
console.log(line);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// src/commands/dev.ts
|
|
2962
|
+
function registerDevCommand(program2) {
|
|
2963
|
+
program2.command("dev").description(
|
|
2964
|
+
"Run local development server with cloud environment variables"
|
|
2965
|
+
).option("-a, --app <app>", "Application ID or name (overrides linked app)").option("-p, --port <port>", "Port to run the dev server on").option("-c, --command <command>", "Custom dev command to run").action(async (options) => {
|
|
1371
2966
|
try {
|
|
1372
2967
|
if (!isLoggedIn()) throw new AuthError();
|
|
1373
|
-
const
|
|
2968
|
+
const profile = getCurrentProfile();
|
|
2969
|
+
if (!profile) throw new AuthError();
|
|
1374
2970
|
const client2 = getApiClient();
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
2971
|
+
let applicationId;
|
|
2972
|
+
let appName;
|
|
2973
|
+
if (options.app) {
|
|
2974
|
+
const _spinner = startSpinner("Finding application...");
|
|
2975
|
+
const apps = await client2.application.allByOrganization.query();
|
|
2976
|
+
const app = findApp4(apps, options.app);
|
|
2977
|
+
if (!app) {
|
|
2978
|
+
failSpinner();
|
|
2979
|
+
const suggestions = findSimilar(
|
|
2980
|
+
options.app,
|
|
2981
|
+
apps.map((a) => a.name)
|
|
2982
|
+
);
|
|
2983
|
+
throw new NotFoundError("Application", options.app, suggestions);
|
|
2984
|
+
}
|
|
2985
|
+
applicationId = app.applicationId;
|
|
2986
|
+
appName = app.name;
|
|
1387
2987
|
succeedSpinner();
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
return;
|
|
2988
|
+
} else if (isProjectLinked()) {
|
|
2989
|
+
const config2 = getProjectConfig();
|
|
2990
|
+
if (!config2) {
|
|
2991
|
+
throw new CliError(
|
|
2992
|
+
"Project config is corrupted. Run 'tarout link' to relink."
|
|
2993
|
+
);
|
|
1395
2994
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
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 });
|
|
2995
|
+
applicationId = config2.applicationId;
|
|
2996
|
+
appName = config2.name;
|
|
1406
2997
|
} else {
|
|
1407
|
-
|
|
2998
|
+
throw new InvalidArgumentError(
|
|
2999
|
+
"No linked application. Run 'tarout link' first or use --app flag."
|
|
3000
|
+
);
|
|
1408
3001
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
try {
|
|
1415
|
-
if (!isLoggedIn()) throw new AuthError();
|
|
1416
|
-
const appIdentifier = command.parent.parent.args[0];
|
|
1417
|
-
if (!existsSync(options.input)) {
|
|
1418
|
-
throw new InvalidArgumentError(`File not found: ${options.input}`);
|
|
3002
|
+
const pkg = readPackageJson();
|
|
3003
|
+
if (!pkg) {
|
|
3004
|
+
throw new CliError(
|
|
3005
|
+
"No package.json found in current directory. Make sure you're in a Node.js project."
|
|
3006
|
+
);
|
|
1419
3007
|
}
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
3008
|
+
const pm = detectPackageManager();
|
|
3009
|
+
let devCommand = options.command;
|
|
3010
|
+
if (!devCommand) {
|
|
3011
|
+
devCommand = getDevCommand(pkg, pm);
|
|
3012
|
+
}
|
|
3013
|
+
const defaultPort = getDefaultPort(pkg);
|
|
3014
|
+
const port = options.port || defaultPort;
|
|
3015
|
+
const _envSpinner = startSpinner(
|
|
3016
|
+
`Fetching environment variables for ${appName}...`
|
|
3017
|
+
);
|
|
3018
|
+
let envVars = {};
|
|
3019
|
+
try {
|
|
3020
|
+
const variables = await client2.envVariable.list.query({
|
|
3021
|
+
applicationId,
|
|
3022
|
+
includeValues: true
|
|
3023
|
+
});
|
|
3024
|
+
envVars = envVarsToObject(variables);
|
|
3025
|
+
succeedSpinner(
|
|
3026
|
+
`Loaded ${Object.keys(envVars).length} environment variables`
|
|
3027
|
+
);
|
|
3028
|
+
} catch (err) {
|
|
1426
3029
|
failSpinner();
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
apps.map((a) => a.name)
|
|
3030
|
+
throw new CliError(
|
|
3031
|
+
`Failed to fetch environment variables: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
1430
3032
|
);
|
|
1431
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1432
3033
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
merge: !options.replace
|
|
1438
|
-
});
|
|
1439
|
-
succeedSpinner(`Imported ${result.imported} variables`);
|
|
3034
|
+
if (options.port) {
|
|
3035
|
+
envVars.PORT = String(port);
|
|
3036
|
+
}
|
|
3037
|
+
const framework = detectFramework(pkg);
|
|
1440
3038
|
if (isJsonMode()) {
|
|
1441
|
-
outputData(
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
3039
|
+
outputData({
|
|
3040
|
+
applicationId,
|
|
3041
|
+
appName,
|
|
3042
|
+
command: devCommand,
|
|
3043
|
+
port,
|
|
3044
|
+
framework: framework?.name || "Unknown",
|
|
3045
|
+
envVarCount: Object.keys(envVars).length,
|
|
3046
|
+
packageManager: pm
|
|
3047
|
+
});
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
log("");
|
|
3051
|
+
log(colors.bold(`Running dev server for ${colors.cyan(appName)}`));
|
|
3052
|
+
log("");
|
|
3053
|
+
log(` Framework: ${colors.dim(framework?.name || "Unknown")}`);
|
|
3054
|
+
log(` Package Manager: ${colors.dim(pm)}`);
|
|
3055
|
+
log(` Command: ${colors.dim(devCommand)}`);
|
|
3056
|
+
log(` Port: ${colors.dim(String(port))}`);
|
|
3057
|
+
log(
|
|
3058
|
+
` Env Variables: ${colors.dim(String(Object.keys(envVars).length))}`
|
|
3059
|
+
);
|
|
3060
|
+
log("");
|
|
3061
|
+
log(colors.dim("\u2500".repeat(50)));
|
|
3062
|
+
log("");
|
|
3063
|
+
const result = await runCommand(devCommand, envVars);
|
|
3064
|
+
if (result.signal) {
|
|
3065
|
+
log("");
|
|
3066
|
+
log(colors.dim(`Process terminated by ${result.signal}`));
|
|
3067
|
+
} else if (result.exitCode !== 0) {
|
|
3068
|
+
log("");
|
|
3069
|
+
log(colors.error(`Process exited with code ${result.exitCode}`));
|
|
3070
|
+
process.exit(result.exitCode);
|
|
1447
3071
|
}
|
|
1448
3072
|
} catch (err) {
|
|
1449
3073
|
handleError(err);
|
|
1450
3074
|
}
|
|
1451
3075
|
});
|
|
1452
3076
|
}
|
|
1453
|
-
function
|
|
3077
|
+
function findApp4(apps, identifier) {
|
|
1454
3078
|
const lowerIdentifier = identifier.toLowerCase();
|
|
1455
3079
|
return apps.find(
|
|
1456
3080
|
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
1457
3081
|
);
|
|
1458
3082
|
}
|
|
1459
|
-
function maskValue(value) {
|
|
1460
|
-
if (!value) return colors.dim("-");
|
|
1461
|
-
if (value.length <= 4) return "****";
|
|
1462
|
-
return value.slice(0, 2) + "****" + value.slice(-2);
|
|
1463
|
-
}
|
|
1464
|
-
function formatDate3(date) {
|
|
1465
|
-
const d = new Date(date);
|
|
1466
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1467
|
-
}
|
|
1468
3083
|
|
|
1469
|
-
// src/commands/
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
db.command("list").alias("ls").description("List all databases").option("-t, --type <type>", "Filter by type (postgres, mysql, redis)").action(async (options) => {
|
|
3084
|
+
// src/commands/domains.ts
|
|
3085
|
+
function registerDomainsCommands(program2) {
|
|
3086
|
+
const domains = program2.command("domains").description("Manage custom domains");
|
|
3087
|
+
domains.command("list").alias("ls").argument("[app]", "Application ID or name (optional)").description("List domains").action(async (appIdentifier) => {
|
|
1474
3088
|
try {
|
|
1475
3089
|
if (!isLoggedIn()) throw new AuthError();
|
|
1476
3090
|
const client2 = getApiClient();
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
client2.
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
databases = databases.concat(
|
|
1498
|
-
mysql.map((db2) => ({
|
|
1499
|
-
id: db2.mysqlId,
|
|
1500
|
-
name: db2.name,
|
|
1501
|
-
type: "mysql",
|
|
1502
|
-
status: db2.applicationStatus,
|
|
1503
|
-
created: db2.createdAt
|
|
1504
|
-
}))
|
|
1505
|
-
);
|
|
1506
|
-
}
|
|
1507
|
-
if (!options.type || options.type === "redis") {
|
|
1508
|
-
databases = databases.concat(
|
|
1509
|
-
redis.map((db2) => ({
|
|
1510
|
-
id: db2.redisId,
|
|
1511
|
-
name: db2.name,
|
|
1512
|
-
type: "redis",
|
|
1513
|
-
status: db2.applicationStatus,
|
|
1514
|
-
created: db2.createdAt
|
|
1515
|
-
}))
|
|
1516
|
-
);
|
|
3091
|
+
const _spinner = startSpinner("Fetching domains...");
|
|
3092
|
+
let domainsList;
|
|
3093
|
+
if (appIdentifier) {
|
|
3094
|
+
const apps = await client2.application.allByOrganization.query();
|
|
3095
|
+
const app = findApp5(apps, appIdentifier);
|
|
3096
|
+
if (!app) {
|
|
3097
|
+
failSpinner();
|
|
3098
|
+
const suggestions = findSimilar(
|
|
3099
|
+
appIdentifier,
|
|
3100
|
+
apps.map((a) => a.name)
|
|
3101
|
+
);
|
|
3102
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
3103
|
+
}
|
|
3104
|
+
domainsList = await client2.domain.byApplicationId.query({
|
|
3105
|
+
applicationId: app.applicationId
|
|
3106
|
+
});
|
|
3107
|
+
} else {
|
|
3108
|
+
domainsList = await client2.domain.all.query({
|
|
3109
|
+
includeUnlinked: true
|
|
3110
|
+
});
|
|
1517
3111
|
}
|
|
3112
|
+
succeedSpinner();
|
|
1518
3113
|
if (isJsonMode()) {
|
|
1519
|
-
outputData(
|
|
3114
|
+
outputData(domainsList);
|
|
1520
3115
|
return;
|
|
1521
3116
|
}
|
|
1522
|
-
if (
|
|
3117
|
+
if (domainsList.length === 0) {
|
|
1523
3118
|
log("");
|
|
1524
|
-
log("No
|
|
3119
|
+
log("No domains found.");
|
|
1525
3120
|
log("");
|
|
1526
|
-
log(
|
|
3121
|
+
log(
|
|
3122
|
+
`Add one with: ${colors.dim("tarout domains add <app> <domain>")}`
|
|
3123
|
+
);
|
|
1527
3124
|
return;
|
|
1528
3125
|
}
|
|
1529
3126
|
log("");
|
|
1530
3127
|
table(
|
|
1531
|
-
["
|
|
1532
|
-
|
|
1533
|
-
colors.cyan(
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
formatDate4(db2.created)
|
|
3128
|
+
["DOMAIN", "APPLICATION", "VERIFIED", "SSL"],
|
|
3129
|
+
domainsList.map((d) => [
|
|
3130
|
+
colors.cyan(d.host),
|
|
3131
|
+
d.application?.name || colors.dim("unlinked"),
|
|
3132
|
+
d.isVerified ? colors.success("Yes") : colors.warn("No"),
|
|
3133
|
+
d.certificateType || colors.dim("-")
|
|
1538
3134
|
])
|
|
1539
3135
|
);
|
|
1540
3136
|
log("");
|
|
1541
3137
|
log(
|
|
1542
|
-
colors.dim(
|
|
3138
|
+
colors.dim(
|
|
3139
|
+
`${domainsList.length} domain${domainsList.length === 1 ? "" : "s"}`
|
|
3140
|
+
)
|
|
1543
3141
|
);
|
|
1544
3142
|
} catch (err) {
|
|
1545
3143
|
handleError(err);
|
|
1546
3144
|
}
|
|
1547
3145
|
});
|
|
1548
|
-
|
|
3146
|
+
domains.command("add").argument("<app>", "Application ID or name").argument("<domain>", "Domain name (e.g., app.example.com)").description("Add a custom domain to an application").action(async (appIdentifier, domainName) => {
|
|
1549
3147
|
try {
|
|
1550
3148
|
if (!isLoggedIn()) throw new AuthError();
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
-
]);
|
|
3149
|
+
if (!isValidDomain(domainName)) {
|
|
3150
|
+
throw new InvalidArgumentError(
|
|
3151
|
+
`Invalid domain format: ${domainName}. Use format like: app.example.com`
|
|
3152
|
+
);
|
|
1564
3153
|
}
|
|
1565
|
-
const slug = generateSlug2(dbName);
|
|
1566
3154
|
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);
|
|
3155
|
+
const _spinner = startSpinner("Finding application...");
|
|
3156
|
+
const apps = await client2.application.allByOrganization.query();
|
|
3157
|
+
const app = findApp5(apps, appIdentifier);
|
|
3158
|
+
if (!app) {
|
|
3159
|
+
failSpinner();
|
|
3160
|
+
const suggestions = findSimilar(
|
|
3161
|
+
appIdentifier,
|
|
3162
|
+
apps.map((a) => a.name)
|
|
3163
|
+
);
|
|
3164
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1599
3165
|
}
|
|
1600
|
-
|
|
1601
|
-
const
|
|
3166
|
+
updateSpinner2("Adding domain...");
|
|
3167
|
+
const domain = await client2.domain.create.mutate({
|
|
3168
|
+
host: domainName,
|
|
3169
|
+
applicationId: app.applicationId
|
|
3170
|
+
});
|
|
3171
|
+
succeedSpinner("Domain added!");
|
|
1602
3172
|
if (isJsonMode()) {
|
|
1603
|
-
outputData(
|
|
3173
|
+
outputData(domain);
|
|
1604
3174
|
return;
|
|
1605
3175
|
}
|
|
1606
|
-
quietOutput(
|
|
1607
|
-
box("
|
|
1608
|
-
`
|
|
1609
|
-
`
|
|
1610
|
-
`
|
|
3176
|
+
quietOutput(domain.domainId);
|
|
3177
|
+
box("Domain Added", [
|
|
3178
|
+
`Domain: ${colors.cyan(domainName)}`,
|
|
3179
|
+
`Application: ${app.name}`,
|
|
3180
|
+
`Status: ${domain.isVerified ? colors.success("Verified") : colors.warn("Pending verification")}`
|
|
1611
3181
|
]);
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
3182
|
+
if (!domain.isVerified) {
|
|
3183
|
+
log("Next steps:");
|
|
3184
|
+
log(" 1. Add DNS record pointing to your app");
|
|
3185
|
+
log(
|
|
3186
|
+
` 2. Verify: ${colors.dim(`tarout domains verify ${domain.domainId.slice(0, 8)}`)}`
|
|
3187
|
+
);
|
|
3188
|
+
log("");
|
|
3189
|
+
}
|
|
1615
3190
|
} catch (err) {
|
|
1616
3191
|
handleError(err);
|
|
1617
3192
|
}
|
|
1618
3193
|
});
|
|
1619
|
-
|
|
3194
|
+
domains.command("remove").alias("rm").argument("<domain>", "Domain ID or hostname").description("Remove a domain").action(async (domainIdentifier) => {
|
|
1620
3195
|
try {
|
|
1621
3196
|
if (!isLoggedIn()) throw new AuthError();
|
|
1622
3197
|
const client2 = getApiClient();
|
|
1623
|
-
const
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
|
|
3198
|
+
const _spinner = startSpinner("Finding domain...");
|
|
3199
|
+
const allDomains = await client2.domain.all.query({
|
|
3200
|
+
includeUnlinked: true
|
|
3201
|
+
});
|
|
3202
|
+
const domain = findDomain(allDomains, domainIdentifier);
|
|
3203
|
+
if (!domain) {
|
|
1627
3204
|
failSpinner();
|
|
1628
3205
|
const suggestions = findSimilar(
|
|
1629
|
-
|
|
1630
|
-
|
|
3206
|
+
domainIdentifier,
|
|
3207
|
+
allDomains.map((d) => d.host)
|
|
1631
3208
|
);
|
|
1632
|
-
throw new NotFoundError("
|
|
3209
|
+
throw new NotFoundError("Domain", domainIdentifier, suggestions);
|
|
1633
3210
|
}
|
|
1634
3211
|
succeedSpinner();
|
|
1635
3212
|
if (!shouldSkipConfirmation()) {
|
|
1636
3213
|
log("");
|
|
1637
|
-
log(`
|
|
1638
|
-
|
|
1639
|
-
|
|
3214
|
+
log(`Domain: ${colors.bold(domain.host)}`);
|
|
3215
|
+
if (domain.application) {
|
|
3216
|
+
log(`Application: ${domain.application.name}`);
|
|
3217
|
+
}
|
|
1640
3218
|
log("");
|
|
1641
3219
|
const confirmed = await confirm(
|
|
1642
|
-
`Are you sure you want to
|
|
3220
|
+
`Are you sure you want to remove "${domain.host}"?`,
|
|
1643
3221
|
false
|
|
1644
3222
|
);
|
|
1645
3223
|
if (!confirmed) {
|
|
@@ -1647,345 +3225,217 @@ function registerDbCommands(program2) {
|
|
|
1647
3225
|
return;
|
|
1648
3226
|
}
|
|
1649
3227
|
}
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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;
|
|
1661
|
-
}
|
|
1662
|
-
succeedSpinner("Database deleted!");
|
|
3228
|
+
const _deleteSpinner = startSpinner("Removing domain...");
|
|
3229
|
+
await client2.domain.delete.mutate({
|
|
3230
|
+
domainId: domain.domainId
|
|
3231
|
+
});
|
|
3232
|
+
succeedSpinner("Domain removed!");
|
|
1663
3233
|
if (isJsonMode()) {
|
|
1664
|
-
outputData({ deleted: true,
|
|
3234
|
+
outputData({ deleted: true, domainId: domain.domainId });
|
|
1665
3235
|
} else {
|
|
1666
|
-
quietOutput(
|
|
3236
|
+
quietOutput(domain.domainId);
|
|
1667
3237
|
}
|
|
1668
3238
|
} catch (err) {
|
|
1669
3239
|
handleError(err);
|
|
1670
3240
|
}
|
|
1671
3241
|
});
|
|
1672
|
-
|
|
3242
|
+
domains.command("verify").argument("<domain>", "Domain ID or hostname").description("Verify domain DNS configuration").action(async (domainIdentifier) => {
|
|
1673
3243
|
try {
|
|
1674
3244
|
if (!isLoggedIn()) throw new AuthError();
|
|
1675
3245
|
const client2 = getApiClient();
|
|
1676
|
-
const
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
3246
|
+
const _spinner = startSpinner("Finding domain...");
|
|
3247
|
+
const allDomains = await client2.domain.all.query({
|
|
3248
|
+
includeUnlinked: true
|
|
3249
|
+
});
|
|
3250
|
+
const domain = findDomain(allDomains, domainIdentifier);
|
|
3251
|
+
if (!domain) {
|
|
1680
3252
|
failSpinner();
|
|
1681
3253
|
const suggestions = findSimilar(
|
|
1682
|
-
|
|
1683
|
-
|
|
3254
|
+
domainIdentifier,
|
|
3255
|
+
allDomains.map((d) => d.host)
|
|
1684
3256
|
);
|
|
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;
|
|
3257
|
+
throw new NotFoundError("Domain", domainIdentifier, suggestions);
|
|
1698
3258
|
}
|
|
3259
|
+
updateSpinner2("Verifying DNS configuration...");
|
|
3260
|
+
const result = await client2.domain.validateDomain.mutate({
|
|
3261
|
+
domainId: domain.domainId
|
|
3262
|
+
});
|
|
1699
3263
|
succeedSpinner();
|
|
1700
3264
|
if (isJsonMode()) {
|
|
1701
|
-
outputData(
|
|
3265
|
+
outputData(result);
|
|
1702
3266
|
return;
|
|
1703
3267
|
}
|
|
1704
3268
|
log("");
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
log(` ${getStatusBadge(dbDetails.applicationStatus)}`);
|
|
1710
|
-
log(` Type: ${getTypeLabel(dbSummary.type)}`);
|
|
1711
|
-
log("");
|
|
1712
|
-
log(`${colors.bold("Connection")}`);
|
|
1713
|
-
if (dbSummary.type === "redis") {
|
|
1714
|
-
if (dbDetails.cloudHost) {
|
|
1715
|
-
log(` Host: ${colors.cyan(dbDetails.cloudHost)}`);
|
|
1716
|
-
log(` Port: ${dbDetails.cloudPort || 6379}`);
|
|
1717
|
-
if (dbDetails.cloudPassword) {
|
|
1718
|
-
log(` Password: ${colors.dim("********")}`);
|
|
1719
|
-
}
|
|
1720
|
-
} else {
|
|
1721
|
-
log(` ${colors.dim("Not yet deployed")}`);
|
|
1722
|
-
}
|
|
3269
|
+
if (result.isValid) {
|
|
3270
|
+
success(`Domain ${colors.cyan(domain.host)} is verified!`);
|
|
3271
|
+
log("");
|
|
3272
|
+
log("DNS records are correctly configured.");
|
|
1723
3273
|
} else {
|
|
1724
|
-
|
|
1725
|
-
log(` Host: ${colors.cyan(dbDetails.cloudHost || "localhost")}`);
|
|
1726
|
-
log(` Port: ${dbDetails.cloudPort || (dbSummary.type === "postgres" ? 5432 : 3306)}`);
|
|
1727
|
-
log(` Database: ${dbDetails.cloudDatabaseName || dbDetails.databaseName}`);
|
|
1728
|
-
log(` Username: ${dbDetails.cloudUsername || dbDetails.databaseUser}`);
|
|
1729
|
-
log(` Password: ${colors.dim("********")}`);
|
|
1730
|
-
} else {
|
|
1731
|
-
log(` ${colors.dim("Not yet deployed")}`);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
log("");
|
|
1735
|
-
if (dbDetails.cloudHost || dbDetails.databaseName) {
|
|
1736
|
-
log(`${colors.bold("Connection String")}`);
|
|
1737
|
-
const connStr = getConnectionString(dbSummary.type, dbDetails);
|
|
1738
|
-
log(` ${colors.cyan(connStr)}`);
|
|
3274
|
+
error(`Domain ${domain.host} verification failed`);
|
|
1739
3275
|
log("");
|
|
3276
|
+
log("Please ensure DNS is configured correctly:");
|
|
3277
|
+
log("");
|
|
3278
|
+
log(` ${colors.bold("Option 1: CNAME Record")}`);
|
|
3279
|
+
log(` Name: ${domain.host}`);
|
|
3280
|
+
log(` Value: ${colors.cyan("your-app.tarout.app")}`);
|
|
3281
|
+
log("");
|
|
3282
|
+
log(` ${colors.bold("Option 2: A Record")}`);
|
|
3283
|
+
log(` Name: ${domain.host}`);
|
|
3284
|
+
log(` Value: ${colors.cyan("(your app's IP address)")}`);
|
|
3285
|
+
log("");
|
|
3286
|
+
log(colors.dim("DNS changes can take up to 48 hours to propagate."));
|
|
1740
3287
|
}
|
|
1741
|
-
log(`${colors.bold("Created")}`);
|
|
1742
|
-
log(` ${new Date(dbDetails.createdAt).toLocaleString()}`);
|
|
1743
|
-
log("");
|
|
1744
|
-
} catch (err) {
|
|
1745
|
-
handleError(err);
|
|
1746
|
-
}
|
|
1747
|
-
});
|
|
1748
|
-
db.command("connect").argument("<db>", "Database ID or name").description("Open interactive database shell").action(async (dbIdentifier) => {
|
|
1749
|
-
try {
|
|
1750
|
-
if (!isLoggedIn()) throw new AuthError();
|
|
1751
|
-
const client2 = getApiClient();
|
|
1752
|
-
const spinner = startSpinner("Connecting to database...");
|
|
1753
|
-
const allDbs = await getAllDatabases(client2);
|
|
1754
|
-
const dbSummary = findDatabase(allDbs, dbIdentifier);
|
|
1755
|
-
if (!dbSummary) {
|
|
1756
|
-
failSpinner();
|
|
1757
|
-
const suggestions = findSimilar(
|
|
1758
|
-
dbIdentifier,
|
|
1759
|
-
allDbs.map((d) => d.name)
|
|
1760
|
-
);
|
|
1761
|
-
throw new NotFoundError("Database", dbIdentifier, suggestions);
|
|
1762
|
-
}
|
|
1763
|
-
let dbDetails;
|
|
1764
|
-
switch (dbSummary.type) {
|
|
1765
|
-
case "postgres":
|
|
1766
|
-
dbDetails = await client2.postgres.one.query({ postgresId: dbSummary.id });
|
|
1767
|
-
break;
|
|
1768
|
-
case "mysql":
|
|
1769
|
-
dbDetails = await client2.mysql.one.query({ mysqlId: dbSummary.id });
|
|
1770
|
-
break;
|
|
1771
|
-
case "redis":
|
|
1772
|
-
dbDetails = await client2.redis.one.query({ redisId: dbSummary.id });
|
|
1773
|
-
break;
|
|
1774
|
-
}
|
|
1775
|
-
succeedSpinner();
|
|
1776
|
-
if (!dbDetails.cloudHost && !dbDetails.databaseName) {
|
|
1777
|
-
throw new CliError("Database not deployed yet. Deploy first.", ExitCode.GENERAL_ERROR);
|
|
1778
|
-
}
|
|
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
3288
|
log("");
|
|
1784
|
-
const child = spawn(command, args, {
|
|
1785
|
-
stdio: "inherit",
|
|
1786
|
-
env: { ...process.env, ...env }
|
|
1787
|
-
});
|
|
1788
|
-
child.on("exit", (code) => {
|
|
1789
|
-
process.exit(code || 0);
|
|
1790
|
-
});
|
|
1791
3289
|
} catch (err) {
|
|
1792
3290
|
handleError(err);
|
|
1793
3291
|
}
|
|
1794
3292
|
});
|
|
1795
3293
|
}
|
|
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) {
|
|
3294
|
+
function findApp5(apps, identifier) {
|
|
1830
3295
|
const lowerIdentifier = identifier.toLowerCase();
|
|
1831
|
-
return
|
|
1832
|
-
(
|
|
3296
|
+
return apps.find(
|
|
3297
|
+
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
1833
3298
|
);
|
|
1834
3299
|
}
|
|
1835
|
-
function
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1841
|
-
}
|
|
1842
|
-
function getTypeLabel(type) {
|
|
1843
|
-
const labels = {
|
|
1844
|
-
postgres: colors.info("PostgreSQL"),
|
|
1845
|
-
mysql: colors.warn("MySQL"),
|
|
1846
|
-
redis: colors.error("Redis")
|
|
1847
|
-
};
|
|
1848
|
-
return labels[type] || type;
|
|
3300
|
+
function findDomain(domains, identifier) {
|
|
3301
|
+
const lowerIdentifier = identifier.toLowerCase();
|
|
3302
|
+
return domains.find(
|
|
3303
|
+
(d) => d.domainId === identifier || d.domainId.startsWith(identifier) || d.host.toLowerCase() === lowerIdentifier
|
|
3304
|
+
);
|
|
1849
3305
|
}
|
|
1850
|
-
function
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
const dbName = details.cloudDatabaseName || details.databaseName || "db";
|
|
1854
|
-
switch (type) {
|
|
1855
|
-
case "postgres":
|
|
1856
|
-
const pgPort = details.cloudPort || 5432;
|
|
1857
|
-
return `postgresql://${user}:****@${host}:${pgPort}/${dbName}`;
|
|
1858
|
-
case "mysql":
|
|
1859
|
-
const myPort = details.cloudPort || 3306;
|
|
1860
|
-
return `mysql://${user}:****@${host}:${myPort}/${dbName}`;
|
|
1861
|
-
case "redis":
|
|
1862
|
-
const redisPort = details.cloudPort || 6379;
|
|
1863
|
-
return `redis://:****@${host}:${redisPort}`;
|
|
1864
|
-
default:
|
|
1865
|
-
return "";
|
|
1866
|
-
}
|
|
3306
|
+
function isValidDomain(domain) {
|
|
3307
|
+
const pattern = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
3308
|
+
return pattern.test(domain);
|
|
1867
3309
|
}
|
|
1868
|
-
function
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const dbName = details.cloudDatabaseName || details.databaseName || "db";
|
|
1873
|
-
switch (type) {
|
|
1874
|
-
case "postgres":
|
|
1875
|
-
return {
|
|
1876
|
-
command: "psql",
|
|
1877
|
-
args: [
|
|
1878
|
-
"-h",
|
|
1879
|
-
host,
|
|
1880
|
-
"-p",
|
|
1881
|
-
String(details.cloudPort || 5432),
|
|
1882
|
-
"-U",
|
|
1883
|
-
user,
|
|
1884
|
-
"-d",
|
|
1885
|
-
dbName
|
|
1886
|
-
],
|
|
1887
|
-
env: { PGPASSWORD: password }
|
|
1888
|
-
};
|
|
1889
|
-
case "mysql":
|
|
1890
|
-
return {
|
|
1891
|
-
command: "mysql",
|
|
1892
|
-
args: [
|
|
1893
|
-
"-h",
|
|
1894
|
-
host,
|
|
1895
|
-
"-P",
|
|
1896
|
-
String(details.cloudPort || 3306),
|
|
1897
|
-
"-u",
|
|
1898
|
-
user,
|
|
1899
|
-
`-p${password}`,
|
|
1900
|
-
dbName
|
|
1901
|
-
],
|
|
1902
|
-
env: {}
|
|
1903
|
-
};
|
|
1904
|
-
case "redis":
|
|
1905
|
-
return {
|
|
1906
|
-
command: "redis-cli",
|
|
1907
|
-
args: [
|
|
1908
|
-
"-h",
|
|
1909
|
-
host,
|
|
1910
|
-
"-p",
|
|
1911
|
-
String(details.cloudPort || 6379),
|
|
1912
|
-
...password ? ["-a", password] : []
|
|
1913
|
-
],
|
|
1914
|
-
env: {}
|
|
1915
|
-
};
|
|
1916
|
-
default:
|
|
1917
|
-
throw new CliError(`Unsupported database type: ${type}`, ExitCode.INVALID_ARGUMENTS);
|
|
1918
|
-
}
|
|
3310
|
+
function updateSpinner2(text) {
|
|
3311
|
+
import("./spinner-2NALE2OE.js").then(({ startSpinner: startSpinner2 }) => {
|
|
3312
|
+
startSpinner2(text);
|
|
3313
|
+
});
|
|
1919
3314
|
}
|
|
1920
3315
|
|
|
1921
|
-
// src/commands/
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
3316
|
+
// src/commands/env.ts
|
|
3317
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3318
|
+
function registerEnvCommands(program2) {
|
|
3319
|
+
const env = program2.command("env").argument("<app>", "Application ID or name").description("Manage environment variables");
|
|
3320
|
+
env.command("list").alias("ls").description("List all environment variables").option("--reveal", "Show actual values (not masked)").action(async (options, command) => {
|
|
1925
3321
|
try {
|
|
1926
3322
|
if (!isLoggedIn()) throw new AuthError();
|
|
3323
|
+
const appIdentifier = command.parent.args[0];
|
|
1927
3324
|
const client2 = getApiClient();
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
);
|
|
1939
|
-
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1940
|
-
}
|
|
1941
|
-
domainsList = await client2.domain.byApplicationId.query({
|
|
1942
|
-
applicationId: app.applicationId
|
|
1943
|
-
});
|
|
1944
|
-
} else {
|
|
1945
|
-
domainsList = await client2.domain.all.query({
|
|
1946
|
-
includeUnlinked: true
|
|
1947
|
-
});
|
|
3325
|
+
const _spinner = startSpinner("Fetching environment variables...");
|
|
3326
|
+
const apps = await client2.application.allByOrganization.query();
|
|
3327
|
+
const app = findApp6(apps, appIdentifier);
|
|
3328
|
+
if (!app) {
|
|
3329
|
+
failSpinner();
|
|
3330
|
+
const suggestions = findSimilar(
|
|
3331
|
+
appIdentifier,
|
|
3332
|
+
apps.map((a) => a.name)
|
|
3333
|
+
);
|
|
3334
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1948
3335
|
}
|
|
3336
|
+
const variables = await client2.envVariable.list.query({
|
|
3337
|
+
applicationId: app.applicationId,
|
|
3338
|
+
includeValues: options.reveal || false
|
|
3339
|
+
});
|
|
1949
3340
|
succeedSpinner();
|
|
1950
3341
|
if (isJsonMode()) {
|
|
1951
|
-
outputData(
|
|
3342
|
+
outputData(variables);
|
|
1952
3343
|
return;
|
|
1953
3344
|
}
|
|
1954
|
-
if (
|
|
3345
|
+
if (variables.length === 0) {
|
|
1955
3346
|
log("");
|
|
1956
|
-
log("No
|
|
3347
|
+
log("No environment variables found.");
|
|
1957
3348
|
log("");
|
|
1958
|
-
log(
|
|
3349
|
+
log(
|
|
3350
|
+
`Set one with: ${colors.dim(`tarout env ${app.name} set KEY=value`)}`
|
|
3351
|
+
);
|
|
1959
3352
|
return;
|
|
1960
3353
|
}
|
|
1961
3354
|
log("");
|
|
1962
3355
|
table(
|
|
1963
|
-
["
|
|
1964
|
-
|
|
1965
|
-
colors.cyan(
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
3356
|
+
["KEY", "VALUE", "SECRET", "UPDATED"],
|
|
3357
|
+
variables.map((v) => [
|
|
3358
|
+
colors.cyan(v.key),
|
|
3359
|
+
options.reveal ? v.value || colors.dim("-") : maskValue(v.value),
|
|
3360
|
+
v.isSecret ? colors.warn("Yes") : "No",
|
|
3361
|
+
formatDate4(v.updatedAt)
|
|
1969
3362
|
])
|
|
1970
3363
|
);
|
|
1971
3364
|
log("");
|
|
1972
|
-
log(
|
|
3365
|
+
log(
|
|
3366
|
+
colors.dim(
|
|
3367
|
+
`${variables.length} variable${variables.length === 1 ? "" : "s"}`
|
|
3368
|
+
)
|
|
3369
|
+
);
|
|
3370
|
+
} catch (err) {
|
|
3371
|
+
handleError(err);
|
|
3372
|
+
}
|
|
3373
|
+
});
|
|
3374
|
+
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) => {
|
|
3375
|
+
try {
|
|
3376
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
3377
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
3378
|
+
const eqIndex = keyValue.indexOf("=");
|
|
3379
|
+
if (eqIndex === -1) {
|
|
3380
|
+
throw new InvalidArgumentError(
|
|
3381
|
+
"Invalid format. Use KEY=value (e.g., API_KEY=secret123)"
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
const key = keyValue.slice(0, eqIndex);
|
|
3385
|
+
const value = keyValue.slice(eqIndex + 1);
|
|
3386
|
+
if (!key) {
|
|
3387
|
+
throw new InvalidArgumentError("Key cannot be empty");
|
|
3388
|
+
}
|
|
3389
|
+
const client2 = getApiClient();
|
|
3390
|
+
const _spinner = startSpinner("Setting environment variable...");
|
|
3391
|
+
const apps = await client2.application.allByOrganization.query();
|
|
3392
|
+
const app = findApp6(apps, appIdentifier);
|
|
3393
|
+
if (!app) {
|
|
3394
|
+
failSpinner();
|
|
3395
|
+
const suggestions = findSimilar(
|
|
3396
|
+
appIdentifier,
|
|
3397
|
+
apps.map((a) => a.name)
|
|
3398
|
+
);
|
|
3399
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
3400
|
+
}
|
|
3401
|
+
const existing = await client2.envVariable.list.query({
|
|
3402
|
+
applicationId: app.applicationId,
|
|
3403
|
+
includeValues: false
|
|
3404
|
+
});
|
|
3405
|
+
const existingVar = existing.find((v) => v.key === key);
|
|
3406
|
+
if (existingVar) {
|
|
3407
|
+
await client2.envVariable.update.mutate({
|
|
3408
|
+
applicationId: app.applicationId,
|
|
3409
|
+
key,
|
|
3410
|
+
value,
|
|
3411
|
+
isSecret: options.secret
|
|
3412
|
+
});
|
|
3413
|
+
} else {
|
|
3414
|
+
await client2.envVariable.create.mutate({
|
|
3415
|
+
applicationId: app.applicationId,
|
|
3416
|
+
key,
|
|
3417
|
+
value,
|
|
3418
|
+
isSecret: options.secret
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
succeedSpinner(`Set ${key}`);
|
|
3422
|
+
if (isJsonMode()) {
|
|
3423
|
+
outputData({ key, updated: !!existingVar });
|
|
3424
|
+
} else {
|
|
3425
|
+
quietOutput(key);
|
|
3426
|
+
}
|
|
1973
3427
|
} catch (err) {
|
|
1974
3428
|
handleError(err);
|
|
1975
3429
|
}
|
|
1976
3430
|
});
|
|
1977
|
-
|
|
3431
|
+
env.command("unset").argument("<key>", "Variable key to remove").description("Remove an environment variable").action(async (key, _options, command) => {
|
|
1978
3432
|
try {
|
|
1979
3433
|
if (!isLoggedIn()) throw new AuthError();
|
|
1980
|
-
|
|
1981
|
-
throw new InvalidArgumentError(
|
|
1982
|
-
`Invalid domain format: ${domainName}. Use format like: app.example.com`
|
|
1983
|
-
);
|
|
1984
|
-
}
|
|
3434
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
1985
3435
|
const client2 = getApiClient();
|
|
1986
|
-
const
|
|
3436
|
+
const _spinner = startSpinner("Removing environment variable...");
|
|
1987
3437
|
const apps = await client2.application.allByOrganization.query();
|
|
1988
|
-
const app =
|
|
3438
|
+
const app = findApp6(apps, appIdentifier);
|
|
1989
3439
|
if (!app) {
|
|
1990
3440
|
failSpinner();
|
|
1991
3441
|
const suggestions = findSimilar(
|
|
@@ -1994,57 +3444,40 @@ function registerDomainsCommands(program2) {
|
|
|
1994
3444
|
);
|
|
1995
3445
|
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
1996
3446
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
applicationId: app.applicationId
|
|
3447
|
+
await client2.envVariable.delete.mutate({
|
|
3448
|
+
applicationId: app.applicationId,
|
|
3449
|
+
key
|
|
2001
3450
|
});
|
|
2002
|
-
succeedSpinner(
|
|
3451
|
+
succeedSpinner(`Removed ${key}`);
|
|
2003
3452
|
if (isJsonMode()) {
|
|
2004
|
-
outputData(
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
quietOutput(domain.domainId);
|
|
2008
|
-
box("Domain Added", [
|
|
2009
|
-
`Domain: ${colors.cyan(domainName)}`,
|
|
2010
|
-
`Application: ${app.name}`,
|
|
2011
|
-
`Status: ${domain.isVerified ? colors.success("Verified") : colors.warn("Pending verification")}`
|
|
2012
|
-
]);
|
|
2013
|
-
if (!domain.isVerified) {
|
|
2014
|
-
log("Next steps:");
|
|
2015
|
-
log(` 1. Add DNS record pointing to your app`);
|
|
2016
|
-
log(` 2. Verify: ${colors.dim(`tarout domains verify ${domain.domainId.slice(0, 8)}`)}`);
|
|
2017
|
-
log("");
|
|
3453
|
+
outputData({ key, deleted: true });
|
|
3454
|
+
} else {
|
|
3455
|
+
quietOutput(key);
|
|
2018
3456
|
}
|
|
2019
3457
|
} catch (err) {
|
|
2020
3458
|
handleError(err);
|
|
2021
3459
|
}
|
|
2022
3460
|
});
|
|
2023
|
-
|
|
3461
|
+
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) => {
|
|
2024
3462
|
try {
|
|
2025
3463
|
if (!isLoggedIn()) throw new AuthError();
|
|
3464
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
2026
3465
|
const client2 = getApiClient();
|
|
2027
|
-
const
|
|
2028
|
-
const
|
|
2029
|
-
const
|
|
2030
|
-
if (!
|
|
3466
|
+
const _spinner = startSpinner("Downloading environment variables...");
|
|
3467
|
+
const apps = await client2.application.allByOrganization.query();
|
|
3468
|
+
const app = findApp6(apps, appIdentifier);
|
|
3469
|
+
if (!app) {
|
|
2031
3470
|
failSpinner();
|
|
2032
3471
|
const suggestions = findSimilar(
|
|
2033
|
-
|
|
2034
|
-
|
|
3472
|
+
appIdentifier,
|
|
3473
|
+
apps.map((a) => a.name)
|
|
2035
3474
|
);
|
|
2036
|
-
throw new NotFoundError("
|
|
3475
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2037
3476
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
log("");
|
|
2041
|
-
log(`Domain: ${colors.bold(domain.host)}`);
|
|
2042
|
-
if (domain.application) {
|
|
2043
|
-
log(`Application: ${domain.application.name}`);
|
|
2044
|
-
}
|
|
2045
|
-
log("");
|
|
3477
|
+
if (existsSync3(options.output) && !shouldSkipConfirmation()) {
|
|
3478
|
+
succeedSpinner();
|
|
2046
3479
|
const confirmed = await confirm(
|
|
2047
|
-
`
|
|
3480
|
+
`File ${options.output} already exists. Overwrite?`,
|
|
2048
3481
|
false
|
|
2049
3482
|
);
|
|
2050
3483
|
if (!confirmed) {
|
|
@@ -2052,90 +3485,323 @@ function registerDomainsCommands(program2) {
|
|
|
2052
3485
|
return;
|
|
2053
3486
|
}
|
|
2054
3487
|
}
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
|
|
3488
|
+
const result = await client2.envVariable.export.query({
|
|
3489
|
+
applicationId: app.applicationId,
|
|
3490
|
+
format: "dotenv",
|
|
3491
|
+
maskSecrets: !options.reveal
|
|
2058
3492
|
});
|
|
2059
|
-
|
|
3493
|
+
writeFileSync2(options.output, result.content);
|
|
3494
|
+
succeedSpinner(`Saved to ${options.output}`);
|
|
2060
3495
|
if (isJsonMode()) {
|
|
2061
|
-
outputData({
|
|
3496
|
+
outputData({ file: options.output, content: result.content });
|
|
2062
3497
|
} else {
|
|
2063
|
-
quietOutput(
|
|
3498
|
+
quietOutput(options.output);
|
|
2064
3499
|
}
|
|
2065
3500
|
} catch (err) {
|
|
2066
3501
|
handleError(err);
|
|
2067
3502
|
}
|
|
2068
3503
|
});
|
|
2069
|
-
|
|
3504
|
+
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) => {
|
|
2070
3505
|
try {
|
|
2071
3506
|
if (!isLoggedIn()) throw new AuthError();
|
|
3507
|
+
const appIdentifier = command.parent.parent.args[0];
|
|
3508
|
+
if (!existsSync3(options.input)) {
|
|
3509
|
+
throw new InvalidArgumentError(`File not found: ${options.input}`);
|
|
3510
|
+
}
|
|
3511
|
+
const content = readFileSync3(options.input, "utf-8");
|
|
2072
3512
|
const client2 = getApiClient();
|
|
2073
|
-
const
|
|
2074
|
-
const
|
|
2075
|
-
const
|
|
2076
|
-
if (!
|
|
3513
|
+
const _spinner = startSpinner("Uploading environment variables...");
|
|
3514
|
+
const apps = await client2.application.allByOrganization.query();
|
|
3515
|
+
const app = findApp6(apps, appIdentifier);
|
|
3516
|
+
if (!app) {
|
|
2077
3517
|
failSpinner();
|
|
2078
3518
|
const suggestions = findSimilar(
|
|
2079
|
-
|
|
2080
|
-
|
|
3519
|
+
appIdentifier,
|
|
3520
|
+
apps.map((a) => a.name)
|
|
2081
3521
|
);
|
|
2082
|
-
throw new NotFoundError("
|
|
3522
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
2083
3523
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
3524
|
+
const result = await client2.envVariable.import.mutate({
|
|
3525
|
+
applicationId: app.applicationId,
|
|
3526
|
+
content,
|
|
3527
|
+
format: "dotenv",
|
|
3528
|
+
merge: !options.replace
|
|
2087
3529
|
});
|
|
2088
|
-
succeedSpinner();
|
|
3530
|
+
succeedSpinner(`Imported ${result.imported} variables`);
|
|
2089
3531
|
if (isJsonMode()) {
|
|
2090
3532
|
outputData(result);
|
|
3533
|
+
} else {
|
|
3534
|
+
quietOutput(String(result.imported));
|
|
3535
|
+
if (result.skipped > 0) {
|
|
3536
|
+
log(colors.dim(`Skipped ${result.skipped} (already exist)`));
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
} catch (err) {
|
|
3540
|
+
handleError(err);
|
|
3541
|
+
}
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
function findApp6(apps, identifier) {
|
|
3545
|
+
const lowerIdentifier = identifier.toLowerCase();
|
|
3546
|
+
return apps.find(
|
|
3547
|
+
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
function maskValue(value) {
|
|
3551
|
+
if (!value) return colors.dim("-");
|
|
3552
|
+
if (value.length <= 4) return "****";
|
|
3553
|
+
return `${value.slice(0, 2)}****${value.slice(-2)}`;
|
|
3554
|
+
}
|
|
3555
|
+
function formatDate4(date) {
|
|
3556
|
+
const d = new Date(date);
|
|
3557
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// src/commands/link.ts
|
|
3561
|
+
import { basename } from "path";
|
|
3562
|
+
function registerLinkCommands(program2) {
|
|
3563
|
+
program2.command("link").argument(
|
|
3564
|
+
"[app]",
|
|
3565
|
+
"Application ID or name (optional, will prompt if not provided)"
|
|
3566
|
+
).description("Link local directory to a Tarout application").option("-y, --yes", "Skip confirmation prompts").action(async (appIdentifier, options) => {
|
|
3567
|
+
try {
|
|
3568
|
+
if (!isLoggedIn()) throw new AuthError();
|
|
3569
|
+
const profile = getCurrentProfile();
|
|
3570
|
+
if (!profile) throw new AuthError();
|
|
3571
|
+
const client2 = getApiClient();
|
|
3572
|
+
const cwd = process.cwd();
|
|
3573
|
+
const dirName = basename(cwd);
|
|
3574
|
+
if (isProjectLinked()) {
|
|
3575
|
+
const existingConfig = getProjectConfig();
|
|
3576
|
+
if (existingConfig && !shouldSkipConfirmation() && !options.yes) {
|
|
3577
|
+
log("");
|
|
3578
|
+
log(
|
|
3579
|
+
`This directory is already linked to ${colors.cyan(existingConfig.name)}`
|
|
3580
|
+
);
|
|
3581
|
+
log(`Application ID: ${colors.dim(existingConfig.applicationId)}`);
|
|
3582
|
+
log("");
|
|
3583
|
+
const confirmed = await confirm(
|
|
3584
|
+
"Do you want to relink to a different application?",
|
|
3585
|
+
false
|
|
3586
|
+
);
|
|
3587
|
+
if (!confirmed) {
|
|
3588
|
+
log("Cancelled.");
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
const _spinner = startSpinner("Fetching applications...");
|
|
3594
|
+
const apps = await client2.application.allByOrganization.query();
|
|
3595
|
+
succeedSpinner();
|
|
3596
|
+
if (apps.length === 0) {
|
|
3597
|
+
log("");
|
|
3598
|
+
log("No applications found in your organization.");
|
|
3599
|
+
log("");
|
|
3600
|
+
log(`Create one with: ${colors.dim("tarout apps create <name>")}`);
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
let selectedApp = null;
|
|
3604
|
+
if (appIdentifier) {
|
|
3605
|
+
selectedApp = findApp7(apps, appIdentifier) || null;
|
|
3606
|
+
if (!selectedApp) {
|
|
3607
|
+
const suggestions = findSimilar(
|
|
3608
|
+
appIdentifier,
|
|
3609
|
+
apps.map((a) => a.name)
|
|
3610
|
+
);
|
|
3611
|
+
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
3612
|
+
}
|
|
3613
|
+
} else {
|
|
3614
|
+
log("");
|
|
3615
|
+
log(`Linking ${colors.cyan(dirName)} to a Tarout application`);
|
|
3616
|
+
log("");
|
|
3617
|
+
const choices = apps.map(
|
|
3618
|
+
(app) => ({
|
|
3619
|
+
name: `${app.name} ${colors.dim(`(${app.applicationId.slice(0, 8)})`)}`,
|
|
3620
|
+
value: app.applicationId
|
|
3621
|
+
})
|
|
3622
|
+
);
|
|
3623
|
+
const selectedId = await select("Select an application:", choices);
|
|
3624
|
+
selectedApp = apps.find(
|
|
3625
|
+
(a) => a.applicationId === selectedId
|
|
3626
|
+
) || null;
|
|
3627
|
+
}
|
|
3628
|
+
if (!selectedApp) {
|
|
3629
|
+
log("No application selected.");
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
setProjectConfig({
|
|
3633
|
+
applicationId: selectedApp.applicationId,
|
|
3634
|
+
name: selectedApp.name,
|
|
3635
|
+
organizationId: profile.organizationId,
|
|
3636
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3637
|
+
});
|
|
3638
|
+
if (isJsonMode()) {
|
|
3639
|
+
outputData({
|
|
3640
|
+
linked: true,
|
|
3641
|
+
applicationId: selectedApp.applicationId,
|
|
3642
|
+
name: selectedApp.name,
|
|
3643
|
+
directory: cwd
|
|
3644
|
+
});
|
|
2091
3645
|
return;
|
|
2092
3646
|
}
|
|
3647
|
+
quietOutput(selectedApp.applicationId);
|
|
3648
|
+
box("Project Linked", [
|
|
3649
|
+
`Application: ${colors.cyan(selectedApp.name)}`,
|
|
3650
|
+
`ID: ${colors.dim(selectedApp.applicationId)}`,
|
|
3651
|
+
`Directory: ${colors.dim(cwd)}`
|
|
3652
|
+
]);
|
|
3653
|
+
log("You can now use:");
|
|
3654
|
+
log(
|
|
3655
|
+
` ${colors.dim("tarout dev")} - Run dev server with cloud env vars`
|
|
3656
|
+
);
|
|
3657
|
+
log(
|
|
3658
|
+
` ${colors.dim("tarout build")} - Build locally with cloud env vars`
|
|
3659
|
+
);
|
|
3660
|
+
log(` ${colors.dim("tarout deploy")} - Deploy to cloud`);
|
|
2093
3661
|
log("");
|
|
2094
|
-
|
|
2095
|
-
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
handleError(err);
|
|
3664
|
+
}
|
|
3665
|
+
});
|
|
3666
|
+
program2.command("unlink").description("Unlink the current directory from Tarout").action(async () => {
|
|
3667
|
+
try {
|
|
3668
|
+
const config2 = getProjectConfig();
|
|
3669
|
+
if (!config2) {
|
|
3670
|
+
log("");
|
|
3671
|
+
log("This directory is not linked to any Tarout application.");
|
|
3672
|
+
log("");
|
|
3673
|
+
log(`Link with: ${colors.dim("tarout link")}`);
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
if (!shouldSkipConfirmation()) {
|
|
3677
|
+
log("");
|
|
3678
|
+
log(`Currently linked to: ${colors.cyan(config2.name)}`);
|
|
3679
|
+
log(`Application ID: ${colors.dim(config2.applicationId)}`);
|
|
3680
|
+
log("");
|
|
3681
|
+
const confirmed = await confirm(
|
|
3682
|
+
"Are you sure you want to unlink this directory?",
|
|
3683
|
+
false
|
|
3684
|
+
);
|
|
3685
|
+
if (!confirmed) {
|
|
3686
|
+
log("Cancelled.");
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
const removed = removeProjectConfig();
|
|
3691
|
+
if (isJsonMode()) {
|
|
3692
|
+
outputData({
|
|
3693
|
+
unlinked: removed,
|
|
3694
|
+
applicationId: config2.applicationId,
|
|
3695
|
+
name: config2.name
|
|
3696
|
+
});
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
if (removed) {
|
|
3700
|
+
log("");
|
|
3701
|
+
log(colors.success(`Unlinked from ${config2.name}`));
|
|
2096
3702
|
log("");
|
|
2097
|
-
log("DNS records are correctly configured.");
|
|
2098
3703
|
} else {
|
|
2099
|
-
error(`Domain ${domain.host} verification failed`);
|
|
2100
3704
|
log("");
|
|
2101
|
-
log("
|
|
3705
|
+
log(colors.warn("Could not remove project configuration."));
|
|
2102
3706
|
log("");
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
3707
|
+
}
|
|
3708
|
+
} catch (err) {
|
|
3709
|
+
handleError(err);
|
|
3710
|
+
}
|
|
3711
|
+
});
|
|
3712
|
+
program2.command("status").description("Show link status for current directory").action(async () => {
|
|
3713
|
+
try {
|
|
3714
|
+
const config2 = getProjectConfig();
|
|
3715
|
+
if (!config2) {
|
|
3716
|
+
if (isJsonMode()) {
|
|
3717
|
+
outputData({ linked: false });
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
2106
3720
|
log("");
|
|
2107
|
-
log(
|
|
2108
|
-
log(
|
|
2109
|
-
log(`
|
|
3721
|
+
log("This directory is not linked to any Tarout application.");
|
|
3722
|
+
log("");
|
|
3723
|
+
log(`Link with: ${colors.dim("tarout link")}`);
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
if (isLoggedIn()) {
|
|
3727
|
+
const client2 = getApiClient();
|
|
3728
|
+
const _spinner = startSpinner("Fetching application status...");
|
|
3729
|
+
try {
|
|
3730
|
+
const app = await client2.application.one.query({
|
|
3731
|
+
applicationId: config2.applicationId
|
|
3732
|
+
});
|
|
3733
|
+
succeedSpinner();
|
|
3734
|
+
if (isJsonMode()) {
|
|
3735
|
+
outputData({
|
|
3736
|
+
linked: true,
|
|
3737
|
+
applicationId: config2.applicationId,
|
|
3738
|
+
name: config2.name,
|
|
3739
|
+
status: app.applicationStatus,
|
|
3740
|
+
url: app.cloudServiceUrl,
|
|
3741
|
+
linkedAt: config2.linkedAt
|
|
3742
|
+
});
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
log("");
|
|
3746
|
+
log(colors.bold("Project Status"));
|
|
3747
|
+
log("");
|
|
3748
|
+
log(`Application: ${colors.cyan(app.name)}`);
|
|
3749
|
+
log(`ID: ${colors.dim(config2.applicationId)}`);
|
|
3750
|
+
log(`Status: ${getStatusIndicator(app.applicationStatus)}`);
|
|
3751
|
+
if (app.cloudServiceUrl) {
|
|
3752
|
+
log(`URL: ${colors.cyan(app.cloudServiceUrl)}`);
|
|
3753
|
+
}
|
|
3754
|
+
log(`Linked: ${new Date(config2.linkedAt).toLocaleString()}`);
|
|
3755
|
+
log("");
|
|
3756
|
+
} catch {
|
|
3757
|
+
failSpinner();
|
|
3758
|
+
log("");
|
|
3759
|
+
log(colors.warn("Warning: Could not fetch application status."));
|
|
3760
|
+
log("The application may have been deleted.");
|
|
3761
|
+
log("");
|
|
3762
|
+
log(`Unlink with: ${colors.dim("tarout unlink")}`);
|
|
3763
|
+
log("");
|
|
3764
|
+
}
|
|
3765
|
+
} else {
|
|
3766
|
+
if (isJsonMode()) {
|
|
3767
|
+
outputData({
|
|
3768
|
+
linked: true,
|
|
3769
|
+
applicationId: config2.applicationId,
|
|
3770
|
+
name: config2.name,
|
|
3771
|
+
linkedAt: config2.linkedAt
|
|
3772
|
+
});
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
3775
|
+
log("");
|
|
3776
|
+
log(colors.bold("Project Status"));
|
|
3777
|
+
log("");
|
|
3778
|
+
log(`Application: ${colors.cyan(config2.name)}`);
|
|
3779
|
+
log(`ID: ${colors.dim(config2.applicationId)}`);
|
|
3780
|
+
log(`Linked: ${new Date(config2.linkedAt).toLocaleString()}`);
|
|
3781
|
+
log("");
|
|
3782
|
+
log(colors.dim("Log in to see full application status."));
|
|
2110
3783
|
log("");
|
|
2111
|
-
log(colors.dim("DNS changes can take up to 48 hours to propagate."));
|
|
2112
3784
|
}
|
|
2113
|
-
log("");
|
|
2114
3785
|
} catch (err) {
|
|
2115
3786
|
handleError(err);
|
|
2116
3787
|
}
|
|
2117
3788
|
});
|
|
2118
3789
|
}
|
|
2119
|
-
function
|
|
3790
|
+
function findApp7(apps, identifier) {
|
|
2120
3791
|
const lowerIdentifier = identifier.toLowerCase();
|
|
2121
3792
|
return apps.find(
|
|
2122
3793
|
(app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
|
|
2123
3794
|
);
|
|
2124
3795
|
}
|
|
2125
|
-
function
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
return
|
|
2134
|
-
}
|
|
2135
|
-
function updateSpinner2(text) {
|
|
2136
|
-
import("./spinner-2NALE2OE.js").then(({ startSpinner: startSpinner2 }) => {
|
|
2137
|
-
startSpinner2(text);
|
|
2138
|
-
});
|
|
3796
|
+
function getStatusIndicator(status) {
|
|
3797
|
+
const indicators = {
|
|
3798
|
+
running: colors.success("running"),
|
|
3799
|
+
idle: colors.warn("idle"),
|
|
3800
|
+
error: colors.error("error"),
|
|
3801
|
+
deploying: colors.info("deploying"),
|
|
3802
|
+
stopped: colors.dim("stopped")
|
|
3803
|
+
};
|
|
3804
|
+
return indicators[status.toLowerCase()] || status;
|
|
2139
3805
|
}
|
|
2140
3806
|
|
|
2141
3807
|
// src/commands/orgs.ts
|
|
@@ -2146,7 +3812,7 @@ function registerOrgsCommands(program2) {
|
|
|
2146
3812
|
if (!isLoggedIn()) throw new AuthError();
|
|
2147
3813
|
const client2 = getApiClient();
|
|
2148
3814
|
const profile = getCurrentProfile();
|
|
2149
|
-
const
|
|
3815
|
+
const _spinner = startSpinner("Fetching organizations...");
|
|
2150
3816
|
const organizations = await client2.organization.all.query();
|
|
2151
3817
|
succeedSpinner();
|
|
2152
3818
|
if (isJsonMode()) {
|
|
@@ -2168,7 +3834,11 @@ function registerOrgsCommands(program2) {
|
|
|
2168
3834
|
])
|
|
2169
3835
|
);
|
|
2170
3836
|
log("");
|
|
2171
|
-
log(
|
|
3837
|
+
log(
|
|
3838
|
+
colors.dim(
|
|
3839
|
+
`${organizations.length} organization${organizations.length === 1 ? "" : "s"}`
|
|
3840
|
+
)
|
|
3841
|
+
);
|
|
2172
3842
|
log("");
|
|
2173
3843
|
log(`Current: ${colors.bold(profile?.organizationName || "None")}`);
|
|
2174
3844
|
} catch (err) {
|
|
@@ -2179,7 +3849,7 @@ function registerOrgsCommands(program2) {
|
|
|
2179
3849
|
try {
|
|
2180
3850
|
if (!isLoggedIn()) throw new AuthError();
|
|
2181
3851
|
const client2 = getApiClient();
|
|
2182
|
-
const
|
|
3852
|
+
const _spinner = startSpinner("Switching organization...");
|
|
2183
3853
|
const organizations = await client2.organization.all.query();
|
|
2184
3854
|
const org = findOrg(organizations, orgIdentifier);
|
|
2185
3855
|
if (!org) {
|
|
@@ -2225,7 +3895,7 @@ function registerEnvsCommands(program2) {
|
|
|
2225
3895
|
if (!isLoggedIn()) throw new AuthError();
|
|
2226
3896
|
const client2 = getApiClient();
|
|
2227
3897
|
const profile = getCurrentProfile();
|
|
2228
|
-
const
|
|
3898
|
+
const _spinner = startSpinner("Fetching environments...");
|
|
2229
3899
|
const environments = await client2.environment.all.query();
|
|
2230
3900
|
succeedSpinner();
|
|
2231
3901
|
if (isJsonMode()) {
|
|
@@ -2256,7 +3926,7 @@ function registerEnvsCommands(program2) {
|
|
|
2256
3926
|
try {
|
|
2257
3927
|
if (!isLoggedIn()) throw new AuthError();
|
|
2258
3928
|
const client2 = getApiClient();
|
|
2259
|
-
const
|
|
3929
|
+
const _spinner = startSpinner("Switching environment...");
|
|
2260
3930
|
const environments = await client2.environment.all.query();
|
|
2261
3931
|
const env = findEnv(environments, envIdentifier);
|
|
2262
3932
|
if (!env) {
|
|
@@ -2322,4 +3992,7 @@ registerDbCommands(program);
|
|
|
2322
3992
|
registerDomainsCommands(program);
|
|
2323
3993
|
registerOrgsCommands(program);
|
|
2324
3994
|
registerEnvsCommands(program);
|
|
3995
|
+
registerLinkCommands(program);
|
|
3996
|
+
registerDevCommand(program);
|
|
3997
|
+
registerBuildCommand(program);
|
|
2325
3998
|
program.parse();
|