@tinybirdco/sdk 0.0.19 → 0.0.21

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.
Files changed (55) hide show
  1. package/README.md +47 -0
  2. package/dist/api/api.d.ts +89 -0
  3. package/dist/api/api.d.ts.map +1 -0
  4. package/dist/api/api.js +218 -0
  5. package/dist/api/api.js.map +1 -0
  6. package/dist/api/api.test.d.ts +2 -0
  7. package/dist/api/api.test.d.ts.map +1 -0
  8. package/dist/api/api.test.js +226 -0
  9. package/dist/api/api.test.js.map +1 -0
  10. package/dist/api/dashboard.d.ts +74 -0
  11. package/dist/api/dashboard.d.ts.map +1 -0
  12. package/dist/api/dashboard.js +102 -0
  13. package/dist/api/dashboard.js.map +1 -0
  14. package/dist/api/dashboard.test.d.ts +2 -0
  15. package/dist/api/dashboard.test.d.ts.map +1 -0
  16. package/dist/api/dashboard.test.js +91 -0
  17. package/dist/api/dashboard.test.js.map +1 -0
  18. package/dist/cli/commands/branch.d.ts +2 -0
  19. package/dist/cli/commands/branch.d.ts.map +1 -1
  20. package/dist/cli/commands/branch.js +12 -1
  21. package/dist/cli/commands/branch.js.map +1 -1
  22. package/dist/cli/commands/build.d.ts +17 -0
  23. package/dist/cli/commands/build.d.ts.map +1 -1
  24. package/dist/cli/commands/build.js +25 -0
  25. package/dist/cli/commands/build.js.map +1 -1
  26. package/dist/cli/commands/dev.d.ts +2 -0
  27. package/dist/cli/commands/dev.d.ts.map +1 -1
  28. package/dist/cli/commands/dev.js +19 -3
  29. package/dist/cli/commands/dev.js.map +1 -1
  30. package/dist/cli/index.js +23 -24
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/cli/output.d.ts +20 -0
  33. package/dist/cli/output.d.ts.map +1 -1
  34. package/dist/cli/output.js +37 -0
  35. package/dist/cli/output.js.map +1 -1
  36. package/dist/client/base.d.ts +3 -17
  37. package/dist/client/base.d.ts.map +1 -1
  38. package/dist/client/base.js +31 -153
  39. package/dist/client/base.js.map +1 -1
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4 -0
  43. package/dist/index.js.map +1 -1
  44. package/package.json +7 -2
  45. package/src/api/api.test.ts +295 -0
  46. package/src/api/api.ts +345 -0
  47. package/src/api/dashboard.test.ts +115 -0
  48. package/src/api/dashboard.ts +121 -0
  49. package/src/cli/commands/branch.ts +15 -1
  50. package/src/cli/commands/build.ts +46 -0
  51. package/src/cli/commands/dev.ts +47 -7
  52. package/src/cli/index.ts +25 -27
  53. package/src/cli/output.ts +54 -0
  54. package/src/client/base.ts +34 -181
  55. package/src/index.ts +23 -0
package/src/api/api.ts ADDED
@@ -0,0 +1,345 @@
1
+ import { createTinybirdFetcher, type TinybirdFetch } from "./fetcher.js";
2
+ import type {
3
+ IngestOptions,
4
+ IngestResult,
5
+ QueryOptions,
6
+ QueryResult,
7
+ TinybirdErrorResponse,
8
+ } from "../client/types.js";
9
+
10
+ const DEFAULT_TIMEOUT = 30000;
11
+
12
+ /**
13
+ * Public, decoupled Tinybird API wrapper configuration
14
+ */
15
+ export interface TinybirdApiConfig {
16
+ /** Tinybird API base URL (e.g. https://api.tinybird.co) */
17
+ baseUrl: string;
18
+ /** Tinybird token used for Authorization bearer header */
19
+ token: string;
20
+ /** Custom fetch implementation (optional) */
21
+ fetch?: typeof fetch;
22
+ /** Default timeout in milliseconds (optional) */
23
+ timeout?: number;
24
+ }
25
+
26
+ /**
27
+ * Request options for the API layer
28
+ */
29
+ export interface TinybirdApiRequestInit extends RequestInit {
30
+ /** Optional token override for a specific request */
31
+ token?: string;
32
+ }
33
+
34
+ export interface TinybirdApiQueryOptions extends QueryOptions {
35
+ /** Optional token override for this request */
36
+ token?: string;
37
+ }
38
+
39
+ export interface TinybirdApiIngestOptions extends IngestOptions {
40
+ /** Optional token override for this request */
41
+ token?: string;
42
+ }
43
+
44
+ /**
45
+ * Error thrown by TinybirdApi when a response is not OK
46
+ */
47
+ export class TinybirdApiError extends Error {
48
+ readonly statusCode: number;
49
+ readonly responseBody?: string;
50
+ readonly response?: TinybirdErrorResponse;
51
+
52
+ constructor(
53
+ message: string,
54
+ statusCode: number,
55
+ responseBody?: string,
56
+ response?: TinybirdErrorResponse
57
+ ) {
58
+ super(message);
59
+ this.name = "TinybirdApiError";
60
+ this.statusCode = statusCode;
61
+ this.responseBody = responseBody;
62
+ this.response = response;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Low-level Tinybird API wrapper.
68
+ *
69
+ * This layer is intentionally decoupled from the typed TinybirdClient layer
70
+ * so it can be used standalone with just baseUrl + token.
71
+ */
72
+ export class TinybirdApi {
73
+ private readonly config: TinybirdApiConfig;
74
+ private readonly baseUrl: string;
75
+ private readonly defaultToken: string;
76
+ private readonly fetchFn: TinybirdFetch;
77
+
78
+ constructor(config: TinybirdApiConfig) {
79
+ if (!config.baseUrl) {
80
+ throw new Error("baseUrl is required");
81
+ }
82
+
83
+ if (!config.token) {
84
+ throw new Error("token is required");
85
+ }
86
+
87
+ this.config = config;
88
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
89
+ this.defaultToken = config.token;
90
+ this.fetchFn = createTinybirdFetcher(config.fetch ?? globalThis.fetch);
91
+ }
92
+
93
+ /**
94
+ * Execute a request against Tinybird API.
95
+ */
96
+ request(path: string, init: TinybirdApiRequestInit = {}): Promise<Response> {
97
+ const { token, headers, ...requestInit } = init;
98
+ const authToken = token ?? this.defaultToken;
99
+ const requestHeaders = new Headers(headers);
100
+
101
+ if (!requestHeaders.has("Authorization")) {
102
+ requestHeaders.set("Authorization", `Bearer ${authToken}`);
103
+ }
104
+
105
+ return this.fetchFn(this.resolveUrl(path), {
106
+ ...requestInit,
107
+ headers: requestHeaders,
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Execute a request and parse JSON response.
113
+ */
114
+ async requestJson<T = unknown>(
115
+ path: string,
116
+ init: TinybirdApiRequestInit = {}
117
+ ): Promise<T> {
118
+ const response = await this.request(path, init);
119
+
120
+ if (!response.ok) {
121
+ await this.handleErrorResponse(response);
122
+ }
123
+
124
+ return (await response.json()) as T;
125
+ }
126
+
127
+ /**
128
+ * Query a Tinybird endpoint
129
+ */
130
+ async query<T = unknown, P extends Record<string, unknown> = Record<string, unknown>>(
131
+ endpointName: string,
132
+ params: P = {} as P,
133
+ options: TinybirdApiQueryOptions = {}
134
+ ): Promise<QueryResult<T>> {
135
+ const url = new URL(`/v0/pipes/${endpointName}.json`, `${this.baseUrl}/`);
136
+
137
+ for (const [key, value] of Object.entries(params)) {
138
+ if (value === undefined || value === null) {
139
+ continue;
140
+ }
141
+
142
+ if (Array.isArray(value)) {
143
+ for (const item of value) {
144
+ url.searchParams.append(key, String(item));
145
+ }
146
+ continue;
147
+ }
148
+
149
+ if (value instanceof Date) {
150
+ url.searchParams.set(key, value.toISOString());
151
+ continue;
152
+ }
153
+
154
+ url.searchParams.set(key, String(value));
155
+ }
156
+
157
+ const response = await this.request(url.toString(), {
158
+ method: "GET",
159
+ token: options.token,
160
+ signal: this.createAbortSignal(options.timeout, options.signal),
161
+ });
162
+
163
+ if (!response.ok) {
164
+ await this.handleErrorResponse(response);
165
+ }
166
+
167
+ return (await response.json()) as QueryResult<T>;
168
+ }
169
+
170
+ /**
171
+ * Ingest a single row into a datasource
172
+ */
173
+ async ingest<T extends Record<string, unknown>>(
174
+ datasourceName: string,
175
+ event: T,
176
+ options: TinybirdApiIngestOptions = {}
177
+ ): Promise<IngestResult> {
178
+ return this.ingestBatch(datasourceName, [event], options);
179
+ }
180
+
181
+ /**
182
+ * Ingest a batch of rows into a datasource
183
+ */
184
+ async ingestBatch<T extends Record<string, unknown>>(
185
+ datasourceName: string,
186
+ events: T[],
187
+ options: TinybirdApiIngestOptions = {}
188
+ ): Promise<IngestResult> {
189
+ if (events.length === 0) {
190
+ return { successful_rows: 0, quarantined_rows: 0 };
191
+ }
192
+
193
+ const url = new URL("/v0/events", `${this.baseUrl}/`);
194
+ url.searchParams.set("name", datasourceName);
195
+
196
+ if (options.wait !== false) {
197
+ url.searchParams.set("wait", "true");
198
+ }
199
+
200
+ const ndjson = events
201
+ .map((event) => JSON.stringify(this.serializeEvent(event)))
202
+ .join("\n");
203
+
204
+ const response = await this.request(url.toString(), {
205
+ method: "POST",
206
+ token: options.token,
207
+ headers: {
208
+ "Content-Type": "application/x-ndjson",
209
+ },
210
+ body: ndjson,
211
+ signal: this.createAbortSignal(options.timeout, options.signal),
212
+ });
213
+
214
+ if (!response.ok) {
215
+ await this.handleErrorResponse(response);
216
+ }
217
+
218
+ return (await response.json()) as IngestResult;
219
+ }
220
+
221
+ /**
222
+ * Execute raw SQL against Tinybird
223
+ */
224
+ async sql<T = unknown>(
225
+ sql: string,
226
+ options: TinybirdApiQueryOptions = {}
227
+ ): Promise<QueryResult<T>> {
228
+ const response = await this.request("/v0/sql", {
229
+ method: "POST",
230
+ token: options.token,
231
+ headers: {
232
+ "Content-Type": "text/plain",
233
+ },
234
+ body: sql,
235
+ signal: this.createAbortSignal(options.timeout, options.signal),
236
+ });
237
+
238
+ if (!response.ok) {
239
+ await this.handleErrorResponse(response);
240
+ }
241
+
242
+ return (await response.json()) as QueryResult<T>;
243
+ }
244
+
245
+ private createAbortSignal(
246
+ timeout?: number,
247
+ existingSignal?: AbortSignal
248
+ ): AbortSignal | undefined {
249
+ const timeoutMs = timeout ?? this.config.timeout ?? DEFAULT_TIMEOUT;
250
+
251
+ if (!timeoutMs && !existingSignal) {
252
+ return undefined;
253
+ }
254
+
255
+ if (!timeoutMs && existingSignal) {
256
+ return existingSignal;
257
+ }
258
+
259
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
260
+
261
+ if (!existingSignal) {
262
+ return timeoutSignal;
263
+ }
264
+
265
+ return AbortSignal.any([timeoutSignal, existingSignal]);
266
+ }
267
+
268
+ private serializeEvent(
269
+ event: Record<string, unknown>
270
+ ): Record<string, unknown> {
271
+ const serialized: Record<string, unknown> = {};
272
+
273
+ for (const [key, value] of Object.entries(event)) {
274
+ serialized[key] = this.serializeValue(value);
275
+ }
276
+
277
+ return serialized;
278
+ }
279
+
280
+ private serializeValue(value: unknown): unknown {
281
+ if (value instanceof Date) {
282
+ return value.toISOString();
283
+ }
284
+
285
+ if (value instanceof Map) {
286
+ return Object.fromEntries(value);
287
+ }
288
+
289
+ if (typeof value === "bigint") {
290
+ return value.toString();
291
+ }
292
+
293
+ if (Array.isArray(value)) {
294
+ return value.map((item) => this.serializeValue(item));
295
+ }
296
+
297
+ if (typeof value === "object" && value !== null) {
298
+ return this.serializeEvent(value as Record<string, unknown>);
299
+ }
300
+
301
+ return value;
302
+ }
303
+
304
+ private async handleErrorResponse(response: Response): Promise<never> {
305
+ const rawBody = await response.text();
306
+ let errorResponse: TinybirdErrorResponse | undefined;
307
+
308
+ try {
309
+ errorResponse = JSON.parse(rawBody) as TinybirdErrorResponse;
310
+ } catch {
311
+ // ignore parse error and keep raw body
312
+ }
313
+
314
+ const message =
315
+ errorResponse?.error ??
316
+ (rawBody
317
+ ? `Request failed with status ${response.status}: ${rawBody}`
318
+ : `Request failed with status ${response.status}`);
319
+
320
+ throw new TinybirdApiError(
321
+ message,
322
+ response.status,
323
+ rawBody || undefined,
324
+ errorResponse
325
+ );
326
+ }
327
+
328
+ private resolveUrl(path: string): string {
329
+ return new URL(path, `${this.baseUrl}/`).toString();
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Create a decoupled Tinybird API wrapper.
335
+ */
336
+ export function createTinybirdApi(
337
+ config: TinybirdApiConfig
338
+ ): TinybirdApi {
339
+ return new TinybirdApi(config);
340
+ }
341
+
342
+ /**
343
+ * Alias for teams that prefer "wrapper" naming.
344
+ */
345
+ export const createTinybirdApiWrapper = createTinybirdApi;
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ parseApiUrl,
4
+ getDashboardUrl,
5
+ getBranchDashboardUrl,
6
+ getLocalDashboardUrl,
7
+ } from "./dashboard.js";
8
+
9
+ describe("parseApiUrl", () => {
10
+ it("parses EU GCP region", () => {
11
+ const result = parseApiUrl("https://api.tinybird.co");
12
+ expect(result).toEqual({ provider: "gcp", region: "europe-west3" });
13
+ });
14
+
15
+ it("parses US East GCP region", () => {
16
+ const result = parseApiUrl("https://api.us-east.tinybird.co");
17
+ expect(result).toEqual({ provider: "gcp", region: "us-east4" });
18
+ });
19
+
20
+ it("parses EU Central AWS region", () => {
21
+ const result = parseApiUrl("https://api.eu-central-1.aws.tinybird.co");
22
+ expect(result).toEqual({ provider: "aws", region: "eu-central-1" });
23
+ });
24
+
25
+ it("parses US East AWS region", () => {
26
+ const result = parseApiUrl("https://api.us-east-1.aws.tinybird.co");
27
+ expect(result).toEqual({ provider: "aws", region: "us-east-1" });
28
+ });
29
+
30
+ it("parses US West AWS region", () => {
31
+ const result = parseApiUrl("https://api.us-west-2.aws.tinybird.co");
32
+ expect(result).toEqual({ provider: "aws", region: "us-west-2" });
33
+ });
34
+
35
+ it("returns null for unknown regions", () => {
36
+ const result = parseApiUrl("https://api.unknown.tinybird.co");
37
+ expect(result).toBeNull();
38
+ });
39
+
40
+ it("returns null for invalid URLs", () => {
41
+ const result = parseApiUrl("not-a-url");
42
+ expect(result).toBeNull();
43
+ });
44
+
45
+ it("returns null for localhost", () => {
46
+ const result = parseApiUrl("http://localhost:7181");
47
+ expect(result).toBeNull();
48
+ });
49
+ });
50
+
51
+ describe("getDashboardUrl", () => {
52
+ it("generates EU GCP workspace URL", () => {
53
+ const result = getDashboardUrl("https://api.tinybird.co", "my_workspace");
54
+ expect(result).toBe("https://cloud.tinybird.co/gcp/europe-west3/my_workspace");
55
+ });
56
+
57
+ it("generates US East GCP workspace URL", () => {
58
+ const result = getDashboardUrl("https://api.us-east.tinybird.co", "my_workspace");
59
+ expect(result).toBe("https://cloud.tinybird.co/gcp/us-east4/my_workspace");
60
+ });
61
+
62
+ it("generates AWS workspace URL", () => {
63
+ const result = getDashboardUrl("https://api.us-west-2.aws.tinybird.co", "my_workspace");
64
+ expect(result).toBe("https://cloud.tinybird.co/aws/us-west-2/my_workspace");
65
+ });
66
+
67
+ it("returns null for unknown regions", () => {
68
+ const result = getDashboardUrl("https://api.unknown.tinybird.co", "my_workspace");
69
+ expect(result).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe("getBranchDashboardUrl", () => {
74
+ it("generates EU GCP branch URL", () => {
75
+ const result = getBranchDashboardUrl("https://api.tinybird.co", "my_workspace", "feature_branch");
76
+ expect(result).toBe("https://cloud.tinybird.co/gcp/europe-west3/my_workspace~feature_branch");
77
+ });
78
+
79
+ it("generates US East GCP branch URL", () => {
80
+ const result = getBranchDashboardUrl("https://api.us-east.tinybird.co", "my_workspace", "feature_branch");
81
+ expect(result).toBe("https://cloud.tinybird.co/gcp/us-east4/my_workspace~feature_branch");
82
+ });
83
+
84
+ it("generates AWS branch URL", () => {
85
+ const result = getBranchDashboardUrl("https://api.us-west-2.aws.tinybird.co", "my_workspace", "my_feature");
86
+ expect(result).toBe("https://cloud.tinybird.co/aws/us-west-2/my_workspace~my_feature");
87
+ });
88
+
89
+ it("returns null for unknown regions", () => {
90
+ const result = getBranchDashboardUrl("https://api.unknown.tinybird.co", "my_workspace", "branch");
91
+ expect(result).toBeNull();
92
+ });
93
+
94
+ it("handles branch names with underscores", () => {
95
+ const result = getBranchDashboardUrl("https://api.tinybird.co", "my_workspace", "feature_with_underscores");
96
+ expect(result).toBe("https://cloud.tinybird.co/gcp/europe-west3/my_workspace~feature_with_underscores");
97
+ });
98
+ });
99
+
100
+ describe("getLocalDashboardUrl", () => {
101
+ it("generates local dashboard URL with default port", () => {
102
+ const result = getLocalDashboardUrl("my_local_workspace");
103
+ expect(result).toBe("https://cloud.tinybird.co/local/7181/my_local_workspace");
104
+ });
105
+
106
+ it("generates local dashboard URL with custom port", () => {
107
+ const result = getLocalDashboardUrl("my_local_workspace", 8080);
108
+ expect(result).toBe("https://cloud.tinybird.co/local/8080/my_local_workspace");
109
+ });
110
+
111
+ it("handles workspace names with underscores", () => {
112
+ const result = getLocalDashboardUrl("dublin_feature_branch");
113
+ expect(result).toBe("https://cloud.tinybird.co/local/7181/dublin_feature_branch");
114
+ });
115
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Tinybird Dashboard URL utilities
3
+ *
4
+ * Generates dashboard links for workspaces and branches
5
+ */
6
+
7
+ /**
8
+ * Region information extracted from API URL
9
+ */
10
+ export interface RegionInfo {
11
+ /** Cloud provider: "gcp" or "aws" */
12
+ provider: string;
13
+ /** Region identifier (e.g., "europe-west3", "us-east4", "us-west-2") */
14
+ region: string;
15
+ }
16
+
17
+ /**
18
+ * Mapping of API hostnames to region information
19
+ *
20
+ * Based on https://www.tinybird.co/docs/api-reference#current-tinybird-regions
21
+ */
22
+ const API_REGION_MAP: Record<string, RegionInfo> = {
23
+ // GCP Regions
24
+ "api.tinybird.co": { provider: "gcp", region: "europe-west3" },
25
+ "api.us-east.tinybird.co": { provider: "gcp", region: "us-east4" },
26
+ // AWS Regions
27
+ "api.eu-central-1.aws.tinybird.co": { provider: "aws", region: "eu-central-1" },
28
+ "api.us-east-1.aws.tinybird.co": { provider: "aws", region: "us-east-1" },
29
+ "api.us-west-2.aws.tinybird.co": { provider: "aws", region: "us-west-2" },
30
+ };
31
+
32
+ /**
33
+ * Parse an API URL to extract region information
34
+ *
35
+ * @param apiUrl - The Tinybird API base URL (e.g., "https://api.tinybird.co")
36
+ * @returns Region info or null if the URL doesn't match a known region
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * parseApiUrl("https://api.tinybird.co")
41
+ * // => { provider: "gcp", region: "europe-west3" }
42
+ *
43
+ * parseApiUrl("https://api.us-west-2.aws.tinybird.co")
44
+ * // => { provider: "aws", region: "us-west-2" }
45
+ * ```
46
+ */
47
+ export function parseApiUrl(apiUrl: string): RegionInfo | null {
48
+ try {
49
+ const url = new URL(apiUrl);
50
+ const hostname = url.hostname;
51
+ return API_REGION_MAP[hostname] ?? null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Generate a Tinybird dashboard URL for a workspace
59
+ *
60
+ * @param apiUrl - The Tinybird API base URL
61
+ * @param workspaceName - The workspace name
62
+ * @returns Dashboard URL or null if the API URL is not recognized
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * getDashboardUrl("https://api.tinybird.co", "my_workspace")
67
+ * // => "https://cloud.tinybird.co/gcp/europe-west3/my_workspace"
68
+ * ```
69
+ */
70
+ export function getDashboardUrl(apiUrl: string, workspaceName: string): string | null {
71
+ const regionInfo = parseApiUrl(apiUrl);
72
+ if (!regionInfo) {
73
+ return null;
74
+ }
75
+
76
+ return `https://cloud.tinybird.co/${regionInfo.provider}/${regionInfo.region}/${workspaceName}`;
77
+ }
78
+
79
+ /**
80
+ * Generate a Tinybird dashboard URL for a branch
81
+ *
82
+ * @param apiUrl - The Tinybird API base URL
83
+ * @param workspaceName - The workspace name
84
+ * @param branchName - The branch name
85
+ * @returns Dashboard URL or null if the API URL is not recognized
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * getBranchDashboardUrl("https://api.tinybird.co", "my_workspace", "feature_branch")
90
+ * // => "https://cloud.tinybird.co/gcp/europe-west3/my_workspace~feature_branch"
91
+ * ```
92
+ */
93
+ export function getBranchDashboardUrl(
94
+ apiUrl: string,
95
+ workspaceName: string,
96
+ branchName: string
97
+ ): string | null {
98
+ const regionInfo = parseApiUrl(apiUrl);
99
+ if (!regionInfo) {
100
+ return null;
101
+ }
102
+
103
+ return `https://cloud.tinybird.co/${regionInfo.provider}/${regionInfo.region}/${workspaceName}~${branchName}`;
104
+ }
105
+
106
+ /**
107
+ * Generate a local Tinybird dashboard URL
108
+ *
109
+ * @param workspaceName - The local workspace name
110
+ * @param port - The local Tinybird port (default: 7181)
111
+ * @returns Local dashboard URL
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * getLocalDashboardUrl("my_local_workspace")
116
+ * // => "https://cloud.tinybird.co/local/7181/my_local_workspace"
117
+ * ```
118
+ */
119
+ export function getLocalDashboardUrl(workspaceName: string, port = 7181): string {
120
+ return `https://cloud.tinybird.co/local/${port}/${workspaceName}`;
121
+ }
@@ -16,6 +16,7 @@ import {
16
16
  removeBranch as removeCachedBranch,
17
17
  listCachedBranches,
18
18
  } from "../branch-store.js";
19
+ import { getBranchDashboardUrl } from "../../api/dashboard.js";
19
20
 
20
21
  /**
21
22
  * Branch command options
@@ -53,6 +54,8 @@ export interface BranchStatusResult {
53
54
  tinybirdBranch?: TinybirdBranch;
54
55
  /** Whether a cached token exists */
55
56
  hasCachedToken: boolean;
57
+ /** Dashboard URL for the branch */
58
+ dashboardUrl?: string;
56
59
  /** Error message if failed */
57
60
  error?: string;
58
61
  }
@@ -135,14 +138,16 @@ export async function runBranchStatus(
135
138
  const tinybirdBranchName = config.tinybirdBranch; // Sanitized name
136
139
  const isMainBranch = config.isMainBranch;
137
140
 
138
- // Fetch the workspace ID from the API
141
+ // Fetch the workspace from the API
139
142
  let workspaceId: string;
143
+ let workspaceName: string;
140
144
  try {
141
145
  const workspace = await getWorkspace({
142
146
  baseUrl: config.baseUrl,
143
147
  token: config.token,
144
148
  });
145
149
  workspaceId = workspace.id;
150
+ workspaceName = workspace.name;
146
151
  } catch (error) {
147
152
  return {
148
153
  success: false,
@@ -154,6 +159,11 @@ export async function runBranchStatus(
154
159
  };
155
160
  }
156
161
 
162
+ // Generate dashboard URL for the branch
163
+ const dashboardUrl = tinybirdBranchName
164
+ ? getBranchDashboardUrl(config.baseUrl, workspaceName, tinybirdBranchName) ?? undefined
165
+ : undefined;
166
+
157
167
  // Check for cached token (use sanitized name)
158
168
  const cachedBranch = tinybirdBranchName ? getBranchToken(workspaceId, tinybirdBranchName) : null;
159
169
  const hasCachedToken = cachedBranch !== null;
@@ -166,6 +176,7 @@ export async function runBranchStatus(
166
176
  tinybirdBranchName,
167
177
  isMainBranch,
168
178
  hasCachedToken,
179
+ dashboardUrl,
169
180
  };
170
181
  }
171
182
 
@@ -186,6 +197,7 @@ export async function runBranchStatus(
186
197
  isMainBranch,
187
198
  tinybirdBranch,
188
199
  hasCachedToken,
200
+ dashboardUrl,
189
201
  };
190
202
  } catch (error) {
191
203
  // If 404, branch doesn't exist yet
@@ -196,6 +208,7 @@ export async function runBranchStatus(
196
208
  tinybirdBranchName,
197
209
  isMainBranch,
198
210
  hasCachedToken,
211
+ dashboardUrl,
199
212
  };
200
213
  }
201
214
 
@@ -205,6 +218,7 @@ export async function runBranchStatus(
205
218
  tinybirdBranchName,
206
219
  isMainBranch,
207
220
  hasCachedToken,
221
+ dashboardUrl,
208
222
  error: (error as Error).message,
209
223
  };
210
224
  }