@tinybirdco/sdk 0.0.35 → 0.0.36

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 (35) hide show
  1. package/dist/api/regions.d.ts +33 -0
  2. package/dist/api/regions.d.ts.map +1 -0
  3. package/dist/api/regions.js +52 -0
  4. package/dist/api/regions.js.map +1 -0
  5. package/dist/api/regions.test.d.ts +2 -0
  6. package/dist/api/regions.test.d.ts.map +1 -0
  7. package/dist/api/regions.test.js +69 -0
  8. package/dist/api/regions.test.js.map +1 -0
  9. package/dist/cli/commands/init.d.ts.map +1 -1
  10. package/dist/cli/commands/init.js +23 -8
  11. package/dist/cli/commands/init.js.map +1 -1
  12. package/dist/cli/commands/init.test.js +7 -0
  13. package/dist/cli/commands/init.test.js.map +1 -1
  14. package/dist/cli/commands/login.d.ts.map +1 -1
  15. package/dist/cli/commands/login.js +13 -3
  16. package/dist/cli/commands/login.js.map +1 -1
  17. package/dist/cli/commands/login.test.js +12 -1
  18. package/dist/cli/commands/login.test.js.map +1 -1
  19. package/dist/cli/region-selector.d.ts +39 -0
  20. package/dist/cli/region-selector.d.ts.map +1 -0
  21. package/dist/cli/region-selector.js +118 -0
  22. package/dist/cli/region-selector.js.map +1 -0
  23. package/dist/cli/region-selector.test.d.ts +2 -0
  24. package/dist/cli/region-selector.test.d.ts.map +1 -0
  25. package/dist/cli/region-selector.test.js +176 -0
  26. package/dist/cli/region-selector.test.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/api/regions.test.ts +89 -0
  29. package/src/api/regions.ts +75 -0
  30. package/src/cli/commands/init.test.ts +8 -0
  31. package/src/cli/commands/init.ts +25 -8
  32. package/src/cli/commands/login.test.ts +13 -1
  33. package/src/cli/commands/login.ts +14 -3
  34. package/src/cli/region-selector.test.ts +227 -0
  35. package/src/cli/region-selector.ts +151 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Region selection utility for CLI commands
3
+ *
4
+ * Provides interactive region selection using @clack/prompts
5
+ */
6
+ /**
7
+ * Result of region selection
8
+ */
9
+ export interface RegionSelectionResult {
10
+ /** Whether selection was successful (not cancelled) */
11
+ success: boolean;
12
+ /** Selected API host URL */
13
+ apiHost?: string;
14
+ /** Selected region name (for display) */
15
+ regionName?: string;
16
+ /** Whether user cancelled */
17
+ cancelled?: boolean;
18
+ }
19
+ /**
20
+ * Prompt user to select a Tinybird region
21
+ *
22
+ * Fetches available regions from the API and presents an interactive selection.
23
+ * Falls back to hardcoded regions if the API call fails.
24
+ *
25
+ * @param defaultApiHost - Optional API host to pre-select in the prompt
26
+ * @returns Selected region info or cancellation result
27
+ */
28
+ export declare function selectRegion(defaultApiHost?: string): Promise<RegionSelectionResult>;
29
+ /**
30
+ * Get API host from config file or prompt for region selection
31
+ *
32
+ * @param configPath - Path to config file (or null if no config)
33
+ * @returns API host URL and source, or null if cancelled
34
+ */
35
+ export declare function getApiHostWithRegionSelection(configPath: string | null): Promise<{
36
+ apiHost: string;
37
+ fromConfig: boolean;
38
+ } | null>;
39
+ //# sourceMappingURL=region-selector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region-selector.d.ts","sourceRoot":"","sources":["../../src/cli/region-selector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2BH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,qBAAqB,CAAC,CA2DhC;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,UAAU,EAAE,MAAM,GAAG,IAAI,GACxB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAyB1D"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Region selection utility for CLI commands
3
+ *
4
+ * Provides interactive region selection using @clack/prompts
5
+ */
6
+ import * as fs from "fs";
7
+ import * as p from "@clack/prompts";
8
+ import { fetchRegions } from "../api/regions.js";
9
+ /**
10
+ * Default fallback regions if API call fails
11
+ */
12
+ const FALLBACK_REGIONS = [
13
+ {
14
+ name: "EU (GCP)",
15
+ api_host: "https://api.europe-west2.gcp.tinybird.co",
16
+ provider: "gcp",
17
+ },
18
+ {
19
+ name: "US East (AWS)",
20
+ api_host: "https://api.us-east.aws.tinybird.co",
21
+ provider: "aws",
22
+ },
23
+ {
24
+ name: "EU (Default)",
25
+ api_host: "https://api.tinybird.co",
26
+ provider: "gcp",
27
+ },
28
+ ];
29
+ /**
30
+ * Prompt user to select a Tinybird region
31
+ *
32
+ * Fetches available regions from the API and presents an interactive selection.
33
+ * Falls back to hardcoded regions if the API call fails.
34
+ *
35
+ * @param defaultApiHost - Optional API host to pre-select in the prompt
36
+ * @returns Selected region info or cancellation result
37
+ */
38
+ export async function selectRegion(defaultApiHost) {
39
+ let regions;
40
+ // Try to fetch regions from API
41
+ try {
42
+ regions = await fetchRegions();
43
+ }
44
+ catch {
45
+ // Fall back to hardcoded regions
46
+ regions = FALLBACK_REGIONS;
47
+ }
48
+ // Ensure we have at least one region
49
+ if (regions.length === 0) {
50
+ regions = FALLBACK_REGIONS;
51
+ }
52
+ // Sort regions: GCP first, then AWS
53
+ regions.sort((a, b) => {
54
+ if (a.provider === b.provider)
55
+ return 0;
56
+ return a.provider === "gcp" ? -1 : 1;
57
+ });
58
+ // Build options for p.select
59
+ const options = regions.map((region) => ({
60
+ value: region.api_host,
61
+ label: `${region.provider} | ${region.name}`,
62
+ hint: region.api_host.replace("https://", ""),
63
+ }));
64
+ // Find initial value if defaultApiHost is provided and matches a region
65
+ // Normalize URLs for comparison (remove trailing slashes, lowercase)
66
+ const normalizeUrl = (url) => url.toLowerCase().replace(/\/+$/, "");
67
+ const initialValue = defaultApiHost
68
+ ? regions.find((r) => normalizeUrl(r.api_host) === normalizeUrl(defaultApiHost))?.api_host
69
+ : undefined;
70
+ const selected = await p.select({
71
+ message: "Select your Tinybird region",
72
+ options,
73
+ initialValue,
74
+ });
75
+ if (p.isCancel(selected)) {
76
+ p.cancel("Operation cancelled");
77
+ return {
78
+ success: false,
79
+ cancelled: true,
80
+ };
81
+ }
82
+ const selectedRegion = regions.find((r) => r.api_host === selected);
83
+ return {
84
+ success: true,
85
+ apiHost: selected,
86
+ regionName: selectedRegion?.name,
87
+ };
88
+ }
89
+ /**
90
+ * Get API host from config file or prompt for region selection
91
+ *
92
+ * @param configPath - Path to config file (or null if no config)
93
+ * @returns API host URL and source, or null if cancelled
94
+ */
95
+ export async function getApiHostWithRegionSelection(configPath) {
96
+ let existingBaseUrl;
97
+ // If we have a JSON config file, try to read baseUrl from it
98
+ if (configPath && configPath.endsWith(".json")) {
99
+ try {
100
+ const content = fs.readFileSync(configPath, "utf-8");
101
+ const config = JSON.parse(content);
102
+ existingBaseUrl = config.baseUrl;
103
+ }
104
+ catch {
105
+ // Ignore errors reading config
106
+ }
107
+ }
108
+ // Prompt for region selection, pre-selecting existing baseUrl if available
109
+ const result = await selectRegion(existingBaseUrl);
110
+ if (!result.success || !result.apiHost) {
111
+ return null;
112
+ }
113
+ return {
114
+ apiHost: result.apiHost,
115
+ fromConfig: false,
116
+ };
117
+ }
118
+ //# sourceMappingURL=region-selector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region-selector.js","sourceRoot":"","sources":["../../src/cli/region-selector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=region-selector.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region-selector.test.d.ts","sourceRoot":"","sources":["../../src/cli/region-selector.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { selectRegion, getApiHostWithRegionSelection } from "./region-selector.js";
6
+ // Mock the regions API
7
+ vi.mock("../api/regions.js", () => ({
8
+ fetchRegions: vi.fn(),
9
+ }));
10
+ // Mock @clack/prompts
11
+ vi.mock("@clack/prompts", () => ({
12
+ select: vi.fn(),
13
+ isCancel: vi.fn(),
14
+ cancel: vi.fn(),
15
+ }));
16
+ import { fetchRegions } from "../api/regions.js";
17
+ import * as p from "@clack/prompts";
18
+ const mockedFetchRegions = vi.mocked(fetchRegions);
19
+ const mockedSelect = vi.mocked(p.select);
20
+ const mockedIsCancel = vi.mocked(p.isCancel);
21
+ describe("selectRegion", () => {
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ mockedIsCancel.mockReturnValue(false);
25
+ });
26
+ it("fetches regions and shows selection prompt with GCP first", async () => {
27
+ mockedFetchRegions.mockResolvedValue([
28
+ { name: "us-east-1", api_host: "https://api.us.tinybird.co", provider: "aws" },
29
+ { name: "europe-west3", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
30
+ ]);
31
+ mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
32
+ const result = await selectRegion();
33
+ expect(result.success).toBe(true);
34
+ expect(result.apiHost).toBe("https://api.eu.tinybird.co");
35
+ expect(result.regionName).toBe("europe-west3");
36
+ // GCP regions should appear first, then AWS
37
+ expect(mockedSelect).toHaveBeenCalledWith({
38
+ message: "Select your Tinybird region",
39
+ options: [
40
+ { value: "https://api.eu.tinybird.co", label: "gcp | europe-west3", hint: "api.eu.tinybird.co" },
41
+ { value: "https://api.us.tinybird.co", label: "aws | us-east-1", hint: "api.us.tinybird.co" },
42
+ ],
43
+ initialValue: undefined,
44
+ });
45
+ });
46
+ it("uses fallback regions when API fails", async () => {
47
+ mockedFetchRegions.mockRejectedValue(new Error("Network error"));
48
+ mockedSelect.mockResolvedValue("https://api.europe-west2.gcp.tinybird.co");
49
+ const result = await selectRegion();
50
+ expect(result.success).toBe(true);
51
+ expect(result.apiHost).toBe("https://api.europe-west2.gcp.tinybird.co");
52
+ expect(mockedSelect).toHaveBeenCalled();
53
+ });
54
+ it("returns cancelled when user cancels", async () => {
55
+ mockedFetchRegions.mockResolvedValue([
56
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
57
+ ]);
58
+ mockedSelect.mockResolvedValue(Symbol("cancel"));
59
+ mockedIsCancel.mockReturnValue(true);
60
+ const result = await selectRegion();
61
+ expect(result.success).toBe(false);
62
+ expect(result.cancelled).toBe(true);
63
+ });
64
+ it("uses fallback regions when API returns empty array", async () => {
65
+ mockedFetchRegions.mockResolvedValue([]);
66
+ mockedSelect.mockResolvedValue("https://api.tinybird.co");
67
+ const result = await selectRegion();
68
+ expect(result.success).toBe(true);
69
+ // Should have used fallback regions
70
+ expect(mockedSelect).toHaveBeenCalled();
71
+ });
72
+ it("pre-selects region when defaultApiHost is provided", async () => {
73
+ mockedFetchRegions.mockResolvedValue([
74
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
75
+ { name: "US", api_host: "https://api.us.tinybird.co", provider: "aws" },
76
+ ]);
77
+ mockedSelect.mockResolvedValue("https://api.us.tinybird.co");
78
+ const result = await selectRegion("https://api.us.tinybird.co");
79
+ expect(result.success).toBe(true);
80
+ expect(mockedSelect).toHaveBeenCalledWith(expect.objectContaining({
81
+ initialValue: "https://api.us.tinybird.co",
82
+ }));
83
+ });
84
+ it("does not set initialValue when defaultApiHost does not match any region", async () => {
85
+ mockedFetchRegions.mockResolvedValue([
86
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
87
+ ]);
88
+ mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
89
+ await selectRegion("https://api.unknown.tinybird.co");
90
+ expect(mockedSelect).toHaveBeenCalledWith(expect.objectContaining({
91
+ initialValue: undefined,
92
+ }));
93
+ });
94
+ });
95
+ describe("getApiHostWithRegionSelection", () => {
96
+ let tempDir;
97
+ beforeEach(() => {
98
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "region-selector-test-"));
99
+ vi.clearAllMocks();
100
+ mockedIsCancel.mockReturnValue(false);
101
+ });
102
+ afterEach(() => {
103
+ try {
104
+ fs.rmSync(tempDir, { recursive: true });
105
+ }
106
+ catch {
107
+ // Ignore cleanup errors
108
+ }
109
+ });
110
+ it("pre-selects baseUrl from config when present", async () => {
111
+ const configPath = path.join(tempDir, "tinybird.config.json");
112
+ fs.writeFileSync(configPath, JSON.stringify({ baseUrl: "https://api.us-east.tinybird.co" }));
113
+ mockedFetchRegions.mockResolvedValue([
114
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
115
+ { name: "US East", api_host: "https://api.us-east.tinybird.co", provider: "aws" },
116
+ ]);
117
+ mockedSelect.mockResolvedValue("https://api.us-east.tinybird.co");
118
+ const result = await getApiHostWithRegionSelection(configPath);
119
+ expect(result).toEqual({
120
+ apiHost: "https://api.us-east.tinybird.co",
121
+ fromConfig: false,
122
+ });
123
+ // Should have called select with initialValue
124
+ expect(mockedSelect).toHaveBeenCalledWith(expect.objectContaining({
125
+ initialValue: "https://api.us-east.tinybird.co",
126
+ }));
127
+ });
128
+ it("prompts for region when config has no baseUrl", async () => {
129
+ const configPath = path.join(tempDir, "tinybird.config.json");
130
+ fs.writeFileSync(configPath, JSON.stringify({ token: "test" }));
131
+ mockedFetchRegions.mockResolvedValue([
132
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
133
+ ]);
134
+ mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
135
+ const result = await getApiHostWithRegionSelection(configPath);
136
+ expect(result).toEqual({
137
+ apiHost: "https://api.eu.tinybird.co",
138
+ fromConfig: false,
139
+ });
140
+ expect(mockedSelect).toHaveBeenCalled();
141
+ });
142
+ it("prompts for region when config file is null", async () => {
143
+ mockedFetchRegions.mockResolvedValue([
144
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
145
+ ]);
146
+ mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
147
+ const result = await getApiHostWithRegionSelection(null);
148
+ expect(result).toEqual({
149
+ apiHost: "https://api.eu.tinybird.co",
150
+ fromConfig: false,
151
+ });
152
+ });
153
+ it("prompts for region when config is not JSON", async () => {
154
+ mockedFetchRegions.mockResolvedValue([
155
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
156
+ ]);
157
+ mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
158
+ const result = await getApiHostWithRegionSelection("/path/to/config.mjs");
159
+ expect(result).toEqual({
160
+ apiHost: "https://api.eu.tinybird.co",
161
+ fromConfig: false,
162
+ });
163
+ });
164
+ it("returns null when user cancels region selection", async () => {
165
+ const configPath = path.join(tempDir, "tinybird.config.json");
166
+ fs.writeFileSync(configPath, JSON.stringify({ token: "test" }));
167
+ mockedFetchRegions.mockResolvedValue([
168
+ { name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
169
+ ]);
170
+ mockedSelect.mockResolvedValue(Symbol("cancel"));
171
+ mockedIsCancel.mockReturnValue(true);
172
+ const result = await getApiHostWithRegionSelection(configPath);
173
+ expect(result).toBeNull();
174
+ });
175
+ });
176
+ //# sourceMappingURL=region-selector.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region-selector.test.js","sourceRoot":"","sources":["../../src/cli/region-selector.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinybirdco/sdk",
3
- "version": "0.0.35",
3
+ "version": "0.0.36",
4
4
  "description": "TypeScript SDK for Tinybird Forward - define datasources and pipes as TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { fetchRegions, RegionsApiError } from "./regions.js";
3
+
4
+ // Mock the fetcher module
5
+ vi.mock("./fetcher.js", () => ({
6
+ tinybirdFetch: vi.fn(),
7
+ }));
8
+
9
+ import { tinybirdFetch } from "./fetcher.js";
10
+
11
+ const mockedFetch = vi.mocked(tinybirdFetch);
12
+
13
+ describe("fetchRegions", () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ it("returns regions from API", async () => {
19
+ mockedFetch.mockResolvedValue({
20
+ ok: true,
21
+ json: () =>
22
+ Promise.resolve({
23
+ regions: [
24
+ {
25
+ name: "EU (GCP)",
26
+ api_host: "https://api.europe-west2.gcp.tinybird.co",
27
+ provider: "gcp",
28
+ },
29
+ {
30
+ name: "US East (AWS)",
31
+ api_host: "https://api.us-east.aws.tinybird.co",
32
+ provider: "aws",
33
+ },
34
+ ],
35
+ }),
36
+ } as Response);
37
+
38
+ const regions = await fetchRegions();
39
+
40
+ expect(regions).toHaveLength(2);
41
+ expect(regions[0].name).toBe("EU (GCP)");
42
+ expect(regions[0].api_host).toBe("https://api.europe-west2.gcp.tinybird.co");
43
+ expect(regions[1].provider).toBe("aws");
44
+ });
45
+
46
+ it("calls the correct endpoint", async () => {
47
+ mockedFetch.mockResolvedValue({
48
+ ok: true,
49
+ json: () => Promise.resolve({ regions: [] }),
50
+ } as Response);
51
+
52
+ await fetchRegions();
53
+
54
+ expect(mockedFetch).toHaveBeenCalledWith(
55
+ "https://api.tinybird.co/v0/regions",
56
+ { method: "GET" }
57
+ );
58
+ });
59
+
60
+ it("throws RegionsApiError on non-ok response", async () => {
61
+ mockedFetch.mockResolvedValue({
62
+ ok: false,
63
+ status: 500,
64
+ statusText: "Internal Server Error",
65
+ text: () => Promise.resolve("Server error"),
66
+ } as Response);
67
+
68
+ await expect(fetchRegions()).rejects.toThrow(RegionsApiError);
69
+ await expect(fetchRegions()).rejects.toThrow("Failed to fetch regions: 500");
70
+ });
71
+
72
+ it("throws RegionsApiError on network error", async () => {
73
+ mockedFetch.mockRejectedValue(new Error("Network error"));
74
+
75
+ await expect(fetchRegions()).rejects.toThrow(RegionsApiError);
76
+ await expect(fetchRegions()).rejects.toThrow("Network error");
77
+ });
78
+
79
+ it("returns empty array when API returns empty regions", async () => {
80
+ mockedFetch.mockResolvedValue({
81
+ ok: true,
82
+ json: () => Promise.resolve({ regions: [] }),
83
+ } as Response);
84
+
85
+ const regions = await fetchRegions();
86
+
87
+ expect(regions).toEqual([]);
88
+ });
89
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tinybird Regions API client
3
+ *
4
+ * Fetches available regions from the Tinybird API
5
+ */
6
+
7
+ import { tinybirdFetch } from "./fetcher.js";
8
+
9
+ /**
10
+ * Default API host used to fetch regions
11
+ * (regions endpoint is available from any host)
12
+ */
13
+ const DEFAULT_HOST = "https://api.tinybird.co";
14
+
15
+ /**
16
+ * Region information from Tinybird API
17
+ */
18
+ export interface TinybirdRegion {
19
+ /** Region name (e.g., "EU (GCP)") */
20
+ name: string;
21
+ /** API host URL (e.g., "https://api.europe-west2.gcp.tinybird.co") */
22
+ api_host: string;
23
+ /** Cloud provider (e.g., "gcp", "aws") */
24
+ provider: string;
25
+ }
26
+
27
+ /**
28
+ * Error thrown by regions API operations
29
+ */
30
+ export class RegionsApiError extends Error {
31
+ constructor(
32
+ message: string,
33
+ public readonly status?: number,
34
+ public readonly body?: unknown
35
+ ) {
36
+ super(message);
37
+ this.name = "RegionsApiError";
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Fetch available Tinybird regions
43
+ *
44
+ * Note: This endpoint doesn't require authentication.
45
+ *
46
+ * @returns Array of available regions
47
+ */
48
+ export async function fetchRegions(): Promise<TinybirdRegion[]> {
49
+ const url = new URL("/v0/regions", DEFAULT_HOST);
50
+
51
+ try {
52
+ const response = await tinybirdFetch(url.toString(), {
53
+ method: "GET",
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const body = await response.text();
58
+ throw new RegionsApiError(
59
+ `Failed to fetch regions: ${response.status} ${response.statusText}`,
60
+ response.status,
61
+ body
62
+ );
63
+ }
64
+
65
+ const data = (await response.json()) as { regions: TinybirdRegion[] };
66
+ return data.regions;
67
+ } catch (error) {
68
+ if (error instanceof RegionsApiError) {
69
+ throw error;
70
+ }
71
+ throw new RegionsApiError(
72
+ `Failed to fetch regions: ${(error as Error).message}`
73
+ );
74
+ }
75
+ }
@@ -9,6 +9,14 @@ vi.mock("../auth.js", () => ({
9
9
  browserLogin: vi.fn().mockResolvedValue({ success: false }),
10
10
  }));
11
11
 
12
+ // Mock the region-selector module to avoid interactive prompts
13
+ vi.mock("../region-selector.js", () => ({
14
+ selectRegion: vi.fn().mockResolvedValue({
15
+ success: true,
16
+ apiHost: "https://api.tinybird.co",
17
+ }),
18
+ }));
19
+
12
20
  describe("Init Command", () => {
13
21
  let tempDir: string;
14
22
 
@@ -17,6 +17,7 @@ import {
17
17
  } from "../config.js";
18
18
  import { browserLogin } from "../auth.js";
19
19
  import { saveTinybirdToken } from "../env.js";
20
+ import { selectRegion } from "../region-selector.js";
20
21
  import { getGitRoot } from "../git.js";
21
22
  import { fetchAllResources } from "../../api/resources.js";
22
23
  import { generateCombinedFile } from "../../codegen/index.js";
@@ -893,7 +894,25 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
893
894
  if (!skipLogin && !hasValidToken(cwd)) {
894
895
  console.log("\nNo authentication found. Starting login flow...\n");
895
896
 
896
- const authResult = await browserLogin();
897
+ // Select region before login (init creates fresh config, so always prompt)
898
+ const regionResult = await selectRegion();
899
+ if (!regionResult.success || !regionResult.apiHost) {
900
+ return {
901
+ success: true,
902
+ created,
903
+ skipped,
904
+ loggedIn: false,
905
+ devMode,
906
+ clientPath: relativeTinybirdDir,
907
+ existingDatafiles:
908
+ existingDatafiles.length > 0 ? existingDatafiles : undefined,
909
+ ciWorkflowCreated,
910
+ cdWorkflowCreated,
911
+ workflowProvider,
912
+ };
913
+ }
914
+
915
+ const authResult = await browserLogin({ apiHost: regionResult.apiHost });
897
916
 
898
917
  if (authResult.success && authResult.token) {
899
918
  // Save token to .env.local
@@ -903,13 +922,11 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
903
922
  created.push(".env.local");
904
923
  }
905
924
 
906
- // If custom base URL, update config file
907
- const baseUrl = authResult.baseUrl ?? "https://api.tinybird.co";
908
- if (baseUrl !== "https://api.tinybird.co") {
909
- const currentConfigPath = findExistingConfigPath(cwd);
910
- if (currentConfigPath && currentConfigPath.endsWith(".json")) {
911
- updateConfig(currentConfigPath, { baseUrl });
912
- }
925
+ // Update config with selected region's baseUrl
926
+ const baseUrl = authResult.baseUrl ?? regionResult.apiHost;
927
+ const currentConfigPath = findExistingConfigPath(cwd);
928
+ if (currentConfigPath && currentConfigPath.endsWith(".json")) {
929
+ updateConfig(currentConfigPath, { baseUrl });
913
930
  }
914
931
 
915
932
  // Generate TypeScript from existing Tinybird resources if requested
@@ -9,10 +9,17 @@ vi.mock("../auth.js", () => ({
9
9
  browserLogin: vi.fn(),
10
10
  }));
11
11
 
12
- // Import the mocked function
12
+ // Mock the region-selector module to avoid interactive prompts
13
+ vi.mock("../region-selector.js", () => ({
14
+ getApiHostWithRegionSelection: vi.fn(),
15
+ }));
16
+
17
+ // Import the mocked functions
13
18
  import { browserLogin } from "../auth.js";
19
+ import { getApiHostWithRegionSelection } from "../region-selector.js";
14
20
 
15
21
  const mockedBrowserLogin = vi.mocked(browserLogin);
22
+ const mockedGetApiHost = vi.mocked(getApiHostWithRegionSelection);
16
23
 
17
24
  describe("Login Command", () => {
18
25
  let tempDir: string;
@@ -20,6 +27,11 @@ describe("Login Command", () => {
20
27
  beforeEach(() => {
21
28
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-login-test-"));
22
29
  vi.clearAllMocks();
30
+ // Default mock for region selection - returns a default apiHost
31
+ mockedGetApiHost.mockResolvedValue({
32
+ apiHost: "https://api.tinybird.co",
33
+ fromConfig: false,
34
+ });
23
35
  });
24
36
 
25
37
  afterEach(() => {