@tinybirdco/sdk 0.0.3 → 0.0.4

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.
@@ -2,11 +2,17 @@
2
2
  * Build command - generates and pushes resources to Tinybird
3
3
  */
4
4
 
5
- import { loadConfig, type ResolvedConfig } from "../config.js";
5
+ import { loadConfig, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js";
6
6
  import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js";
7
7
  import { buildToTinybird, type BuildApiResult } from "../../api/build.js";
8
8
  import { deployToMain } from "../../api/deploy.js";
9
9
  import { getOrCreateBranch } from "../../api/branches.js";
10
+ import {
11
+ getLocalTokens,
12
+ getOrCreateLocalWorkspace,
13
+ getLocalWorkspaceName,
14
+ LocalNotRunningError,
15
+ } from "../../api/local.js";
10
16
 
11
17
  /**
12
18
  * Build command options
@@ -20,6 +26,8 @@ export interface BuildCommandOptions {
20
26
  tokenOverride?: string;
21
27
  /** Use /v1/deploy instead of /v1/build (for main branch) */
22
28
  useDeployEndpoint?: boolean;
29
+ /** Override the devMode from config */
30
+ devModeOverride?: DevMode;
23
31
  }
24
32
 
25
33
  /**
@@ -86,84 +94,142 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
86
94
  };
87
95
  }
88
96
 
89
- // Deploy to Tinybird
90
- // Determine token and endpoint based on git branch
91
- let effectiveToken = options.tokenOverride ?? config.token;
92
- let useDeployEndpoint = options.useDeployEndpoint ?? config.isMainBranch;
93
-
94
- // For feature branches, get or create the Tinybird branch and use its token
97
+ // Determine devMode
98
+ const devMode = options.devModeOverride ?? config.devMode;
95
99
  const debug = !!process.env.TINYBIRD_DEBUG;
100
+
96
101
  if (debug) {
97
- console.log(`[debug] isMainBranch: ${config.isMainBranch}`);
98
- console.log(`[debug] tinybirdBranch: ${config.tinybirdBranch}`);
99
- console.log(`[debug] tokenOverride: ${!!options.tokenOverride}`);
102
+ console.log(`[debug] devMode: ${devMode}`);
100
103
  }
101
- if (!config.isMainBranch && config.tinybirdBranch && !options.tokenOverride) {
102
- if (debug) {
103
- console.log(`[debug] Getting/creating Tinybird branch: ${config.tinybirdBranch}`);
104
- }
104
+
105
+ let deployResult: BuildApiResult;
106
+
107
+ // Handle local mode
108
+ if (devMode === "local") {
105
109
  try {
106
- const tinybirdBranch = await getOrCreateBranch(
110
+ // Get tokens from local container
111
+ if (debug) {
112
+ console.log(`[debug] Getting local tokens from ${LOCAL_BASE_URL}/tokens`);
113
+ }
114
+
115
+ const localTokens = await getLocalTokens();
116
+
117
+ // Get or create workspace based on branch name
118
+ const workspaceName = getLocalWorkspaceName(config.tinybirdBranch, config.cwd);
119
+ if (debug) {
120
+ console.log(`[debug] Using local workspace: ${workspaceName}`);
121
+ }
122
+
123
+ const { workspace, wasCreated } = await getOrCreateLocalWorkspace(localTokens, workspaceName);
124
+ if (debug) {
125
+ console.log(`[debug] Workspace ${wasCreated ? "created" : "found"}: ${workspace.name}`);
126
+ }
127
+
128
+ // Always use /v1/build for local (no deploy endpoint)
129
+ deployResult = await buildToTinybird(
107
130
  {
108
- baseUrl: config.baseUrl,
109
- token: config.token,
131
+ baseUrl: LOCAL_BASE_URL,
132
+ token: workspace.token,
110
133
  },
111
- config.tinybirdBranch
134
+ buildResult.resources
112
135
  );
113
-
114
- if (!tinybirdBranch.token) {
136
+ } catch (error) {
137
+ if (error instanceof LocalNotRunningError) {
115
138
  return {
116
139
  success: false,
117
140
  build: buildResult,
118
- error: `Branch '${config.tinybirdBranch}' was created but no token was returned.`,
141
+ error: error.message,
119
142
  durationMs: Date.now() - startTime,
120
143
  };
121
144
  }
145
+ return {
146
+ success: false,
147
+ build: buildResult,
148
+ error: `Local build failed: ${(error as Error).message}`,
149
+ durationMs: Date.now() - startTime,
150
+ };
151
+ }
152
+ } else {
153
+ // Branch mode (default) - existing logic
154
+ // Deploy to Tinybird
155
+ // Determine token and endpoint based on git branch
156
+ let effectiveToken = options.tokenOverride ?? config.token;
157
+ // Use deploy endpoint if on main branch OR if no branch can be detected
158
+ let useDeployEndpoint = options.useDeployEndpoint ?? (config.isMainBranch || !config.tinybirdBranch);
122
159
 
123
- effectiveToken = tinybirdBranch.token;
124
- useDeployEndpoint = false; // Always use /v1/build for branches
160
+ if (debug) {
161
+ console.log(`[debug] isMainBranch: ${config.isMainBranch}`);
162
+ console.log(`[debug] tinybirdBranch: ${config.tinybirdBranch}`);
163
+ console.log(`[debug] tokenOverride: ${!!options.tokenOverride}`);
164
+ }
165
+
166
+ // For feature branches, get or create the Tinybird branch and use its token
167
+ if (!config.isMainBranch && config.tinybirdBranch && !options.tokenOverride) {
125
168
  if (debug) {
126
- console.log(`[debug] Using branch token for branch: ${config.tinybirdBranch}`);
169
+ console.log(`[debug] Getting/creating Tinybird branch: ${config.tinybirdBranch}`);
170
+ }
171
+ try {
172
+ const tinybirdBranch = await getOrCreateBranch(
173
+ {
174
+ baseUrl: config.baseUrl,
175
+ token: config.token,
176
+ },
177
+ config.tinybirdBranch
178
+ );
179
+
180
+ if (!tinybirdBranch.token) {
181
+ return {
182
+ success: false,
183
+ build: buildResult,
184
+ error: `Branch '${config.tinybirdBranch}' was created but no token was returned.`,
185
+ durationMs: Date.now() - startTime,
186
+ };
187
+ }
188
+
189
+ effectiveToken = tinybirdBranch.token;
190
+ useDeployEndpoint = false; // Always use /v1/build for branches
191
+ if (debug) {
192
+ console.log(`[debug] Using branch token for branch: ${config.tinybirdBranch}`);
193
+ }
194
+ } catch (error) {
195
+ return {
196
+ success: false,
197
+ build: buildResult,
198
+ error: `Failed to get/create branch: ${(error as Error).message}`,
199
+ durationMs: Date.now() - startTime,
200
+ };
201
+ }
202
+ }
203
+
204
+ try {
205
+ // Use /v1/deploy for main branch, /v1/build for feature branches
206
+ if (useDeployEndpoint) {
207
+ deployResult = await deployToMain(
208
+ {
209
+ baseUrl: config.baseUrl,
210
+ token: effectiveToken,
211
+ },
212
+ buildResult.resources
213
+ );
214
+ } else {
215
+ deployResult = await buildToTinybird(
216
+ {
217
+ baseUrl: config.baseUrl,
218
+ token: effectiveToken,
219
+ },
220
+ buildResult.resources
221
+ );
127
222
  }
128
223
  } catch (error) {
129
224
  return {
130
225
  success: false,
131
226
  build: buildResult,
132
- error: `Failed to get/create branch: ${(error as Error).message}`,
227
+ error: `Deploy failed: ${(error as Error).message}`,
133
228
  durationMs: Date.now() - startTime,
134
229
  };
135
230
  }
136
231
  }
137
232
 
138
- let deployResult: BuildApiResult;
139
- try {
140
- // Use /v1/deploy for main branch, /v1/build for feature branches
141
- if (useDeployEndpoint) {
142
- deployResult = await deployToMain(
143
- {
144
- baseUrl: config.baseUrl,
145
- token: effectiveToken,
146
- },
147
- buildResult.resources
148
- );
149
- } else {
150
- deployResult = await buildToTinybird(
151
- {
152
- baseUrl: config.baseUrl,
153
- token: effectiveToken,
154
- },
155
- buildResult.resources
156
- );
157
- }
158
- } catch (error) {
159
- return {
160
- success: false,
161
- build: buildResult,
162
- error: `Deploy failed: ${(error as Error).message}`,
163
- durationMs: Date.now() - startTime,
164
- };
165
- }
166
-
167
233
  if (!deployResult.success) {
168
234
  return {
169
235
  success: false,
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as path from "path";
6
6
  import { watch } from "chokidar";
7
- import { loadConfig, configExists, findConfigFile, hasValidToken, updateConfig, type ResolvedConfig } from "../config.js";
7
+ import { loadConfig, configExists, findConfigFile, hasValidToken, updateConfig, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js";
8
8
  import { runBuild, type BuildCommandResult } from "./build.js";
9
9
  import { getOrCreateBranch, type TinybirdBranch } from "../../api/branches.js";
10
10
  import { browserLogin } from "../auth.js";
@@ -13,6 +13,12 @@ import {
13
13
  validatePipeSchemas,
14
14
  type SchemaValidationResult,
15
15
  } from "../utils/schema-validation.js";
16
+ import {
17
+ getLocalTokens,
18
+ getOrCreateLocalWorkspace,
19
+ getLocalWorkspaceName,
20
+ type LocalWorkspace,
21
+ } from "../../api/local.js";
16
22
 
17
23
  /**
18
24
  * Login result info
@@ -44,6 +50,8 @@ export interface DevCommandOptions {
44
50
  onLoginComplete?: (info: LoginInfo) => void;
45
51
  /** Callback when schema validation completes */
46
52
  onSchemaValidation?: (result: SchemaValidationResult) => void;
53
+ /** Override the devMode from config */
54
+ devModeOverride?: DevMode;
47
55
  }
48
56
 
49
57
  /**
@@ -54,10 +62,14 @@ export interface BranchReadyInfo {
54
62
  gitBranch: string | null;
55
63
  /** Whether we're on the main branch */
56
64
  isMainBranch: boolean;
57
- /** Tinybird branch info (null if on main) */
65
+ /** Tinybird branch info (null if on main or local mode) */
58
66
  tinybirdBranch?: TinybirdBranch;
59
67
  /** Whether the branch was newly created */
60
68
  wasCreated?: boolean;
69
+ /** Whether using local mode */
70
+ isLocal?: boolean;
71
+ /** Local workspace info (only in local mode) */
72
+ localWorkspace?: LocalWorkspace;
61
73
  }
62
74
 
63
75
  /**
@@ -98,8 +110,19 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
98
110
  );
99
111
  }
100
112
 
101
- // Check if authentication is set up, if not trigger login
102
- if (!hasValidToken(cwd)) {
113
+ // Load config first to determine devMode
114
+ let config: ResolvedConfig;
115
+ try {
116
+ config = loadConfig(cwd);
117
+ } catch (error) {
118
+ throw error;
119
+ }
120
+
121
+ // Determine devMode
122
+ const devMode = options.devModeOverride ?? config.devMode;
123
+
124
+ // Check if authentication is set up, if not trigger login (skip for local mode)
125
+ if (devMode !== "local" && !hasValidToken(cwd)) {
103
126
  console.log("No authentication found. Starting login flow...\n");
104
127
 
105
128
  const authResult = await browserLogin();
@@ -134,51 +157,65 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
134
157
  workspaceName: authResult.workspaceName,
135
158
  userEmail: authResult.userEmail,
136
159
  });
137
- }
138
160
 
139
- // Load config (now should have valid token)
140
- let config: ResolvedConfig;
141
- try {
161
+ // Reload config after login
142
162
  config = loadConfig(cwd);
143
- } catch (error) {
144
- throw error;
145
163
  }
146
164
 
147
- // Determine effective token based on git branch
165
+ // Determine effective token and branch info based on devMode
148
166
  let effectiveToken = config.token;
167
+ let effectiveBaseUrl = config.baseUrl;
149
168
  let branchInfo: BranchReadyInfo = {
150
169
  gitBranch: config.gitBranch,
151
170
  isMainBranch: config.isMainBranch,
152
171
  };
153
172
 
154
- // If we're on a feature branch, get or create the Tinybird branch
155
- // Use tinybirdBranch (sanitized name) for Tinybird API, gitBranch for display
156
- if (!config.isMainBranch && config.tinybirdBranch) {
157
- const branchName = config.tinybirdBranch; // Sanitized name for Tinybird
158
-
159
- // Always fetch fresh from API to avoid stale cache issues
160
- const tinybirdBranch = await getOrCreateBranch(
161
- {
162
- baseUrl: config.baseUrl,
163
- token: config.token,
164
- },
165
- branchName
166
- );
167
-
168
- if (!tinybirdBranch.token) {
169
- throw new Error(
170
- `Branch '${branchName}' was created but no token was returned. ` +
171
- `This may be an API issue.`
172
- );
173
- }
173
+ if (devMode === "local") {
174
+ // Local mode: get tokens from local container and set up workspace
175
+ const localTokens = await getLocalTokens();
176
+ const workspaceName = getLocalWorkspaceName(config.tinybirdBranch, config.cwd);
177
+ const { workspace, wasCreated } = await getOrCreateLocalWorkspace(localTokens, workspaceName);
174
178
 
175
- effectiveToken = tinybirdBranch.token;
179
+ effectiveToken = workspace.token;
180
+ effectiveBaseUrl = LOCAL_BASE_URL;
176
181
  branchInfo = {
177
- gitBranch: config.gitBranch, // Original git branch name for display
178
- isMainBranch: false,
179
- tinybirdBranch,
180
- wasCreated: tinybirdBranch.wasCreated ?? false,
182
+ gitBranch: config.gitBranch,
183
+ isMainBranch: false, // Local mode always uses build, not deploy
184
+ isLocal: true,
185
+ localWorkspace: workspace,
186
+ wasCreated,
181
187
  };
188
+ } else {
189
+ // Branch mode: use Tinybird cloud with branches
190
+ // If we're on a feature branch, get or create the Tinybird branch
191
+ // Use tinybirdBranch (sanitized name) for Tinybird API, gitBranch for display
192
+ if (!config.isMainBranch && config.tinybirdBranch) {
193
+ const branchName = config.tinybirdBranch; // Sanitized name for Tinybird
194
+
195
+ // Always fetch fresh from API to avoid stale cache issues
196
+ const tinybirdBranch = await getOrCreateBranch(
197
+ {
198
+ baseUrl: config.baseUrl,
199
+ token: config.token,
200
+ },
201
+ branchName
202
+ );
203
+
204
+ if (!tinybirdBranch.token) {
205
+ throw new Error(
206
+ `Branch '${branchName}' was created but no token was returned. ` +
207
+ `This may be an API issue.`
208
+ );
209
+ }
210
+
211
+ effectiveToken = tinybirdBranch.token;
212
+ branchInfo = {
213
+ gitBranch: config.gitBranch, // Original git branch name for display
214
+ isMainBranch: false,
215
+ tinybirdBranch,
216
+ wasCreated: tinybirdBranch.wasCreated ?? false,
217
+ };
218
+ }
182
219
  }
183
220
 
184
221
  // Notify about branch readiness
@@ -212,7 +249,8 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
212
249
  const result = await runBuild({
213
250
  cwd: config.cwd,
214
251
  tokenOverride: effectiveToken,
215
- useDeployEndpoint: config.isMainBranch,
252
+ useDeployEndpoint: devMode !== "local" && config.isMainBranch,
253
+ devModeOverride: devMode,
216
254
  });
217
255
  options.onBuildComplete?.(result);
218
256
 
@@ -234,7 +272,7 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
234
272
  const validation = await validatePipeSchemas({
235
273
  entities: result.build.entities,
236
274
  pipeNames: changedPipes,
237
- baseUrl: config.baseUrl,
275
+ baseUrl: effectiveBaseUrl,
238
276
  token: effectiveToken,
239
277
  });
240
278
 
@@ -267,6 +267,53 @@ describe("Config", () => {
267
267
 
268
268
  expect(() => loadConfig(tempDir)).toThrow("Failed to parse");
269
269
  });
270
+
271
+ it("defaults devMode to branch when not specified", () => {
272
+ const config = {
273
+ include: ["lib/datasources.ts"],
274
+ token: "test-token",
275
+ };
276
+ fs.writeFileSync(
277
+ path.join(tempDir, "tinybird.json"),
278
+ JSON.stringify(config)
279
+ );
280
+
281
+ const result = loadConfig(tempDir);
282
+
283
+ expect(result.devMode).toBe("branch");
284
+ });
285
+
286
+ it("loads devMode as branch when explicitly set", () => {
287
+ const config = {
288
+ include: ["lib/datasources.ts"],
289
+ token: "test-token",
290
+ devMode: "branch",
291
+ };
292
+ fs.writeFileSync(
293
+ path.join(tempDir, "tinybird.json"),
294
+ JSON.stringify(config)
295
+ );
296
+
297
+ const result = loadConfig(tempDir);
298
+
299
+ expect(result.devMode).toBe("branch");
300
+ });
301
+
302
+ it("loads devMode as local when set", () => {
303
+ const config = {
304
+ include: ["lib/datasources.ts"],
305
+ token: "test-token",
306
+ devMode: "local",
307
+ };
308
+ fs.writeFileSync(
309
+ path.join(tempDir, "tinybird.json"),
310
+ JSON.stringify(config)
311
+ );
312
+
313
+ const result = loadConfig(tempDir);
314
+
315
+ expect(result.devMode).toBe("local");
316
+ });
270
317
  });
271
318
 
272
319
  describe("updateConfig", () => {
package/src/cli/config.ts CHANGED
@@ -6,6 +6,13 @@ import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import { getCurrentGitBranch, isMainBranch, getTinybirdBranchName } from "./git.js";
8
8
 
9
+ /**
10
+ * Development mode options
11
+ * - "branch": Use Tinybird cloud with branches (default)
12
+ * - "local": Use local Tinybird container at localhost:7181
13
+ */
14
+ export type DevMode = "branch" | "local";
15
+
9
16
  /**
10
17
  * Tinybird configuration file structure
11
18
  */
@@ -18,6 +25,8 @@ export interface TinybirdConfig {
18
25
  token: string;
19
26
  /** Tinybird API base URL (optional, defaults to EU region) */
20
27
  baseUrl?: string;
28
+ /** Development mode: "branch" (default) or "local" */
29
+ devMode?: DevMode;
21
30
  }
22
31
 
23
32
  /**
@@ -40,6 +49,8 @@ export interface ResolvedConfig {
40
49
  tinybirdBranch: string | null;
41
50
  /** Whether we're on the main/master branch */
42
51
  isMainBranch: boolean;
52
+ /** Development mode: "branch" or "local" */
53
+ devMode: DevMode;
43
54
  }
44
55
 
45
56
  /**
@@ -47,6 +58,11 @@ export interface ResolvedConfig {
47
58
  */
48
59
  const DEFAULT_BASE_URL = "https://api.tinybird.co";
49
60
 
61
+ /**
62
+ * Local Tinybird base URL
63
+ */
64
+ export const LOCAL_BASE_URL = "http://localhost:7181";
65
+
50
66
  /**
51
67
  * Config file name
52
68
  */
@@ -243,6 +259,9 @@ export function loadConfig(cwd: string = process.cwd()): ResolvedConfig {
243
259
  const gitBranch = getCurrentGitBranch();
244
260
  const tinybirdBranch = getTinybirdBranchName();
245
261
 
262
+ // Resolve devMode (default to "branch")
263
+ const devMode: DevMode = config.devMode ?? "branch";
264
+
246
265
  return {
247
266
  include,
248
267
  token: resolvedToken,
@@ -252,6 +271,7 @@ export function loadConfig(cwd: string = process.cwd()): ResolvedConfig {
252
271
  gitBranch,
253
272
  tinybirdBranch,
254
273
  isMainBranch: isMainBranch(),
274
+ devMode,
255
275
  };
256
276
  }
257
277
 
package/src/cli/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  runBranchStatus,
24
24
  runBranchDelete,
25
25
  } from "./commands/branch.js";
26
+ import type { DevMode } from "./config.js";
26
27
 
27
28
  const __dirname = dirname(fileURLToPath(import.meta.url));
28
29
  const packageJson = JSON.parse(
@@ -137,14 +138,27 @@ function createCli(): Command {
137
138
  .description("Build and push resources to Tinybird")
138
139
  .option("--dry-run", "Generate without pushing to API")
139
140
  .option("--debug", "Show debug output including API requests/responses")
141
+ .option("--local", "Use local Tinybird container")
142
+ .option("--branch", "Use Tinybird cloud with branches")
140
143
  .action(async (options) => {
141
144
  if (options.debug) {
142
145
  process.env.TINYBIRD_DEBUG = "1";
143
146
  }
144
- console.log(`[${formatTime()}] Building...\n`);
147
+
148
+ // Determine devMode override
149
+ let devModeOverride: DevMode | undefined;
150
+ if (options.local) {
151
+ devModeOverride = "local";
152
+ } else if (options.branch) {
153
+ devModeOverride = "branch";
154
+ }
155
+
156
+ const modeLabel = devModeOverride === "local" ? " (local)" : "";
157
+ console.log(`[${formatTime()}] Building${modeLabel}...\n`);
145
158
 
146
159
  const result = await runBuild({
147
160
  dryRun: options.dryRun,
161
+ devModeOverride,
148
162
  });
149
163
 
150
164
  if (!result.success) {
@@ -190,12 +204,23 @@ function createCli(): Command {
190
204
  program
191
205
  .command("dev")
192
206
  .description("Watch for changes and sync with Tinybird")
193
- .action(async () => {
207
+ .option("--local", "Use local Tinybird container")
208
+ .option("--branch", "Use Tinybird cloud with branches")
209
+ .action(async (options) => {
210
+ // Determine devMode override
211
+ let devModeOverride: DevMode | undefined;
212
+ if (options.local) {
213
+ devModeOverride = "local";
214
+ } else if (options.branch) {
215
+ devModeOverride = "branch";
216
+ }
217
+
194
218
  console.log(`tinybird dev v${VERSION}`);
195
219
  console.log("Loading config from tinybird.json...\n");
196
220
 
197
221
  try {
198
222
  const controller = await runDev({
223
+ devModeOverride,
199
224
  onLoginComplete: (info) => {
200
225
  console.log("\nAuthentication successful!");
201
226
  if (info.workspaceName) {
@@ -207,7 +232,18 @@ function createCli(): Command {
207
232
  console.log("");
208
233
  },
209
234
  onBranchReady: (info) => {
210
- if (info.isMainBranch) {
235
+ if (info.isLocal) {
236
+ // Local mode
237
+ const workspaceName = info.localWorkspace?.name ?? "unknown";
238
+ if (info.wasCreated) {
239
+ console.log(`Using local Tinybird container`);
240
+ console.log(`Creating local workspace '${workspaceName}'...`);
241
+ console.log("Workspace created.\n");
242
+ } else {
243
+ console.log(`Using local Tinybird container`);
244
+ console.log(`Using existing local workspace '${workspaceName}'\n`);
245
+ }
246
+ } else if (info.isMainBranch) {
211
247
  console.log("On main branch - deploying to workspace\n");
212
248
  } else if (info.gitBranch) {
213
249
  const tinybirdName = info.tinybirdBranch?.name ?? info.gitBranch;