@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/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/lib/output.ts
13
- import chalk from "chalk";
14
- import Table from "cli-table3";
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
- // 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://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
- ]);
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
- 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);
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
- 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);
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 spinner = startSpinner("Fetching applications...");
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(colors.dim(`${applications.length} application${applications.length === 1 ? "" : "s"}`));
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 spinner = startSpinner("Creating application...");
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(` 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)}`)}`);
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 spinner = startSpinner("Finding application...");
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 deleteSpinner = startSpinner("Deleting application...");
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 spinner = startSpinner("Fetching application...");
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 spinner = startSpinner("Finding application...");
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("No URL found for this application. Deploy first or add a domain.");
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 open2(url);
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
- } else if (diffDays === 1) {
986
+ }
987
+ if (diffDays === 1) {
892
988
  return "Yesterday";
893
- } else if (diffDays < 7) {
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/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)
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
- throw new NotFoundError("Application", appIdentifier, suggestions);
1053
+ return;
916
1054
  }
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()) {
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
- deploymentId: result.deploymentId,
962
- status: "deploying"
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 spinner = startSpinner("Fetching status...");
2257
+ const _spinner = startSpinner("Fetching status...");
984
2258
  const apps = await client2.application.allByOrganization.query();
985
- const appSummary = findApp2(apps, appIdentifier);
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 spinner = startSpinner("Cancelling deployment...");
2306
+ const _spinner = startSpinner("Cancelling deployment...");
1033
2307
  const apps = await client2.application.allByOrganization.query();
1034
- const app = findApp2(apps, appIdentifier);
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 spinner = startSpinner("Fetching deployments...");
2332
+ const _spinner = startSpinner("Fetching deployments...");
1059
2333
  const apps = await client2.application.allByOrganization.query();
1060
- const appSummary = findApp2(apps, appIdentifier);
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(0, parseInt(options.limit, 10));
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(`Deploy with: ${colors.dim(`tarout deploy ${appSummary.applicationId.slice(0, 8)}`)}`);
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
- formatDate2(d.createdAt)
2371
+ formatDate3(d.createdAt)
1093
2372
  ])
1094
2373
  );
1095
2374
  log("");
1096
- log(colors.dim(`${limitedDeployments.length} deployment${limitedDeployments.length === 1 ? "" : "s"}`));
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("-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) => {
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 spinner = startSpinner("Fetching logs...");
2639
+ const _spinner = startSpinner("Fetching logs...");
1108
2640
  const apps = await client2.application.allByOrganization.query();
1109
- const app = findApp2(apps, appIdentifier);
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(logs.entries[logs.entries.length - 1].timestamp).getTime() : Date.now();
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 findApp2(apps, identifier) {
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 formatDate2(date) {
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
- // 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
- });
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
- succeedSpinner(`Set ${key}`);
1331
- if (isJsonMode()) {
1332
- outputData({ key, updated: !!existingVar });
1333
- } else {
1334
- quietOutput(key);
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
- } 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)
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
- await client2.envVariable.delete.mutate({
1357
- applicationId: app.applicationId,
1358
- key
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
- succeedSpinner(`Removed ${key}`);
1361
- if (isJsonMode()) {
1362
- outputData({ key, deleted: true });
1363
- } else {
1364
- quietOutput(key);
1365
- }
1366
- } catch (err) {
1367
- handleError(err);
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
- 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) => {
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 appIdentifier = command.parent.parent.args[0];
2968
+ const profile = getCurrentProfile();
2969
+ if (!profile) throw new AuthError();
1374
2970
  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()) {
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
- const confirmed = await confirm(
1389
- `File ${options.output} already exists. Overwrite?`,
1390
- false
1391
- );
1392
- if (!confirmed) {
1393
- log("Cancelled.");
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
- 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 });
2995
+ applicationId = config2.applicationId;
2996
+ appName = config2.name;
1406
2997
  } else {
1407
- quietOutput(options.output);
2998
+ throw new InvalidArgumentError(
2999
+ "No linked application. Run 'tarout link' first or use --app flag."
3000
+ );
1408
3001
  }
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}`);
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 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) {
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
- const suggestions = findSimilar(
1428
- appIdentifier,
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
- 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`);
3034
+ if (options.port) {
3035
+ envVars.PORT = String(port);
3036
+ }
3037
+ const framework = detectFramework(pkg);
1440
3038
  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
- }
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 findApp3(apps, identifier) {
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/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) => {
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 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
- );
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(databases);
3114
+ outputData(domainsList);
1520
3115
  return;
1521
3116
  }
1522
- if (databases.length === 0) {
3117
+ if (domainsList.length === 0) {
1523
3118
  log("");
1524
- log("No databases found.");
3119
+ log("No domains found.");
1525
3120
  log("");
1526
- log(`Create one with: ${colors.dim("tarout db create <name>")}`);
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
- ["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)
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(`${databases.length} database${databases.length === 1 ? "" : "s"}`)
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
- 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) => {
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
- 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
- ]);
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 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);
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
- succeedSpinner("Database created!");
1601
- const dbId = database.postgresId || database.mysqlId || database.redisId;
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(database);
3173
+ outputData(domain);
1604
3174
  return;
1605
3175
  }
1606
- quietOutput(dbId);
1607
- box("Database Created", [
1608
- `ID: ${colors.cyan(dbId)}`,
1609
- `Name: ${database.name}`,
1610
- `Type: ${getTypeLabel(dbType)}`
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
- log("Next steps:");
1613
- log(` View connection info: ${colors.dim(`tarout db info ${dbId.slice(0, 8)}`)}`);
1614
- log("");
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
- db.command("delete").alias("rm").argument("<db>", "Database ID or name").description("Delete a database").action(async (dbIdentifier) => {
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 spinner = startSpinner("Finding database...");
1624
- const allDbs = await getAllDatabases(client2);
1625
- const dbInfo = findDatabase(allDbs, dbIdentifier);
1626
- if (!dbInfo) {
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
- dbIdentifier,
1630
- allDbs.map((d) => d.name)
3206
+ domainIdentifier,
3207
+ allDomains.map((d) => d.host)
1631
3208
  );
1632
- throw new NotFoundError("Database", dbIdentifier, suggestions);
3209
+ throw new NotFoundError("Domain", domainIdentifier, suggestions);
1633
3210
  }
1634
3211
  succeedSpinner();
1635
3212
  if (!shouldSkipConfirmation()) {
1636
3213
  log("");
1637
- log(`Database: ${colors.bold(dbInfo.name)}`);
1638
- log(`Type: ${getTypeLabel(dbInfo.type)}`);
1639
- log(`ID: ${colors.dim(dbInfo.id)}`);
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 delete "${dbInfo.name}"? This cannot be undone.`,
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 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!");
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, id: dbInfo.id });
3234
+ outputData({ deleted: true, domainId: domain.domainId });
1665
3235
  } else {
1666
- quietOutput(dbInfo.id);
3236
+ quietOutput(domain.domainId);
1667
3237
  }
1668
3238
  } catch (err) {
1669
3239
  handleError(err);
1670
3240
  }
1671
3241
  });
1672
- db.command("info").argument("<db>", "Database ID or name").description("Show database details and connection info").action(async (dbIdentifier) => {
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 spinner = startSpinner("Fetching database info...");
1677
- const allDbs = await getAllDatabases(client2);
1678
- const dbSummary = findDatabase(allDbs, dbIdentifier);
1679
- if (!dbSummary) {
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
- dbIdentifier,
1683
- allDbs.map((d) => d.name)
3254
+ domainIdentifier,
3255
+ allDomains.map((d) => d.host)
1684
3256
  );
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;
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(dbDetails);
3265
+ outputData(result);
1702
3266
  return;
1703
3267
  }
1704
3268
  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
- }
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
- 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)}`);
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
- 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) {
3294
+ function findApp5(apps, identifier) {
1830
3295
  const lowerIdentifier = identifier.toLowerCase();
1831
- return databases.find(
1832
- (db) => db.id === identifier || db.id.startsWith(identifier) || db.name.toLowerCase() === lowerIdentifier
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 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;
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 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
- }
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 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
- }
3310
+ function updateSpinner2(text) {
3311
+ import("./spinner-2NALE2OE.js").then(({ startSpinner: startSpinner2 }) => {
3312
+ startSpinner2(text);
3313
+ });
1919
3314
  }
1920
3315
 
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) => {
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 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
- });
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(domainsList);
3342
+ outputData(variables);
1952
3343
  return;
1953
3344
  }
1954
- if (domainsList.length === 0) {
3345
+ if (variables.length === 0) {
1955
3346
  log("");
1956
- log("No domains found.");
3347
+ log("No environment variables found.");
1957
3348
  log("");
1958
- log(`Add one with: ${colors.dim("tarout domains add <app> <domain>")}`);
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
- ["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("-")
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(colors.dim(`${domainsList.length} domain${domainsList.length === 1 ? "" : "s"}`));
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
- 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) => {
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
- if (!isValidDomain(domainName)) {
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 spinner = startSpinner("Finding application...");
3436
+ const _spinner = startSpinner("Removing environment variable...");
1987
3437
  const apps = await client2.application.allByOrganization.query();
1988
- const app = findApp4(apps, appIdentifier);
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
- updateSpinner2("Adding domain...");
1998
- const domain = await client2.domain.create.mutate({
1999
- host: domainName,
2000
- applicationId: app.applicationId
3447
+ await client2.envVariable.delete.mutate({
3448
+ applicationId: app.applicationId,
3449
+ key
2001
3450
  });
2002
- succeedSpinner("Domain added!");
3451
+ succeedSpinner(`Removed ${key}`);
2003
3452
  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("");
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
- domains.command("remove").alias("rm").argument("<domain>", "Domain ID or hostname").description("Remove a domain").action(async (domainIdentifier) => {
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 spinner = startSpinner("Finding domain...");
2028
- const allDomains = await client2.domain.all.query({ includeUnlinked: true });
2029
- const domain = findDomain(allDomains, domainIdentifier);
2030
- if (!domain) {
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
- domainIdentifier,
2034
- allDomains.map((d) => d.host)
3472
+ appIdentifier,
3473
+ apps.map((a) => a.name)
2035
3474
  );
2036
- throw new NotFoundError("Domain", domainIdentifier, suggestions);
3475
+ throw new NotFoundError("Application", appIdentifier, suggestions);
2037
3476
  }
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("");
3477
+ if (existsSync3(options.output) && !shouldSkipConfirmation()) {
3478
+ succeedSpinner();
2046
3479
  const confirmed = await confirm(
2047
- `Are you sure you want to remove "${domain.host}"?`,
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 deleteSpinner = startSpinner("Removing domain...");
2056
- await client2.domain.delete.mutate({
2057
- domainId: domain.domainId
3488
+ const result = await client2.envVariable.export.query({
3489
+ applicationId: app.applicationId,
3490
+ format: "dotenv",
3491
+ maskSecrets: !options.reveal
2058
3492
  });
2059
- succeedSpinner("Domain removed!");
3493
+ writeFileSync2(options.output, result.content);
3494
+ succeedSpinner(`Saved to ${options.output}`);
2060
3495
  if (isJsonMode()) {
2061
- outputData({ deleted: true, domainId: domain.domainId });
3496
+ outputData({ file: options.output, content: result.content });
2062
3497
  } else {
2063
- quietOutput(domain.domainId);
3498
+ quietOutput(options.output);
2064
3499
  }
2065
3500
  } catch (err) {
2066
3501
  handleError(err);
2067
3502
  }
2068
3503
  });
2069
- domains.command("verify").argument("<domain>", "Domain ID or hostname").description("Verify domain DNS configuration").action(async (domainIdentifier) => {
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 spinner = startSpinner("Finding domain...");
2074
- const allDomains = await client2.domain.all.query({ includeUnlinked: true });
2075
- const domain = findDomain(allDomains, domainIdentifier);
2076
- if (!domain) {
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
- domainIdentifier,
2080
- allDomains.map((d) => d.host)
3519
+ appIdentifier,
3520
+ apps.map((a) => a.name)
2081
3521
  );
2082
- throw new NotFoundError("Domain", domainIdentifier, suggestions);
3522
+ throw new NotFoundError("Application", appIdentifier, suggestions);
2083
3523
  }
2084
- updateSpinner2("Verifying DNS configuration...");
2085
- const result = await client2.domain.validateDomain.mutate({
2086
- domainId: domain.domainId
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
- if (result.isValid) {
2095
- success(`Domain ${colors.cyan(domain.host)} is verified!`);
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("Please ensure DNS is configured correctly:");
3705
+ log(colors.warn("Could not remove project configuration."));
2102
3706
  log("");
2103
- log(` ${colors.bold("Option 1: CNAME Record")}`);
2104
- log(` Name: ${domain.host}`);
2105
- log(` Value: ${colors.cyan("your-app.tarout.app")}`);
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(` ${colors.bold("Option 2: A Record")}`);
2108
- log(` Name: ${domain.host}`);
2109
- log(` Value: ${colors.cyan("(your app's IP address)")}`);
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 findApp4(apps, identifier) {
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 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
- });
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 spinner = startSpinner("Fetching organizations...");
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(colors.dim(`${organizations.length} organization${organizations.length === 1 ? "" : "s"}`));
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 spinner = startSpinner("Switching organization...");
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 spinner = startSpinner("Fetching environments...");
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 spinner = startSpinner("Switching environment...");
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();