@tarout/cli 0.1.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/index.js ADDED
@@ -0,0 +1,2325 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ failSpinner,
4
+ startSpinner,
5
+ succeedSpinner,
6
+ updateSpinner
7
+ } from "./chunk-GSKD67K4.js";
8
+
9
+ // src/index.ts
10
+ import { Command } from "commander";
11
+
12
+ // src/lib/output.ts
13
+ import chalk from "chalk";
14
+ import Table from "cli-table3";
15
+
16
+ // src/utils/json.ts
17
+ function jsonSuccess(data, meta) {
18
+ return {
19
+ success: true,
20
+ data,
21
+ ...meta && { meta }
22
+ };
23
+ }
24
+ function jsonError(code, message, suggestions) {
25
+ return {
26
+ success: false,
27
+ error: {
28
+ code,
29
+ message,
30
+ ...suggestions && { suggestions }
31
+ }
32
+ };
33
+ }
34
+ function outputJson(response) {
35
+ console.log(JSON.stringify(response, null, 2));
36
+ }
37
+
38
+ // src/lib/output.ts
39
+ var globalOptions = {
40
+ json: false,
41
+ quiet: false,
42
+ verbose: false,
43
+ noColor: false,
44
+ yes: false
45
+ };
46
+ function setGlobalOptions(options) {
47
+ globalOptions = { ...globalOptions, ...options };
48
+ }
49
+ function isJsonMode() {
50
+ return globalOptions.json;
51
+ }
52
+ function shouldSkipConfirmation() {
53
+ return globalOptions.yes;
54
+ }
55
+ function c(colorFn, str) {
56
+ return globalOptions.noColor ? str : colorFn(str);
57
+ }
58
+ var colors = {
59
+ success: (str) => c(chalk.green, str),
60
+ error: (str) => c(chalk.red, str),
61
+ warn: (str) => c(chalk.yellow, str),
62
+ info: (str) => c(chalk.blue, str),
63
+ dim: (str) => c(chalk.dim, str),
64
+ bold: (str) => c(chalk.bold, str),
65
+ cyan: (str) => c(chalk.cyan, str)
66
+ };
67
+ function getStatusBadge(status) {
68
+ const badges = {
69
+ running: colors.success("\u25CF running"),
70
+ idle: colors.warn("\u25CB idle"),
71
+ error: colors.error("\u2717 error"),
72
+ done: colors.success("\u2713 done"),
73
+ deploying: colors.info("\u25D0 deploying"),
74
+ stopped: colors.dim("\u25CB stopped"),
75
+ cancelled: colors.warn("\u25CB cancelled")
76
+ };
77
+ return badges[status.toLowerCase()] || status;
78
+ }
79
+ function log(message) {
80
+ if (!globalOptions.quiet && !globalOptions.json) {
81
+ console.log(message);
82
+ }
83
+ }
84
+ function success(message) {
85
+ if (!globalOptions.quiet && !globalOptions.json) {
86
+ console.log(colors.success(`\u2713 ${message}`));
87
+ }
88
+ }
89
+ function error(message, suggestions) {
90
+ if (globalOptions.json) {
91
+ outputJson(jsonError("ERROR", message, suggestions));
92
+ return;
93
+ }
94
+ console.error(colors.error(`Error: ${message}`));
95
+ if (suggestions && suggestions.length > 0) {
96
+ console.error("");
97
+ console.error("Did you mean one of these?");
98
+ for (const suggestion of suggestions) {
99
+ console.error(colors.dim(` - ${suggestion}`));
100
+ }
101
+ }
102
+ }
103
+ function table(headers, rows) {
104
+ if (globalOptions.json) {
105
+ return;
106
+ }
107
+ const t = new Table({
108
+ head: headers.map((h) => colors.bold(h)),
109
+ style: { head: [], border: [] },
110
+ chars: {
111
+ top: "",
112
+ "top-mid": "",
113
+ "top-left": "",
114
+ "top-right": "",
115
+ bottom: "",
116
+ "bottom-mid": "",
117
+ "bottom-left": "",
118
+ "bottom-right": "",
119
+ left: " ",
120
+ "left-mid": "",
121
+ mid: "",
122
+ "mid-mid": "",
123
+ right: "",
124
+ "right-mid": "",
125
+ middle: " "
126
+ }
127
+ });
128
+ for (const row of rows) {
129
+ t.push(row);
130
+ }
131
+ console.log(t.toString());
132
+ }
133
+ function outputData(data) {
134
+ if (globalOptions.json) {
135
+ outputJson(jsonSuccess(data));
136
+ }
137
+ }
138
+ function quietOutput(message) {
139
+ if (globalOptions.quiet || globalOptions.json) {
140
+ console.log(message);
141
+ }
142
+ }
143
+ function box(title, content) {
144
+ if (globalOptions.json || globalOptions.quiet) {
145
+ return;
146
+ }
147
+ console.log("");
148
+ console.log(colors.bold(title));
149
+ for (const line of content) {
150
+ console.log(` ${line}`);
151
+ }
152
+ console.log("");
153
+ }
154
+
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://app.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
+ // src/lib/errors.ts
324
+ var CliError = class extends Error {
325
+ constructor(message, code = ExitCode.GENERAL_ERROR, suggestions) {
326
+ super(message);
327
+ this.code = code;
328
+ this.suggestions = suggestions;
329
+ this.name = "CliError";
330
+ }
331
+ };
332
+ var AuthError = class extends CliError {
333
+ constructor(message = "Not logged in. Run 'tarout login' first.") {
334
+ super(message, ExitCode.AUTH_ERROR);
335
+ }
336
+ };
337
+ var NotFoundError = class extends CliError {
338
+ constructor(resource, id, suggestions) {
339
+ super(`${resource} "${id}" not found`, ExitCode.NOT_FOUND, suggestions);
340
+ }
341
+ };
342
+ var InvalidArgumentError = class extends CliError {
343
+ constructor(message) {
344
+ super(message, ExitCode.INVALID_ARGUMENTS);
345
+ }
346
+ };
347
+ function handleError(err) {
348
+ if (err instanceof CliError) {
349
+ if (isJsonMode()) {
350
+ outputJson(
351
+ jsonError(
352
+ getErrorCode(err.code),
353
+ err.message,
354
+ err.suggestions
355
+ )
356
+ );
357
+ } else {
358
+ error(err.message, err.suggestions);
359
+ }
360
+ exit(err.code);
361
+ }
362
+ if (err && typeof err === "object" && "code" in err) {
363
+ const trpcError = err;
364
+ const exitCode = mapTrpcErrorCode(trpcError.code);
365
+ if (isJsonMode()) {
366
+ outputJson(jsonError(trpcError.code, trpcError.message));
367
+ } else {
368
+ error(trpcError.message);
369
+ }
370
+ exit(exitCode);
371
+ }
372
+ const message = err instanceof Error ? err.message : String(err);
373
+ if (isJsonMode()) {
374
+ outputJson(jsonError("UNKNOWN_ERROR", message));
375
+ } else {
376
+ error(message);
377
+ }
378
+ exit(ExitCode.GENERAL_ERROR);
379
+ }
380
+ function getErrorCode(exitCode) {
381
+ const codes = {
382
+ [ExitCode.SUCCESS]: "SUCCESS",
383
+ [ExitCode.GENERAL_ERROR]: "ERROR",
384
+ [ExitCode.INVALID_ARGUMENTS]: "INVALID_ARGUMENTS",
385
+ [ExitCode.AUTH_ERROR]: "AUTH_ERROR",
386
+ [ExitCode.NOT_FOUND]: "NOT_FOUND",
387
+ [ExitCode.PERMISSION_DENIED]: "PERMISSION_DENIED"
388
+ };
389
+ return codes[exitCode] || "ERROR";
390
+ }
391
+ function mapTrpcErrorCode(code) {
392
+ const mapping = {
393
+ UNAUTHORIZED: ExitCode.AUTH_ERROR,
394
+ FORBIDDEN: ExitCode.PERMISSION_DENIED,
395
+ NOT_FOUND: ExitCode.NOT_FOUND,
396
+ BAD_REQUEST: ExitCode.INVALID_ARGUMENTS,
397
+ PARSE_ERROR: ExitCode.INVALID_ARGUMENTS
398
+ };
399
+ return mapping[code] || ExitCode.GENERAL_ERROR;
400
+ }
401
+ function findSimilar(target, candidates, maxResults = 3) {
402
+ const targetLower = target.toLowerCase();
403
+ return candidates.map((candidate) => ({
404
+ candidate,
405
+ score: similarity(targetLower, candidate.toLowerCase())
406
+ })).filter(({ score }) => score > 0.3).sort((a, b) => b.score - a.score).slice(0, maxResults).map(({ candidate }) => candidate);
407
+ }
408
+ function similarity(s1, s2) {
409
+ if (s1 === s2) return 1;
410
+ if (s1.length < 2 || s2.length < 2) return 0;
411
+ const bigrams1 = /* @__PURE__ */ new Set();
412
+ for (let i = 0; i < s1.length - 1; i++) {
413
+ bigrams1.add(s1.slice(i, i + 2));
414
+ }
415
+ let intersection = 0;
416
+ for (let i = 0; i < s2.length - 1; i++) {
417
+ if (bigrams1.has(s2.slice(i, i + 2))) {
418
+ intersection++;
419
+ }
420
+ }
421
+ return 2 * intersection / (s1.length - 1 + s2.length - 1);
422
+ }
423
+
424
+ // src/commands/auth.ts
425
+ function registerAuthCommands(program2) {
426
+ program2.command("login").description("Authenticate with Tarout via browser").option("--api-url <url>", "Custom API URL", "https://app.tarout.sa").action(async (options) => {
427
+ try {
428
+ if (isLoggedIn()) {
429
+ const profile = getCurrentProfile();
430
+ if (profile) {
431
+ log(`Already logged in as ${colors.cyan(profile.userEmail)}`);
432
+ log(`Organization: ${profile.organizationName}`);
433
+ log("");
434
+ log(`Run ${colors.dim("tarout logout")} to sign out first.`);
435
+ return;
436
+ }
437
+ }
438
+ const apiUrl = options.apiUrl;
439
+ log("");
440
+ log("Opening browser to authenticate...");
441
+ const authServer = await startAuthServer();
442
+ const callbackUrl = `http://localhost:${authServer.port}/callback`;
443
+ const authUrl = `${apiUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
444
+ await open(authUrl);
445
+ const spinner = startSpinner("Waiting for authentication...");
446
+ try {
447
+ const authData = await authServer.waitForCallback();
448
+ succeedSpinner("Authentication successful!");
449
+ authServer.close();
450
+ setProfile("default", {
451
+ token: authData.token,
452
+ apiUrl,
453
+ userId: authData.userId,
454
+ userEmail: authData.userEmail,
455
+ userName: authData.userName,
456
+ organizationId: authData.organizationId,
457
+ organizationName: authData.organizationName,
458
+ environmentId: authData.environmentId,
459
+ environmentName: authData.environmentName
460
+ });
461
+ setCurrentProfile("default");
462
+ if (isJsonMode()) {
463
+ outputData({
464
+ success: true,
465
+ user: {
466
+ id: authData.userId,
467
+ email: authData.userEmail,
468
+ name: authData.userName
469
+ },
470
+ organization: {
471
+ id: authData.organizationId,
472
+ name: authData.organizationName
473
+ },
474
+ environment: {
475
+ id: authData.environmentId,
476
+ name: authData.environmentName
477
+ }
478
+ });
479
+ } else {
480
+ log("");
481
+ success(`Logged in as ${colors.cyan(authData.userEmail)}`);
482
+ box("Account", [
483
+ `Organization: ${colors.bold(authData.organizationName)}`,
484
+ `Environment: ${colors.bold(authData.environmentName)}`
485
+ ]);
486
+ }
487
+ } catch (err) {
488
+ failSpinner("Authentication failed");
489
+ authServer.close();
490
+ throw err;
491
+ }
492
+ } catch (err) {
493
+ handleError(err);
494
+ }
495
+ });
496
+ program2.command("logout").description("Sign out and clear stored credentials").action(async () => {
497
+ try {
498
+ if (!isLoggedIn()) {
499
+ if (isJsonMode()) {
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);
515
+ }
516
+ });
517
+ program2.command("whoami").description("Show current authenticated user").action(async () => {
518
+ try {
519
+ if (!isLoggedIn()) {
520
+ throw new AuthError();
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);
562
+ }
563
+ });
564
+ }
565
+
566
+ // src/commands/apps.ts
567
+ import open2 from "open";
568
+
569
+ // src/lib/api.ts
570
+ import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
571
+ import superjson from "superjson";
572
+ var client = null;
573
+ function createApiClient() {
574
+ if (!isLoggedIn()) {
575
+ throw new AuthError();
576
+ }
577
+ const token = getToken();
578
+ const apiUrl = getApiUrl();
579
+ return createTRPCProxyClient({
580
+ transformer: superjson,
581
+ links: [
582
+ httpBatchLink({
583
+ url: `${apiUrl}/api/trpc`,
584
+ headers: () => ({
585
+ "x-api-key": token
586
+ })
587
+ })
588
+ ]
589
+ });
590
+ }
591
+ function getApiClient() {
592
+ if (!client) {
593
+ client = createApiClient();
594
+ }
595
+ return client;
596
+ }
597
+
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
+ // src/commands/apps.ts
635
+ function registerAppsCommands(program2) {
636
+ const apps = program2.command("apps").description("Manage applications");
637
+ apps.command("list").alias("ls").description("List all applications").action(async () => {
638
+ try {
639
+ if (!isLoggedIn()) throw new AuthError();
640
+ const client2 = getApiClient();
641
+ const spinner = startSpinner("Fetching applications...");
642
+ const applications = await client2.application.allByOrganization.query();
643
+ succeedSpinner();
644
+ if (isJsonMode()) {
645
+ outputData(applications);
646
+ return;
647
+ }
648
+ if (applications.length === 0) {
649
+ log("");
650
+ log("No applications found.");
651
+ log("");
652
+ log(`Create one with: ${colors.dim("tarout apps create <name>")}`);
653
+ return;
654
+ }
655
+ log("");
656
+ table(
657
+ ["ID", "NAME", "STATUS", "DOMAIN", "CREATED"],
658
+ applications.map((app) => [
659
+ colors.cyan(app.applicationId.slice(0, 8)),
660
+ app.name,
661
+ getStatusBadge(app.applicationStatus),
662
+ app.domain || colors.dim("-"),
663
+ formatDate(app.createdAt)
664
+ ])
665
+ );
666
+ log("");
667
+ log(colors.dim(`${applications.length} application${applications.length === 1 ? "" : "s"}`));
668
+ } catch (err) {
669
+ handleError(err);
670
+ }
671
+ });
672
+ apps.command("create").argument("[name]", "Application name").description("Create a new application").option("-d, --description <description>", "Application description").action(async (name, options) => {
673
+ try {
674
+ if (!isLoggedIn()) throw new AuthError();
675
+ const profile = getCurrentProfile();
676
+ if (!profile) throw new AuthError();
677
+ let appName = name;
678
+ let description = options.description;
679
+ if (!appName) {
680
+ appName = await input("Application name:");
681
+ }
682
+ if (!description && !shouldSkipConfirmation()) {
683
+ description = await input("Description (optional):");
684
+ }
685
+ const slug = generateSlug(appName);
686
+ const client2 = getApiClient();
687
+ const spinner = startSpinner("Creating application...");
688
+ const application = await client2.application.create.mutate({
689
+ name: appName,
690
+ appName: slug,
691
+ description: description || void 0,
692
+ organizationId: profile.organizationId
693
+ });
694
+ succeedSpinner("Application created!");
695
+ if (isJsonMode()) {
696
+ outputData(application);
697
+ return;
698
+ }
699
+ quietOutput(application.applicationId);
700
+ box("Application Created", [
701
+ `ID: ${colors.cyan(application.applicationId)}`,
702
+ `Name: ${application.name}`,
703
+ `Slug: ${application.appName}`
704
+ ]);
705
+ log("Next steps:");
706
+ log(` 1. Connect a source: ${colors.dim(`tarout apps info ${application.applicationId.slice(0, 8)}`)}`);
707
+ log(` 2. Deploy: ${colors.dim(`tarout deploy ${application.applicationId.slice(0, 8)}`)}`);
708
+ log("");
709
+ } catch (err) {
710
+ handleError(err);
711
+ }
712
+ });
713
+ apps.command("delete").alias("rm").argument("<app>", "Application ID or name").description("Delete an application").action(async (appIdentifier) => {
714
+ try {
715
+ if (!isLoggedIn()) throw new AuthError();
716
+ const client2 = getApiClient();
717
+ const spinner = startSpinner("Finding application...");
718
+ const apps2 = await client2.application.allByOrganization.query();
719
+ const app = findApp(apps2, appIdentifier);
720
+ if (!app) {
721
+ failSpinner();
722
+ const suggestions = findSimilar(
723
+ appIdentifier,
724
+ apps2.map((a) => a.name)
725
+ );
726
+ throw new NotFoundError("Application", appIdentifier, suggestions);
727
+ }
728
+ succeedSpinner();
729
+ if (!shouldSkipConfirmation()) {
730
+ log("");
731
+ log(`Application: ${colors.bold(app.name)}`);
732
+ log(`ID: ${colors.dim(app.applicationId)}`);
733
+ log("");
734
+ const confirmed = await confirm(
735
+ `Are you sure you want to delete "${app.name}"? This cannot be undone.`,
736
+ false
737
+ );
738
+ if (!confirmed) {
739
+ log("Cancelled.");
740
+ return;
741
+ }
742
+ }
743
+ const deleteSpinner = startSpinner("Deleting application...");
744
+ await client2.application.delete.mutate({
745
+ applicationId: app.applicationId
746
+ });
747
+ succeedSpinner("Application deleted!");
748
+ if (isJsonMode()) {
749
+ outputData({ deleted: true, applicationId: app.applicationId });
750
+ } else {
751
+ quietOutput(app.applicationId);
752
+ }
753
+ } catch (err) {
754
+ handleError(err);
755
+ }
756
+ });
757
+ apps.command("info").argument("<app>", "Application ID or name").description("Show application details").action(async (appIdentifier) => {
758
+ try {
759
+ if (!isLoggedIn()) throw new AuthError();
760
+ const client2 = getApiClient();
761
+ const spinner = startSpinner("Fetching application...");
762
+ const apps2 = await client2.application.allByOrganization.query();
763
+ const appSummary = findApp(apps2, appIdentifier);
764
+ if (!appSummary) {
765
+ failSpinner();
766
+ const suggestions = findSimilar(
767
+ appIdentifier,
768
+ apps2.map((a) => a.name)
769
+ );
770
+ throw new NotFoundError("Application", appIdentifier, suggestions);
771
+ }
772
+ const app = await client2.application.one.query({
773
+ applicationId: appSummary.applicationId
774
+ });
775
+ succeedSpinner();
776
+ if (isJsonMode()) {
777
+ outputData(app);
778
+ return;
779
+ }
780
+ log("");
781
+ log(colors.bold(app.name));
782
+ log(colors.dim(app.applicationId));
783
+ log("");
784
+ log(`${colors.bold("Status")}`);
785
+ log(` ${getStatusBadge(app.applicationStatus)}`);
786
+ log("");
787
+ log(`${colors.bold("Source")}`);
788
+ if (app.sourceType) {
789
+ log(` Type: ${app.sourceType}`);
790
+ if (app.github) {
791
+ log(` Repository: ${app.github.repository}`);
792
+ log(` Branch: ${app.github.branch}`);
793
+ } else if (app.gitlab) {
794
+ log(` Repository: ${app.gitlab.gitlabRepository}`);
795
+ log(` Branch: ${app.gitlab.gitlabBranch}`);
796
+ } else if (app.bitbucket) {
797
+ log(` Repository: ${app.bitbucket.bitbucketRepository}`);
798
+ log(` Branch: ${app.bitbucket.bitbucketBranch}`);
799
+ }
800
+ } else {
801
+ log(` ${colors.dim("Not configured")}`);
802
+ }
803
+ log("");
804
+ log(`${colors.bold("Build")}`);
805
+ log(` Type: ${app.buildType || colors.dim("Not configured")}`);
806
+ if (app.dockerfile) {
807
+ log(` Dockerfile: ${app.dockerfile}`);
808
+ }
809
+ log("");
810
+ log(`${colors.bold("Domain")}`);
811
+ if (app.domain && app.domain.length > 0) {
812
+ for (const domain of app.domain) {
813
+ log(` ${domain.host}`);
814
+ }
815
+ } else {
816
+ log(` ${colors.dim("No custom domain")}`);
817
+ }
818
+ log("");
819
+ if (app.cloudServiceUrl) {
820
+ log(`${colors.bold("Cloud")}`);
821
+ log(` URL: ${app.cloudServiceUrl}`);
822
+ if (app.cloudRegion) {
823
+ log(` Region: ${app.cloudRegion}`);
824
+ }
825
+ log("");
826
+ }
827
+ log(`${colors.bold("Created")}`);
828
+ log(` ${new Date(app.createdAt).toLocaleString()}`);
829
+ log("");
830
+ } catch (err) {
831
+ handleError(err);
832
+ }
833
+ });
834
+ apps.command("open").argument("<app>", "Application ID or name").description("Open application URL in browser").action(async (appIdentifier) => {
835
+ try {
836
+ if (!isLoggedIn()) throw new AuthError();
837
+ const client2 = getApiClient();
838
+ const spinner = startSpinner("Finding application...");
839
+ const apps2 = await client2.application.allByOrganization.query();
840
+ const appSummary = findApp(apps2, appIdentifier);
841
+ if (!appSummary) {
842
+ failSpinner();
843
+ const suggestions = findSimilar(
844
+ appIdentifier,
845
+ apps2.map((a) => a.name)
846
+ );
847
+ throw new NotFoundError("Application", appIdentifier, suggestions);
848
+ }
849
+ const app = await client2.application.one.query({
850
+ applicationId: appSummary.applicationId
851
+ });
852
+ succeedSpinner();
853
+ let url = null;
854
+ if (app.domain && app.domain.length > 0) {
855
+ url = `https://${app.domain[0].host}`;
856
+ } else if (app.cloudServiceUrl) {
857
+ url = app.cloudServiceUrl;
858
+ }
859
+ if (!url) {
860
+ error("No URL found for this application. Deploy first or add a domain.");
861
+ return;
862
+ }
863
+ if (isJsonMode()) {
864
+ outputData({ url });
865
+ return;
866
+ }
867
+ log(`Opening ${colors.cyan(url)}...`);
868
+ await open2(url);
869
+ } catch (err) {
870
+ handleError(err);
871
+ }
872
+ });
873
+ }
874
+ function findApp(apps, identifier) {
875
+ const lowerIdentifier = identifier.toLowerCase();
876
+ return apps.find(
877
+ (app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
878
+ );
879
+ }
880
+ function generateSlug(name) {
881
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
882
+ }
883
+ function formatDate(date) {
884
+ const d = new Date(date);
885
+ const now = /* @__PURE__ */ new Date();
886
+ const diffDays = Math.floor(
887
+ (now.getTime() - d.getTime()) / (1e3 * 60 * 60 * 24)
888
+ );
889
+ if (diffDays === 0) {
890
+ return "Today";
891
+ } else if (diffDays === 1) {
892
+ return "Yesterday";
893
+ } else if (diffDays < 7) {
894
+ return `${diffDays}d ago`;
895
+ } else {
896
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
897
+ }
898
+ }
899
+
900
+ // src/commands/deploy.ts
901
+ function registerDeployCommands(program2) {
902
+ 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").action(async (appIdentifier, options) => {
903
+ try {
904
+ if (!isLoggedIn()) throw new AuthError();
905
+ const client2 = getApiClient();
906
+ const spinner = startSpinner("Finding application...");
907
+ const apps = await client2.application.allByOrganization.query();
908
+ const app = findApp2(apps, appIdentifier);
909
+ if (!app) {
910
+ failSpinner();
911
+ const suggestions = findSimilar(
912
+ appIdentifier,
913
+ apps.map((a) => a.name)
914
+ );
915
+ throw new NotFoundError("Application", appIdentifier, suggestions);
916
+ }
917
+ updateSpinner(`Deploying ${app.name}...`);
918
+ const result = await client2.application.deployToCloud.mutate({
919
+ applicationId: app.applicationId,
920
+ region: options.region
921
+ });
922
+ if (options.wait) {
923
+ let attempts = 0;
924
+ const maxAttempts = 120;
925
+ while (attempts < maxAttempts) {
926
+ await sleep(5e3);
927
+ attempts++;
928
+ const fullApp = await client2.application.one.query({
929
+ applicationId: app.applicationId
930
+ });
931
+ updateSpinner(
932
+ `Deploying ${app.name}... (${attempts * 5}s)`
933
+ );
934
+ if (fullApp.applicationStatus === "done" || fullApp.applicationStatus === "running") {
935
+ succeedSpinner("Deployment successful!");
936
+ if (isJsonMode()) {
937
+ outputData({
938
+ deploymentId: result.deploymentId,
939
+ status: fullApp.applicationStatus,
940
+ url: fullApp.cloudServiceUrl
941
+ });
942
+ } else {
943
+ quietOutput(fullApp.cloudServiceUrl || result.deploymentId);
944
+ log("");
945
+ log(`URL: ${colors.cyan(fullApp.cloudServiceUrl || "Pending...")}`);
946
+ log("");
947
+ }
948
+ return;
949
+ }
950
+ if (fullApp.applicationStatus === "error") {
951
+ failSpinner("Deployment failed");
952
+ throw new CliError("Deployment failed. Check logs for details.", ExitCode.GENERAL_ERROR);
953
+ }
954
+ }
955
+ failSpinner("Deployment timed out");
956
+ throw new CliError("Deployment timed out after 10 minutes", ExitCode.GENERAL_ERROR);
957
+ } else {
958
+ succeedSpinner("Deployment started!");
959
+ if (isJsonMode()) {
960
+ outputData({
961
+ deploymentId: result.deploymentId,
962
+ status: "deploying"
963
+ });
964
+ } else {
965
+ quietOutput(result.deploymentId);
966
+ log("");
967
+ log(`Deployment ID: ${colors.cyan(result.deploymentId)}`);
968
+ log("");
969
+ log("Deployment is running in the background.");
970
+ log(`Check status: ${colors.dim(`tarout deploy:status ${app.applicationId.slice(0, 8)}`)}`);
971
+ log(`View logs: ${colors.dim(`tarout logs ${app.applicationId.slice(0, 8)}`)}`);
972
+ log("");
973
+ }
974
+ }
975
+ } catch (err) {
976
+ handleError(err);
977
+ }
978
+ });
979
+ program2.command("deploy:status").argument("<app>", "Application ID or name").description("Check deployment status").action(async (appIdentifier) => {
980
+ try {
981
+ if (!isLoggedIn()) throw new AuthError();
982
+ const client2 = getApiClient();
983
+ const spinner = startSpinner("Fetching status...");
984
+ const apps = await client2.application.allByOrganization.query();
985
+ const appSummary = findApp2(apps, appIdentifier);
986
+ if (!appSummary) {
987
+ failSpinner();
988
+ const suggestions = findSimilar(
989
+ appIdentifier,
990
+ apps.map((a) => a.name)
991
+ );
992
+ throw new NotFoundError("Application", appIdentifier, suggestions);
993
+ }
994
+ const app = await client2.application.one.query({
995
+ applicationId: appSummary.applicationId
996
+ });
997
+ const cloudStatus = await client2.application.getCloudDeploymentStatus.query({
998
+ applicationId: appSummary.applicationId
999
+ });
1000
+ succeedSpinner();
1001
+ if (isJsonMode()) {
1002
+ outputData({
1003
+ applicationId: app.applicationId,
1004
+ name: app.name,
1005
+ status: app.applicationStatus,
1006
+ url: app.cloudServiceUrl,
1007
+ cloudStatus
1008
+ });
1009
+ return;
1010
+ }
1011
+ log("");
1012
+ log(`${colors.bold(app.name)}`);
1013
+ log("");
1014
+ log(`Status: ${getStatusBadge(app.applicationStatus)}`);
1015
+ if (app.cloudServiceUrl) {
1016
+ log(`URL: ${colors.cyan(app.cloudServiceUrl)}`);
1017
+ }
1018
+ if (cloudStatus) {
1019
+ log(`Provider: ${cloudStatus.provider}`);
1020
+ log(`Region: ${cloudStatus.region}`);
1021
+ log(`Updated: ${new Date(cloudStatus.updatedAt).toLocaleString()}`);
1022
+ }
1023
+ log("");
1024
+ } catch (err) {
1025
+ handleError(err);
1026
+ }
1027
+ });
1028
+ program2.command("deploy:cancel").argument("<app>", "Application ID or name").description("Cancel a running deployment").action(async (appIdentifier) => {
1029
+ try {
1030
+ if (!isLoggedIn()) throw new AuthError();
1031
+ const client2 = getApiClient();
1032
+ const spinner = startSpinner("Cancelling deployment...");
1033
+ const apps = await client2.application.allByOrganization.query();
1034
+ const app = findApp2(apps, appIdentifier);
1035
+ if (!app) {
1036
+ failSpinner();
1037
+ const suggestions = findSimilar(
1038
+ appIdentifier,
1039
+ apps.map((a) => a.name)
1040
+ );
1041
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1042
+ }
1043
+ await client2.application.cancelDeployment.mutate({
1044
+ applicationId: app.applicationId
1045
+ });
1046
+ succeedSpinner("Deployment cancelled");
1047
+ if (isJsonMode()) {
1048
+ outputData({ cancelled: true, applicationId: app.applicationId });
1049
+ }
1050
+ } catch (err) {
1051
+ handleError(err);
1052
+ }
1053
+ });
1054
+ program2.command("deploy:list").argument("<app>", "Application ID or name").description("List recent deployments").option("-n, --limit <number>", "Number of deployments to show", "10").action(async (appIdentifier, options) => {
1055
+ try {
1056
+ if (!isLoggedIn()) throw new AuthError();
1057
+ const client2 = getApiClient();
1058
+ const spinner = startSpinner("Fetching deployments...");
1059
+ const apps = await client2.application.allByOrganization.query();
1060
+ const appSummary = findApp2(apps, appIdentifier);
1061
+ if (!appSummary) {
1062
+ failSpinner();
1063
+ const suggestions = findSimilar(
1064
+ appIdentifier,
1065
+ apps.map((a) => a.name)
1066
+ );
1067
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1068
+ }
1069
+ const deployments = await client2.deployment.all.query({
1070
+ applicationId: appSummary.applicationId
1071
+ });
1072
+ succeedSpinner();
1073
+ const limitedDeployments = deployments.slice(0, parseInt(options.limit, 10));
1074
+ if (isJsonMode()) {
1075
+ outputData(limitedDeployments);
1076
+ return;
1077
+ }
1078
+ if (limitedDeployments.length === 0) {
1079
+ log("");
1080
+ log("No deployments found.");
1081
+ log("");
1082
+ log(`Deploy with: ${colors.dim(`tarout deploy ${appSummary.applicationId.slice(0, 8)}`)}`);
1083
+ return;
1084
+ }
1085
+ log("");
1086
+ table(
1087
+ ["ID", "STATUS", "TITLE", "CREATED"],
1088
+ limitedDeployments.map((d) => [
1089
+ colors.cyan(d.deploymentId.slice(0, 8)),
1090
+ getStatusBadge(d.status),
1091
+ d.title || colors.dim("-"),
1092
+ formatDate2(d.createdAt)
1093
+ ])
1094
+ );
1095
+ log("");
1096
+ log(colors.dim(`${limitedDeployments.length} deployment${limitedDeployments.length === 1 ? "" : "s"}`));
1097
+ } catch (err) {
1098
+ handleError(err);
1099
+ }
1100
+ });
1101
+ }
1102
+ function registerLogsCommand(program2) {
1103
+ program2.command("logs").argument("<app>", "Application ID or name").description("View application logs").option("-l, --level <level>", "Log level (ALL, ERROR, WARN, INFO, DEBUG)", "ALL").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
+ try {
1105
+ if (!isLoggedIn()) throw new AuthError();
1106
+ const client2 = getApiClient();
1107
+ const spinner = startSpinner("Fetching logs...");
1108
+ const apps = await client2.application.allByOrganization.query();
1109
+ const app = findApp2(apps, appIdentifier);
1110
+ if (!app) {
1111
+ failSpinner();
1112
+ const suggestions = findSimilar(
1113
+ appIdentifier,
1114
+ apps.map((a) => a.name)
1115
+ );
1116
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1117
+ }
1118
+ let timeRange;
1119
+ if (options.since) {
1120
+ const since = parseDuration(options.since);
1121
+ if (since) {
1122
+ timeRange = {
1123
+ start: new Date(Date.now() - since).toISOString(),
1124
+ end: null
1125
+ };
1126
+ }
1127
+ }
1128
+ const logs = await client2.logs.getCloudRunLogs.query({
1129
+ applicationId: app.applicationId,
1130
+ level: options.level.toUpperCase(),
1131
+ limit: parseInt(options.limit, 10),
1132
+ timeRange
1133
+ });
1134
+ succeedSpinner();
1135
+ if (isJsonMode()) {
1136
+ outputData(logs);
1137
+ return;
1138
+ }
1139
+ if (!logs.entries || logs.entries.length === 0) {
1140
+ log("");
1141
+ log("No logs found.");
1142
+ return;
1143
+ }
1144
+ log("");
1145
+ for (const entry of logs.entries) {
1146
+ printLogEntry(entry);
1147
+ }
1148
+ if (options.follow) {
1149
+ log("");
1150
+ log(colors.dim("Streaming logs... (Ctrl+C to stop)"));
1151
+ log("");
1152
+ let lastTimestamp = logs.entries.length > 0 ? new Date(logs.entries[logs.entries.length - 1].timestamp).getTime() : Date.now();
1153
+ while (true) {
1154
+ await sleep(2e3);
1155
+ const newLogs = await client2.logs.getCloudRunLogs.query({
1156
+ applicationId: app.applicationId,
1157
+ level: options.level.toUpperCase(),
1158
+ limit: 50,
1159
+ timeRange: {
1160
+ start: new Date(lastTimestamp + 1).toISOString(),
1161
+ end: null
1162
+ }
1163
+ });
1164
+ if (newLogs.entries && newLogs.entries.length > 0) {
1165
+ for (const entry of newLogs.entries) {
1166
+ const entryTime = new Date(entry.timestamp).getTime();
1167
+ if (entryTime > lastTimestamp) {
1168
+ printLogEntry(entry);
1169
+ lastTimestamp = entryTime;
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ } catch (err) {
1176
+ handleError(err);
1177
+ }
1178
+ });
1179
+ }
1180
+ function findApp2(apps, identifier) {
1181
+ const lowerIdentifier = identifier.toLowerCase();
1182
+ return apps.find(
1183
+ (app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
1184
+ );
1185
+ }
1186
+ function formatDate2(date) {
1187
+ const d = new Date(date);
1188
+ return d.toLocaleString("en-US", {
1189
+ month: "short",
1190
+ day: "numeric",
1191
+ hour: "2-digit",
1192
+ minute: "2-digit"
1193
+ });
1194
+ }
1195
+ function sleep(ms) {
1196
+ return new Promise((resolve) => setTimeout(resolve, ms));
1197
+ }
1198
+ function parseDuration(duration) {
1199
+ const match = duration.match(/^(\d+)(s|m|h|d)$/);
1200
+ if (!match) return null;
1201
+ const value = parseInt(match[1], 10);
1202
+ const unit = match[2];
1203
+ const multipliers = {
1204
+ s: 1e3,
1205
+ m: 60 * 1e3,
1206
+ h: 60 * 60 * 1e3,
1207
+ d: 24 * 60 * 60 * 1e3
1208
+ };
1209
+ return value * (multipliers[unit] || 0);
1210
+ }
1211
+ function printLogEntry(entry) {
1212
+ const time = new Date(entry.timestamp);
1213
+ const timeStr = time.toLocaleTimeString("en-US", {
1214
+ hour12: false,
1215
+ hour: "2-digit",
1216
+ minute: "2-digit",
1217
+ second: "2-digit"
1218
+ });
1219
+ const levelColors = {
1220
+ ERROR: colors.error,
1221
+ WARN: colors.warn,
1222
+ INFO: colors.info,
1223
+ DEBUG: colors.dim,
1224
+ DEFAULT: colors.dim
1225
+ };
1226
+ const colorFn = levelColors[entry.severity] || levelColors.DEFAULT;
1227
+ const level = entry.severity.padEnd(5);
1228
+ console.log(`${colors.dim(timeStr)} ${colorFn(level)} ${entry.message}`);
1229
+ }
1230
+
1231
+ // src/commands/env.ts
1232
+ import { readFileSync, writeFileSync, existsSync } from "fs";
1233
+ function registerEnvCommands(program2) {
1234
+ const env = program2.command("env").argument("<app>", "Application ID or name").description("Manage environment variables");
1235
+ env.command("list").alias("ls").description("List all environment variables").option("--reveal", "Show actual values (not masked)").action(async (options, command) => {
1236
+ try {
1237
+ if (!isLoggedIn()) throw new AuthError();
1238
+ const appIdentifier = command.parent.args[0];
1239
+ const client2 = getApiClient();
1240
+ const spinner = startSpinner("Fetching environment variables...");
1241
+ const apps = await client2.application.allByOrganization.query();
1242
+ const app = findApp3(apps, appIdentifier);
1243
+ if (!app) {
1244
+ failSpinner();
1245
+ const suggestions = findSimilar(
1246
+ appIdentifier,
1247
+ apps.map((a) => a.name)
1248
+ );
1249
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1250
+ }
1251
+ const variables = await client2.envVariable.list.query({
1252
+ applicationId: app.applicationId,
1253
+ includeValues: options.reveal || false
1254
+ });
1255
+ succeedSpinner();
1256
+ if (isJsonMode()) {
1257
+ outputData(variables);
1258
+ return;
1259
+ }
1260
+ if (variables.length === 0) {
1261
+ log("");
1262
+ log("No environment variables found.");
1263
+ log("");
1264
+ log(`Set one with: ${colors.dim(`tarout env ${app.name} set KEY=value`)}`);
1265
+ return;
1266
+ }
1267
+ log("");
1268
+ table(
1269
+ ["KEY", "VALUE", "SECRET", "UPDATED"],
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
+ });
1329
+ }
1330
+ succeedSpinner(`Set ${key}`);
1331
+ if (isJsonMode()) {
1332
+ outputData({ key, updated: !!existingVar });
1333
+ } else {
1334
+ quietOutput(key);
1335
+ }
1336
+ } catch (err) {
1337
+ handleError(err);
1338
+ }
1339
+ });
1340
+ env.command("unset").argument("<key>", "Variable key to remove").description("Remove an environment variable").action(async (key, options, command) => {
1341
+ try {
1342
+ if (!isLoggedIn()) throw new AuthError();
1343
+ const appIdentifier = command.parent.parent.args[0];
1344
+ const client2 = getApiClient();
1345
+ const spinner = startSpinner("Removing environment variable...");
1346
+ const apps = await client2.application.allByOrganization.query();
1347
+ const app = findApp3(apps, appIdentifier);
1348
+ if (!app) {
1349
+ failSpinner();
1350
+ const suggestions = findSimilar(
1351
+ appIdentifier,
1352
+ apps.map((a) => a.name)
1353
+ );
1354
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1355
+ }
1356
+ await client2.envVariable.delete.mutate({
1357
+ applicationId: app.applicationId,
1358
+ key
1359
+ });
1360
+ succeedSpinner(`Removed ${key}`);
1361
+ if (isJsonMode()) {
1362
+ outputData({ key, deleted: true });
1363
+ } else {
1364
+ quietOutput(key);
1365
+ }
1366
+ } catch (err) {
1367
+ handleError(err);
1368
+ }
1369
+ });
1370
+ 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) => {
1371
+ try {
1372
+ if (!isLoggedIn()) throw new AuthError();
1373
+ const appIdentifier = command.parent.parent.args[0];
1374
+ const client2 = getApiClient();
1375
+ const spinner = startSpinner("Downloading environment variables...");
1376
+ const apps = await client2.application.allByOrganization.query();
1377
+ const app = findApp3(apps, appIdentifier);
1378
+ if (!app) {
1379
+ failSpinner();
1380
+ const suggestions = findSimilar(
1381
+ appIdentifier,
1382
+ apps.map((a) => a.name)
1383
+ );
1384
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1385
+ }
1386
+ if (existsSync(options.output) && !shouldSkipConfirmation()) {
1387
+ succeedSpinner();
1388
+ const confirmed = await confirm(
1389
+ `File ${options.output} already exists. Overwrite?`,
1390
+ false
1391
+ );
1392
+ if (!confirmed) {
1393
+ log("Cancelled.");
1394
+ return;
1395
+ }
1396
+ }
1397
+ const result = await client2.envVariable.export.query({
1398
+ applicationId: app.applicationId,
1399
+ format: "dotenv",
1400
+ maskSecrets: !options.reveal
1401
+ });
1402
+ writeFileSync(options.output, result.content);
1403
+ succeedSpinner(`Saved to ${options.output}`);
1404
+ if (isJsonMode()) {
1405
+ outputData({ file: options.output, content: result.content });
1406
+ } else {
1407
+ quietOutput(options.output);
1408
+ }
1409
+ } catch (err) {
1410
+ handleError(err);
1411
+ }
1412
+ });
1413
+ 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) => {
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}`);
1419
+ }
1420
+ const content = readFileSync(options.input, "utf-8");
1421
+ const client2 = getApiClient();
1422
+ const spinner = startSpinner("Uploading environment variables...");
1423
+ const apps = await client2.application.allByOrganization.query();
1424
+ const app = findApp3(apps, appIdentifier);
1425
+ if (!app) {
1426
+ failSpinner();
1427
+ const suggestions = findSimilar(
1428
+ appIdentifier,
1429
+ apps.map((a) => a.name)
1430
+ );
1431
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1432
+ }
1433
+ const result = await client2.envVariable.import.mutate({
1434
+ applicationId: app.applicationId,
1435
+ content,
1436
+ format: "dotenv",
1437
+ merge: !options.replace
1438
+ });
1439
+ succeedSpinner(`Imported ${result.imported} variables`);
1440
+ if (isJsonMode()) {
1441
+ outputData(result);
1442
+ } else {
1443
+ quietOutput(String(result.imported));
1444
+ if (result.skipped > 0) {
1445
+ log(colors.dim(`Skipped ${result.skipped} (already exist)`));
1446
+ }
1447
+ }
1448
+ } catch (err) {
1449
+ handleError(err);
1450
+ }
1451
+ });
1452
+ }
1453
+ function findApp3(apps, identifier) {
1454
+ const lowerIdentifier = identifier.toLowerCase();
1455
+ return apps.find(
1456
+ (app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
1457
+ );
1458
+ }
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
+
1469
+ // src/commands/db.ts
1470
+ import { spawn } from "child_process";
1471
+ function registerDbCommands(program2) {
1472
+ const db = program2.command("db").description("Manage databases");
1473
+ db.command("list").alias("ls").description("List all databases").option("-t, --type <type>", "Filter by type (postgres, mysql, redis)").action(async (options) => {
1474
+ try {
1475
+ if (!isLoggedIn()) throw new AuthError();
1476
+ const client2 = getApiClient();
1477
+ const spinner = startSpinner("Fetching databases...");
1478
+ const [postgres, mysql, redis] = await Promise.all([
1479
+ client2.postgres.allByOrganization.query(),
1480
+ client2.mysql.allByOrganization.query(),
1481
+ client2.redis.allByOrganization.query()
1482
+ ]);
1483
+ succeedSpinner();
1484
+ let databases = [];
1485
+ if (!options.type || options.type === "postgres") {
1486
+ databases = databases.concat(
1487
+ postgres.map((db2) => ({
1488
+ id: db2.postgresId,
1489
+ name: db2.name,
1490
+ type: "postgres",
1491
+ status: db2.applicationStatus,
1492
+ created: db2.createdAt
1493
+ }))
1494
+ );
1495
+ }
1496
+ if (!options.type || options.type === "mysql") {
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
+ );
1517
+ }
1518
+ if (isJsonMode()) {
1519
+ outputData(databases);
1520
+ return;
1521
+ }
1522
+ if (databases.length === 0) {
1523
+ log("");
1524
+ log("No databases found.");
1525
+ log("");
1526
+ log(`Create one with: ${colors.dim("tarout db create <name>")}`);
1527
+ return;
1528
+ }
1529
+ log("");
1530
+ table(
1531
+ ["ID", "NAME", "TYPE", "STATUS", "CREATED"],
1532
+ databases.map((db2) => [
1533
+ colors.cyan(db2.id.slice(0, 8)),
1534
+ db2.name,
1535
+ getTypeLabel(db2.type),
1536
+ getStatusBadge(db2.status),
1537
+ formatDate4(db2.created)
1538
+ ])
1539
+ );
1540
+ log("");
1541
+ log(
1542
+ colors.dim(`${databases.length} database${databases.length === 1 ? "" : "s"}`)
1543
+ );
1544
+ } catch (err) {
1545
+ handleError(err);
1546
+ }
1547
+ });
1548
+ db.command("create").argument("[name]", "Database name").description("Create a new database").option("-t, --type <type>", "Database type (postgres, mysql, redis)", "postgres").option("-d, --description <description>", "Database description").action(async (name, options) => {
1549
+ try {
1550
+ if (!isLoggedIn()) throw new AuthError();
1551
+ const profile = getCurrentProfile();
1552
+ if (!profile) throw new AuthError();
1553
+ let dbName = name;
1554
+ let dbType = options.type;
1555
+ if (!dbName) {
1556
+ dbName = await input("Database name:");
1557
+ }
1558
+ if (!options.type && !shouldSkipConfirmation()) {
1559
+ dbType = await select("Database type:", [
1560
+ { name: "PostgreSQL", value: "postgres" },
1561
+ { name: "MySQL", value: "mysql" },
1562
+ { name: "Redis", value: "redis" }
1563
+ ]);
1564
+ }
1565
+ const slug = generateSlug2(dbName);
1566
+ const client2 = getApiClient();
1567
+ const spinner = startSpinner(`Creating ${dbType} database...`);
1568
+ let database;
1569
+ switch (dbType) {
1570
+ case "postgres":
1571
+ database = await client2.postgres.create.mutate({
1572
+ name: dbName,
1573
+ appName: slug,
1574
+ dockerImage: "postgres:17",
1575
+ organizationId: profile.organizationId,
1576
+ description: options.description
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);
1599
+ }
1600
+ succeedSpinner("Database created!");
1601
+ const dbId = database.postgresId || database.mysqlId || database.redisId;
1602
+ if (isJsonMode()) {
1603
+ outputData(database);
1604
+ return;
1605
+ }
1606
+ quietOutput(dbId);
1607
+ box("Database Created", [
1608
+ `ID: ${colors.cyan(dbId)}`,
1609
+ `Name: ${database.name}`,
1610
+ `Type: ${getTypeLabel(dbType)}`
1611
+ ]);
1612
+ log("Next steps:");
1613
+ log(` View connection info: ${colors.dim(`tarout db info ${dbId.slice(0, 8)}`)}`);
1614
+ log("");
1615
+ } catch (err) {
1616
+ handleError(err);
1617
+ }
1618
+ });
1619
+ db.command("delete").alias("rm").argument("<db>", "Database ID or name").description("Delete a database").action(async (dbIdentifier) => {
1620
+ try {
1621
+ if (!isLoggedIn()) throw new AuthError();
1622
+ const client2 = getApiClient();
1623
+ const spinner = startSpinner("Finding database...");
1624
+ const allDbs = await getAllDatabases(client2);
1625
+ const dbInfo = findDatabase(allDbs, dbIdentifier);
1626
+ if (!dbInfo) {
1627
+ failSpinner();
1628
+ const suggestions = findSimilar(
1629
+ dbIdentifier,
1630
+ allDbs.map((d) => d.name)
1631
+ );
1632
+ throw new NotFoundError("Database", dbIdentifier, suggestions);
1633
+ }
1634
+ succeedSpinner();
1635
+ if (!shouldSkipConfirmation()) {
1636
+ log("");
1637
+ log(`Database: ${colors.bold(dbInfo.name)}`);
1638
+ log(`Type: ${getTypeLabel(dbInfo.type)}`);
1639
+ log(`ID: ${colors.dim(dbInfo.id)}`);
1640
+ log("");
1641
+ const confirmed = await confirm(
1642
+ `Are you sure you want to delete "${dbInfo.name}"? This cannot be undone.`,
1643
+ false
1644
+ );
1645
+ if (!confirmed) {
1646
+ log("Cancelled.");
1647
+ return;
1648
+ }
1649
+ }
1650
+ const deleteSpinner = startSpinner("Deleting database...");
1651
+ switch (dbInfo.type) {
1652
+ case "postgres":
1653
+ await client2.postgres.remove.mutate({ postgresId: dbInfo.id });
1654
+ break;
1655
+ case "mysql":
1656
+ await client2.mysql.remove.mutate({ mysqlId: dbInfo.id });
1657
+ break;
1658
+ case "redis":
1659
+ await client2.redis.remove.mutate({ redisId: dbInfo.id });
1660
+ break;
1661
+ }
1662
+ succeedSpinner("Database deleted!");
1663
+ if (isJsonMode()) {
1664
+ outputData({ deleted: true, id: dbInfo.id });
1665
+ } else {
1666
+ quietOutput(dbInfo.id);
1667
+ }
1668
+ } catch (err) {
1669
+ handleError(err);
1670
+ }
1671
+ });
1672
+ db.command("info").argument("<db>", "Database ID or name").description("Show database details and connection info").action(async (dbIdentifier) => {
1673
+ try {
1674
+ if (!isLoggedIn()) throw new AuthError();
1675
+ const client2 = getApiClient();
1676
+ const spinner = startSpinner("Fetching database info...");
1677
+ const allDbs = await getAllDatabases(client2);
1678
+ const dbSummary = findDatabase(allDbs, dbIdentifier);
1679
+ if (!dbSummary) {
1680
+ failSpinner();
1681
+ const suggestions = findSimilar(
1682
+ dbIdentifier,
1683
+ allDbs.map((d) => d.name)
1684
+ );
1685
+ throw new NotFoundError("Database", dbIdentifier, suggestions);
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;
1698
+ }
1699
+ succeedSpinner();
1700
+ if (isJsonMode()) {
1701
+ outputData(dbDetails);
1702
+ return;
1703
+ }
1704
+ log("");
1705
+ log(colors.bold(dbDetails.name));
1706
+ log(colors.dim(dbSummary.id));
1707
+ log("");
1708
+ log(`${colors.bold("Status")}`);
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
+ }
1723
+ } else {
1724
+ if (dbDetails.cloudInstanceId || dbDetails.databaseName) {
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)}`);
1739
+ log("");
1740
+ }
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
+ 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
+ } catch (err) {
1792
+ handleError(err);
1793
+ }
1794
+ });
1795
+ }
1796
+ async function getAllDatabases(client2) {
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) {
1830
+ const lowerIdentifier = identifier.toLowerCase();
1831
+ return databases.find(
1832
+ (db) => db.id === identifier || db.id.startsWith(identifier) || db.name.toLowerCase() === lowerIdentifier
1833
+ );
1834
+ }
1835
+ function generateSlug2(name) {
1836
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
1837
+ }
1838
+ function formatDate4(date) {
1839
+ const d = new Date(date);
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;
1849
+ }
1850
+ function getConnectionString(type, details) {
1851
+ const host = details.cloudHost || "localhost";
1852
+ const user = details.cloudUsername || details.databaseUser || "user";
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
+ }
1867
+ }
1868
+ function getConnectCommand(type, details) {
1869
+ const host = details.cloudHost || "localhost";
1870
+ const user = details.cloudUsername || details.databaseUser || "user";
1871
+ const password = details.cloudPassword || details.databasePassword || "";
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
+ }
1919
+ }
1920
+
1921
+ // src/commands/domains.ts
1922
+ function registerDomainsCommands(program2) {
1923
+ const domains = program2.command("domains").description("Manage custom domains");
1924
+ domains.command("list").alias("ls").argument("[app]", "Application ID or name (optional)").description("List domains").action(async (appIdentifier) => {
1925
+ try {
1926
+ if (!isLoggedIn()) throw new AuthError();
1927
+ const client2 = getApiClient();
1928
+ const spinner = startSpinner("Fetching domains...");
1929
+ let domainsList;
1930
+ if (appIdentifier) {
1931
+ const apps = await client2.application.allByOrganization.query();
1932
+ const app = findApp4(apps, appIdentifier);
1933
+ if (!app) {
1934
+ failSpinner();
1935
+ const suggestions = findSimilar(
1936
+ appIdentifier,
1937
+ apps.map((a) => a.name)
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
+ });
1948
+ }
1949
+ succeedSpinner();
1950
+ if (isJsonMode()) {
1951
+ outputData(domainsList);
1952
+ return;
1953
+ }
1954
+ if (domainsList.length === 0) {
1955
+ log("");
1956
+ log("No domains found.");
1957
+ log("");
1958
+ log(`Add one with: ${colors.dim("tarout domains add <app> <domain>")}`);
1959
+ return;
1960
+ }
1961
+ log("");
1962
+ table(
1963
+ ["DOMAIN", "APPLICATION", "VERIFIED", "SSL"],
1964
+ domainsList.map((d) => [
1965
+ colors.cyan(d.host),
1966
+ d.application?.name || colors.dim("unlinked"),
1967
+ d.isVerified ? colors.success("Yes") : colors.warn("No"),
1968
+ d.certificateType || colors.dim("-")
1969
+ ])
1970
+ );
1971
+ log("");
1972
+ log(colors.dim(`${domainsList.length} domain${domainsList.length === 1 ? "" : "s"}`));
1973
+ } catch (err) {
1974
+ handleError(err);
1975
+ }
1976
+ });
1977
+ 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) => {
1978
+ try {
1979
+ if (!isLoggedIn()) throw new AuthError();
1980
+ if (!isValidDomain(domainName)) {
1981
+ throw new InvalidArgumentError(
1982
+ `Invalid domain format: ${domainName}. Use format like: app.example.com`
1983
+ );
1984
+ }
1985
+ const client2 = getApiClient();
1986
+ const spinner = startSpinner("Finding application...");
1987
+ const apps = await client2.application.allByOrganization.query();
1988
+ const app = findApp4(apps, appIdentifier);
1989
+ if (!app) {
1990
+ failSpinner();
1991
+ const suggestions = findSimilar(
1992
+ appIdentifier,
1993
+ apps.map((a) => a.name)
1994
+ );
1995
+ throw new NotFoundError("Application", appIdentifier, suggestions);
1996
+ }
1997
+ updateSpinner2("Adding domain...");
1998
+ const domain = await client2.domain.create.mutate({
1999
+ host: domainName,
2000
+ applicationId: app.applicationId
2001
+ });
2002
+ succeedSpinner("Domain added!");
2003
+ if (isJsonMode()) {
2004
+ outputData(domain);
2005
+ return;
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("");
2018
+ }
2019
+ } catch (err) {
2020
+ handleError(err);
2021
+ }
2022
+ });
2023
+ domains.command("remove").alias("rm").argument("<domain>", "Domain ID or hostname").description("Remove a domain").action(async (domainIdentifier) => {
2024
+ try {
2025
+ if (!isLoggedIn()) throw new AuthError();
2026
+ const client2 = getApiClient();
2027
+ const spinner = startSpinner("Finding domain...");
2028
+ const allDomains = await client2.domain.all.query({ includeUnlinked: true });
2029
+ const domain = findDomain(allDomains, domainIdentifier);
2030
+ if (!domain) {
2031
+ failSpinner();
2032
+ const suggestions = findSimilar(
2033
+ domainIdentifier,
2034
+ allDomains.map((d) => d.host)
2035
+ );
2036
+ throw new NotFoundError("Domain", domainIdentifier, suggestions);
2037
+ }
2038
+ succeedSpinner();
2039
+ if (!shouldSkipConfirmation()) {
2040
+ log("");
2041
+ log(`Domain: ${colors.bold(domain.host)}`);
2042
+ if (domain.application) {
2043
+ log(`Application: ${domain.application.name}`);
2044
+ }
2045
+ log("");
2046
+ const confirmed = await confirm(
2047
+ `Are you sure you want to remove "${domain.host}"?`,
2048
+ false
2049
+ );
2050
+ if (!confirmed) {
2051
+ log("Cancelled.");
2052
+ return;
2053
+ }
2054
+ }
2055
+ const deleteSpinner = startSpinner("Removing domain...");
2056
+ await client2.domain.delete.mutate({
2057
+ domainId: domain.domainId
2058
+ });
2059
+ succeedSpinner("Domain removed!");
2060
+ if (isJsonMode()) {
2061
+ outputData({ deleted: true, domainId: domain.domainId });
2062
+ } else {
2063
+ quietOutput(domain.domainId);
2064
+ }
2065
+ } catch (err) {
2066
+ handleError(err);
2067
+ }
2068
+ });
2069
+ domains.command("verify").argument("<domain>", "Domain ID or hostname").description("Verify domain DNS configuration").action(async (domainIdentifier) => {
2070
+ try {
2071
+ if (!isLoggedIn()) throw new AuthError();
2072
+ const client2 = getApiClient();
2073
+ const spinner = startSpinner("Finding domain...");
2074
+ const allDomains = await client2.domain.all.query({ includeUnlinked: true });
2075
+ const domain = findDomain(allDomains, domainIdentifier);
2076
+ if (!domain) {
2077
+ failSpinner();
2078
+ const suggestions = findSimilar(
2079
+ domainIdentifier,
2080
+ allDomains.map((d) => d.host)
2081
+ );
2082
+ throw new NotFoundError("Domain", domainIdentifier, suggestions);
2083
+ }
2084
+ updateSpinner2("Verifying DNS configuration...");
2085
+ const result = await client2.domain.validateDomain.mutate({
2086
+ domainId: domain.domainId
2087
+ });
2088
+ succeedSpinner();
2089
+ if (isJsonMode()) {
2090
+ outputData(result);
2091
+ return;
2092
+ }
2093
+ log("");
2094
+ if (result.isValid) {
2095
+ success(`Domain ${colors.cyan(domain.host)} is verified!`);
2096
+ log("");
2097
+ log("DNS records are correctly configured.");
2098
+ } else {
2099
+ error(`Domain ${domain.host} verification failed`);
2100
+ log("");
2101
+ log("Please ensure DNS is configured correctly:");
2102
+ log("");
2103
+ log(` ${colors.bold("Option 1: CNAME Record")}`);
2104
+ log(` Name: ${domain.host}`);
2105
+ log(` Value: ${colors.cyan("your-app.tarout.app")}`);
2106
+ log("");
2107
+ log(` ${colors.bold("Option 2: A Record")}`);
2108
+ log(` Name: ${domain.host}`);
2109
+ log(` Value: ${colors.cyan("(your app's IP address)")}`);
2110
+ log("");
2111
+ log(colors.dim("DNS changes can take up to 48 hours to propagate."));
2112
+ }
2113
+ log("");
2114
+ } catch (err) {
2115
+ handleError(err);
2116
+ }
2117
+ });
2118
+ }
2119
+ function findApp4(apps, identifier) {
2120
+ const lowerIdentifier = identifier.toLowerCase();
2121
+ return apps.find(
2122
+ (app) => app.applicationId === identifier || app.applicationId.startsWith(identifier) || app.name.toLowerCase() === lowerIdentifier || app.appName?.toLowerCase() === lowerIdentifier
2123
+ );
2124
+ }
2125
+ function findDomain(domains, identifier) {
2126
+ const lowerIdentifier = identifier.toLowerCase();
2127
+ return domains.find(
2128
+ (d) => d.domainId === identifier || d.domainId.startsWith(identifier) || d.host.toLowerCase() === lowerIdentifier
2129
+ );
2130
+ }
2131
+ function isValidDomain(domain) {
2132
+ const pattern = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
2133
+ return pattern.test(domain);
2134
+ }
2135
+ function updateSpinner2(text) {
2136
+ import("./spinner-2NALE2OE.js").then(({ startSpinner: startSpinner2 }) => {
2137
+ startSpinner2(text);
2138
+ });
2139
+ }
2140
+
2141
+ // src/commands/orgs.ts
2142
+ function registerOrgsCommands(program2) {
2143
+ const orgs = program2.command("orgs").description("Manage organizations");
2144
+ orgs.command("list").alias("ls").description("List your organizations").action(async () => {
2145
+ try {
2146
+ if (!isLoggedIn()) throw new AuthError();
2147
+ const client2 = getApiClient();
2148
+ const profile = getCurrentProfile();
2149
+ const spinner = startSpinner("Fetching organizations...");
2150
+ const organizations = await client2.organization.all.query();
2151
+ succeedSpinner();
2152
+ if (isJsonMode()) {
2153
+ outputData(organizations);
2154
+ return;
2155
+ }
2156
+ if (organizations.length === 0) {
2157
+ log("");
2158
+ log("No organizations found.");
2159
+ return;
2160
+ }
2161
+ log("");
2162
+ table(
2163
+ ["ID", "NAME", "ACTIVE"],
2164
+ organizations.map((org) => [
2165
+ colors.cyan(org.id.slice(0, 8)),
2166
+ org.name,
2167
+ org.id === profile?.organizationId ? colors.success("*") : ""
2168
+ ])
2169
+ );
2170
+ log("");
2171
+ log(colors.dim(`${organizations.length} organization${organizations.length === 1 ? "" : "s"}`));
2172
+ log("");
2173
+ log(`Current: ${colors.bold(profile?.organizationName || "None")}`);
2174
+ } catch (err) {
2175
+ handleError(err);
2176
+ }
2177
+ });
2178
+ orgs.command("switch").argument("<org>", "Organization ID or name").description("Switch to a different organization").action(async (orgIdentifier) => {
2179
+ try {
2180
+ if (!isLoggedIn()) throw new AuthError();
2181
+ const client2 = getApiClient();
2182
+ const spinner = startSpinner("Switching organization...");
2183
+ const organizations = await client2.organization.all.query();
2184
+ const org = findOrg(organizations, orgIdentifier);
2185
+ if (!org) {
2186
+ failSpinner();
2187
+ const suggestions = findSimilar(
2188
+ orgIdentifier,
2189
+ organizations.map((o) => o.name)
2190
+ );
2191
+ throw new NotFoundError("Organization", orgIdentifier, suggestions);
2192
+ }
2193
+ const envs = await client2.environment.all.query();
2194
+ const defaultEnv = envs.find((e) => e.organizationId === org.id) || envs[0];
2195
+ updateProfile({
2196
+ organizationId: org.id,
2197
+ organizationName: org.name,
2198
+ environmentId: defaultEnv?.environmentId,
2199
+ environmentName: defaultEnv?.name || "production"
2200
+ });
2201
+ succeedSpinner(`Switched to ${org.name}`);
2202
+ if (isJsonMode()) {
2203
+ outputData({
2204
+ organizationId: org.id,
2205
+ organizationName: org.name,
2206
+ environmentId: defaultEnv?.environmentId,
2207
+ environmentName: defaultEnv?.name
2208
+ });
2209
+ } else {
2210
+ quietOutput(org.id);
2211
+ log("");
2212
+ log(`Organization: ${colors.bold(org.name)}`);
2213
+ log(`Environment: ${colors.bold(defaultEnv?.name || "production")}`);
2214
+ log("");
2215
+ }
2216
+ } catch (err) {
2217
+ handleError(err);
2218
+ }
2219
+ });
2220
+ }
2221
+ function registerEnvsCommands(program2) {
2222
+ const envs = program2.command("envs").description("Manage environments (production/staging)");
2223
+ envs.command("list").alias("ls").description("List environments").action(async () => {
2224
+ try {
2225
+ if (!isLoggedIn()) throw new AuthError();
2226
+ const client2 = getApiClient();
2227
+ const profile = getCurrentProfile();
2228
+ const spinner = startSpinner("Fetching environments...");
2229
+ const environments = await client2.environment.all.query();
2230
+ succeedSpinner();
2231
+ if (isJsonMode()) {
2232
+ outputData(environments);
2233
+ return;
2234
+ }
2235
+ if (environments.length === 0) {
2236
+ log("");
2237
+ log("No environments found.");
2238
+ return;
2239
+ }
2240
+ log("");
2241
+ table(
2242
+ ["ID", "NAME", "ACTIVE"],
2243
+ environments.map((env) => [
2244
+ colors.cyan(env.environmentId.slice(0, 8)),
2245
+ env.name,
2246
+ env.environmentId === profile?.environmentId ? colors.success("*") : ""
2247
+ ])
2248
+ );
2249
+ log("");
2250
+ log(`Current: ${colors.bold(profile?.environmentName || "None")}`);
2251
+ } catch (err) {
2252
+ handleError(err);
2253
+ }
2254
+ });
2255
+ envs.command("switch").argument("<env>", "Environment ID or name").description("Switch to a different environment").action(async (envIdentifier) => {
2256
+ try {
2257
+ if (!isLoggedIn()) throw new AuthError();
2258
+ const client2 = getApiClient();
2259
+ const spinner = startSpinner("Switching environment...");
2260
+ const environments = await client2.environment.all.query();
2261
+ const env = findEnv(environments, envIdentifier);
2262
+ if (!env) {
2263
+ failSpinner();
2264
+ const suggestions = findSimilar(
2265
+ envIdentifier,
2266
+ environments.map((e) => e.name)
2267
+ );
2268
+ throw new NotFoundError("Environment", envIdentifier, suggestions);
2269
+ }
2270
+ updateProfile({
2271
+ environmentId: env.environmentId,
2272
+ environmentName: env.name
2273
+ });
2274
+ succeedSpinner(`Switched to ${env.name}`);
2275
+ if (isJsonMode()) {
2276
+ outputData({
2277
+ environmentId: env.environmentId,
2278
+ environmentName: env.name
2279
+ });
2280
+ } else {
2281
+ quietOutput(env.environmentId);
2282
+ log("");
2283
+ log(`Environment: ${colors.bold(env.name)}`);
2284
+ log("");
2285
+ }
2286
+ } catch (err) {
2287
+ handleError(err);
2288
+ }
2289
+ });
2290
+ }
2291
+ function findOrg(orgs, identifier) {
2292
+ const lowerIdentifier = identifier.toLowerCase();
2293
+ return orgs.find(
2294
+ (org) => org.id === identifier || org.id.startsWith(identifier) || org.name.toLowerCase() === lowerIdentifier
2295
+ );
2296
+ }
2297
+ function findEnv(envs, identifier) {
2298
+ const lowerIdentifier = identifier.toLowerCase();
2299
+ return envs.find(
2300
+ (env) => env.environmentId === identifier || env.environmentId.startsWith(identifier) || env.name.toLowerCase() === lowerIdentifier
2301
+ );
2302
+ }
2303
+
2304
+ // src/index.ts
2305
+ var program = new Command();
2306
+ program.name("tarout").description("Tarout PaaS Command Line Interface").version("0.1.0").option("--json", "Output as JSON (machine-readable)").option("-y, --yes", "Skip all confirmation prompts").option("-q, --quiet", "Minimal output").option("-v, --verbose", "Extra debug information").option("--no-color", "Disable colored output").hook("preAction", (thisCommand) => {
2307
+ const opts = thisCommand.opts();
2308
+ setGlobalOptions({
2309
+ json: opts.json || false,
2310
+ yes: opts.yes || false,
2311
+ quiet: opts.quiet || false,
2312
+ verbose: opts.verbose || false,
2313
+ noColor: opts.color === false
2314
+ });
2315
+ });
2316
+ registerAuthCommands(program);
2317
+ registerAppsCommands(program);
2318
+ registerDeployCommands(program);
2319
+ registerLogsCommand(program);
2320
+ registerEnvCommands(program);
2321
+ registerDbCommands(program);
2322
+ registerDomainsCommands(program);
2323
+ registerOrgsCommands(program);
2324
+ registerEnvsCommands(program);
2325
+ program.parse();