@tinybirdco/sdk 0.0.29 → 0.0.31

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 (95) hide show
  1. package/README.md +44 -4
  2. package/dist/cli/commands/branch.js +5 -5
  3. package/dist/cli/commands/branch.js.map +1 -1
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.js +2 -2
  6. package/dist/cli/commands/build.js.map +1 -1
  7. package/dist/cli/commands/build.test.d.ts +2 -0
  8. package/dist/cli/commands/build.test.d.ts.map +1 -0
  9. package/dist/cli/commands/build.test.js +266 -0
  10. package/dist/cli/commands/build.test.js.map +1 -0
  11. package/dist/cli/commands/clear.d.ts.map +1 -1
  12. package/dist/cli/commands/clear.js +2 -2
  13. package/dist/cli/commands/clear.js.map +1 -1
  14. package/dist/cli/commands/deploy.js +2 -2
  15. package/dist/cli/commands/deploy.js.map +1 -1
  16. package/dist/cli/commands/dev.js +12 -12
  17. package/dist/cli/commands/dev.js.map +1 -1
  18. package/dist/cli/commands/info.d.ts +104 -0
  19. package/dist/cli/commands/info.d.ts.map +1 -0
  20. package/dist/cli/commands/info.js +140 -0
  21. package/dist/cli/commands/info.js.map +1 -0
  22. package/dist/cli/commands/info.test.d.ts +2 -0
  23. package/dist/cli/commands/info.test.d.ts.map +1 -0
  24. package/dist/cli/commands/info.test.js +336 -0
  25. package/dist/cli/commands/info.test.js.map +1 -0
  26. package/dist/cli/commands/init.d.ts.map +1 -1
  27. package/dist/cli/commands/init.js +44 -26
  28. package/dist/cli/commands/init.js.map +1 -1
  29. package/dist/cli/commands/init.test.js +44 -25
  30. package/dist/cli/commands/init.test.js.map +1 -1
  31. package/dist/cli/commands/login.d.ts.map +1 -1
  32. package/dist/cli/commands/login.js +7 -6
  33. package/dist/cli/commands/login.js.map +1 -1
  34. package/dist/cli/commands/login.test.js +1 -1
  35. package/dist/cli/commands/login.test.js.map +1 -1
  36. package/dist/cli/commands/open-dashboard.d.ts +39 -0
  37. package/dist/cli/commands/open-dashboard.d.ts.map +1 -0
  38. package/dist/cli/commands/open-dashboard.js +127 -0
  39. package/dist/cli/commands/open-dashboard.js.map +1 -0
  40. package/dist/cli/commands/open-dashboard.test.d.ts +2 -0
  41. package/dist/cli/commands/open-dashboard.test.d.ts.map +1 -0
  42. package/dist/cli/commands/open-dashboard.test.js +373 -0
  43. package/dist/cli/commands/open-dashboard.test.js.map +1 -0
  44. package/dist/cli/commands/preview.d.ts.map +1 -1
  45. package/dist/cli/commands/preview.js +2 -2
  46. package/dist/cli/commands/preview.js.map +1 -1
  47. package/dist/cli/config-loader.d.ts +18 -0
  48. package/dist/cli/config-loader.d.ts.map +1 -0
  49. package/dist/cli/config-loader.js +57 -0
  50. package/dist/cli/config-loader.js.map +1 -0
  51. package/dist/cli/config-types.d.ts +28 -0
  52. package/dist/cli/config-types.d.ts.map +1 -0
  53. package/dist/cli/config-types.js +8 -0
  54. package/dist/cli/config-types.js.map +1 -0
  55. package/dist/cli/config.d.ts +63 -29
  56. package/dist/cli/config.d.ts.map +1 -1
  57. package/dist/cli/config.js +139 -43
  58. package/dist/cli/config.js.map +1 -1
  59. package/dist/cli/config.test.js +73 -9
  60. package/dist/cli/config.test.js.map +1 -1
  61. package/dist/cli/index.js +64 -0
  62. package/dist/cli/index.js.map +1 -1
  63. package/dist/cli/output.d.ts +40 -0
  64. package/dist/cli/output.d.ts.map +1 -1
  65. package/dist/cli/output.js +74 -0
  66. package/dist/cli/output.js.map +1 -1
  67. package/dist/client/base.d.ts.map +1 -1
  68. package/dist/client/base.js +21 -9
  69. package/dist/client/base.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/package.json +5 -1
  73. package/src/cli/commands/branch.ts +5 -5
  74. package/src/cli/commands/build.test.ts +310 -0
  75. package/src/cli/commands/build.ts +2 -2
  76. package/src/cli/commands/clear.ts +2 -2
  77. package/src/cli/commands/deploy.ts +2 -2
  78. package/src/cli/commands/dev.ts +12 -12
  79. package/src/cli/commands/info.test.ts +398 -0
  80. package/src/cli/commands/info.ts +253 -0
  81. package/src/cli/commands/init.test.ts +53 -37
  82. package/src/cli/commands/init.ts +49 -30
  83. package/src/cli/commands/login.test.ts +1 -1
  84. package/src/cli/commands/login.ts +7 -6
  85. package/src/cli/commands/open-dashboard.test.ts +472 -0
  86. package/src/cli/commands/open-dashboard.ts +177 -0
  87. package/src/cli/commands/preview.ts +2 -2
  88. package/src/cli/config-loader.ts +87 -0
  89. package/src/cli/config-types.ts +29 -0
  90. package/src/cli/config.test.ts +95 -8
  91. package/src/cli/config.ts +179 -70
  92. package/src/cli/index.ts +73 -0
  93. package/src/cli/output.ts +111 -0
  94. package/src/client/base.ts +33 -16
  95. package/src/index.ts +4 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Universal config file loader
3
+ * Supports .json, .cjs, and .mjs files
4
+ */
5
+
6
+ import * as fs from "node:fs/promises";
7
+ import * as path from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+
10
+ export type MaybePromise<T> = T | Promise<T>;
11
+
12
+ export type LoadedConfig<T> = {
13
+ config: T;
14
+ filepath: string;
15
+ };
16
+
17
+ export type LoadConfigOptions = {
18
+ cwd?: string;
19
+ };
20
+
21
+ function isObject(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ async function readJsonFile<T>(filepath: string): Promise<T> {
26
+ const raw = await fs.readFile(filepath, "utf8");
27
+ return JSON.parse(raw) as T;
28
+ }
29
+
30
+ /**
31
+ * Resolve the config export from a module
32
+ * Supports default export, module.exports, and function configs
33
+ */
34
+ async function resolveConfigExport(mod: unknown): Promise<unknown> {
35
+ const moduleObj = mod as Record<string, unknown>;
36
+ const exported = moduleObj?.default ?? mod;
37
+
38
+ // Allow config as function (sync/async)
39
+ if (typeof exported === "function") {
40
+ return await (exported as () => MaybePromise<unknown>)();
41
+ }
42
+ return exported;
43
+ }
44
+
45
+ /**
46
+ * Load a config file from disk
47
+ * Supports .json, .cjs, and .mjs files
48
+ */
49
+ export async function loadConfigFile<T = unknown>(
50
+ configPath: string,
51
+ opts: LoadConfigOptions = {}
52
+ ): Promise<LoadedConfig<T>> {
53
+ const cwd = opts.cwd ?? process.cwd();
54
+ const filepath = path.isAbsolute(configPath)
55
+ ? configPath
56
+ : path.resolve(cwd, configPath);
57
+
58
+ const ext = path.extname(filepath).toLowerCase();
59
+
60
+ if (ext === ".json") {
61
+ const config = await readJsonFile<T>(filepath);
62
+ return { config, filepath };
63
+ }
64
+
65
+ if (ext === ".mjs" || ext === ".cjs") {
66
+ // Load JS modules via runtime import for bundler compatibility
67
+ const url = pathToFileURL(filepath).href;
68
+ const mod = await import(
69
+ /* webpackIgnore: true */
70
+ /* @vite-ignore */
71
+ url
72
+ );
73
+ const config = await resolveConfigExport(mod);
74
+
75
+ if (!isObject(config)) {
76
+ throw new Error(
77
+ `Config in ${filepath} must export an object (or a function returning an object).`
78
+ );
79
+ }
80
+
81
+ return { config: config as T, filepath };
82
+ }
83
+
84
+ throw new Error(
85
+ `Unsupported config extension "${ext}". Use .json, .mjs, or .cjs`
86
+ );
87
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Configuration types for tinybird.config.{ts,js,json}
3
+ *
4
+ * This file is separate from config.ts to avoid pulling in esbuild
5
+ * when these types are imported by client code.
6
+ */
7
+
8
+ /**
9
+ * Development mode options
10
+ * - "branch": Use Tinybird cloud with branches (default)
11
+ * - "local": Use local Tinybird container at localhost:7181
12
+ */
13
+ export type DevMode = "branch" | "local";
14
+
15
+ /**
16
+ * Tinybird configuration file structure
17
+ */
18
+ export interface TinybirdConfig {
19
+ /** Array of TypeScript files to scan for datasources and pipes */
20
+ include?: string[];
21
+ /** @deprecated Use `include` instead. Path to the TypeScript schema entry point */
22
+ schema?: string;
23
+ /** API token (supports ${ENV_VAR} interpolation) */
24
+ token: string;
25
+ /** Tinybird API base URL (optional, defaults to EU region) */
26
+ baseUrl?: string;
27
+ /** Development mode: "branch" (default) or "local" */
28
+ devMode?: DevMode;
29
+ }
@@ -8,6 +8,7 @@ import {
8
8
  getRelativeTinybirdDir,
9
9
  findConfigFile,
10
10
  loadConfig,
11
+ loadConfigAsync,
11
12
  getConfigPath,
12
13
  configExists,
13
14
  updateConfig,
@@ -73,20 +74,60 @@ describe("Config", () => {
73
74
  });
74
75
 
75
76
  describe("findConfigFile", () => {
76
- it("finds tinybird.json in current directory", () => {
77
+ it("finds tinybird.config.json in current directory", () => {
78
+ const configPath = path.join(tempDir, "tinybird.config.json");
79
+ fs.writeFileSync(configPath, "{}");
80
+
81
+ const result = findConfigFile(tempDir);
82
+ expect(result).not.toBe(null);
83
+ expect(result?.path).toBe(configPath);
84
+ expect(result?.type).toBe("tinybird.config.json");
85
+ });
86
+
87
+ it("finds tinybird.json in current directory (legacy)", () => {
77
88
  const configPath = path.join(tempDir, "tinybird.json");
78
89
  fs.writeFileSync(configPath, "{}");
79
90
 
80
- expect(findConfigFile(tempDir)).toBe(configPath);
91
+ const result = findConfigFile(tempDir);
92
+ expect(result).not.toBe(null);
93
+ expect(result?.path).toBe(configPath);
94
+ expect(result?.type).toBe("tinybird.json");
81
95
  });
82
96
 
83
- it("finds tinybird.json in parent directory", () => {
97
+ it("finds tinybird.config.json in parent directory", () => {
84
98
  const nestedDir = path.join(tempDir, "src", "app");
85
99
  fs.mkdirSync(nestedDir, { recursive: true });
86
- const configPath = path.join(tempDir, "tinybird.json");
100
+ const configPath = path.join(tempDir, "tinybird.config.json");
87
101
  fs.writeFileSync(configPath, "{}");
88
102
 
89
- expect(findConfigFile(nestedDir)).toBe(configPath);
103
+ const result = findConfigFile(nestedDir);
104
+ expect(result).not.toBe(null);
105
+ expect(result?.path).toBe(configPath);
106
+ expect(result?.type).toBe("tinybird.config.json");
107
+ });
108
+
109
+ it("prioritizes tinybird.config.json over tinybird.json", () => {
110
+ const newConfig = path.join(tempDir, "tinybird.config.json");
111
+ const legacyConfig = path.join(tempDir, "tinybird.json");
112
+ fs.writeFileSync(newConfig, "{}");
113
+ fs.writeFileSync(legacyConfig, "{}");
114
+
115
+ const result = findConfigFile(tempDir);
116
+ expect(result).not.toBe(null);
117
+ expect(result?.path).toBe(newConfig);
118
+ expect(result?.type).toBe("tinybird.config.json");
119
+ });
120
+
121
+ it("prioritizes tinybird.config.mjs over tinybird.config.cjs", () => {
122
+ const mjsConfig = path.join(tempDir, "tinybird.config.mjs");
123
+ const cjsConfig = path.join(tempDir, "tinybird.config.cjs");
124
+ fs.writeFileSync(mjsConfig, "export default {};");
125
+ fs.writeFileSync(cjsConfig, "module.exports = {};");
126
+
127
+ const result = findConfigFile(tempDir);
128
+ expect(result).not.toBe(null);
129
+ expect(result?.path).toBe(mjsConfig);
130
+ expect(result?.type).toBe("tinybird.config.mjs");
90
131
  });
91
132
 
92
133
  it("returns null when no config file exists", () => {
@@ -95,8 +136,10 @@ describe("Config", () => {
95
136
  });
96
137
 
97
138
  describe("getConfigPath", () => {
98
- it("returns path to tinybird.json in directory", () => {
99
- expect(getConfigPath(tempDir)).toBe(path.join(tempDir, "tinybird.json"));
139
+ it("returns path to tinybird.config.json (new default) in directory", () => {
140
+ expect(getConfigPath(tempDir)).toBe(
141
+ path.join(tempDir, "tinybird.config.json")
142
+ );
100
143
  });
101
144
  });
102
145
 
@@ -123,7 +166,7 @@ describe("Config", () => {
123
166
  });
124
167
 
125
168
  it("throws error when no config file exists", () => {
126
- expect(() => loadConfig(tempDir)).toThrow("Could not find tinybird.json");
169
+ expect(() => loadConfig(tempDir)).toThrow("Could not find config file");
127
170
  });
128
171
 
129
172
  it("loads config with include array", () => {
@@ -286,6 +329,50 @@ describe("Config", () => {
286
329
  });
287
330
  });
288
331
 
332
+ describe("loadConfigAsync", () => {
333
+ beforeEach(() => {
334
+ // Mock git functions to avoid git dependency in tests
335
+ vi.mock("./git.js", () => ({
336
+ getCurrentGitBranch: () => "main",
337
+ isMainBranch: () => true,
338
+ getTinybirdBranchName: () => null,
339
+ }));
340
+ });
341
+
342
+ it("loads tinybird.config.mjs", async () => {
343
+ fs.writeFileSync(
344
+ path.join(tempDir, "tinybird.config.mjs"),
345
+ `export default {
346
+ include: ["lib/datasources.ts"],
347
+ token: "test-token"
348
+ };`
349
+ );
350
+
351
+ const result = await loadConfigAsync(tempDir);
352
+
353
+ expect(result.include).toEqual(["lib/datasources.ts"]);
354
+ expect(result.token).toBe("test-token");
355
+ expect(result.baseUrl).toBe("https://api.tinybird.co");
356
+ });
357
+
358
+ it("loads tinybird.config.cjs", async () => {
359
+ fs.writeFileSync(
360
+ path.join(tempDir, "tinybird.config.cjs"),
361
+ `module.exports = {
362
+ include: ["lib/datasources.ts"],
363
+ token: "test-token"
364
+ };`
365
+ );
366
+
367
+ const result = await loadConfigAsync(tempDir);
368
+
369
+ expect(result.include).toEqual(["lib/datasources.ts"]);
370
+ expect(result.token).toBe("test-token");
371
+ expect(result.baseUrl).toBe("https://api.tinybird.co");
372
+ });
373
+
374
+ });
375
+
289
376
  describe("updateConfig", () => {
290
377
  it("updates existing config file", () => {
291
378
  const configPath = path.join(tempDir, "tinybird.json");
package/src/cli/config.ts CHANGED
@@ -6,28 +6,9 @@ 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
-
16
- /**
17
- * Tinybird configuration file structure
18
- */
19
- export interface TinybirdConfig {
20
- /** Array of TypeScript files to scan for datasources and pipes */
21
- include?: string[];
22
- /** @deprecated Use `include` instead. Path to the TypeScript schema entry point */
23
- schema?: string;
24
- /** API token (supports ${ENV_VAR} interpolation) */
25
- token: string;
26
- /** Tinybird API base URL (optional, defaults to EU region) */
27
- baseUrl?: string;
28
- /** Development mode: "branch" (default) or "local" */
29
- devMode?: DevMode;
30
- }
9
+ // Re-export types from config-types.ts (separate file to avoid bundling esbuild)
10
+ export type { DevMode, TinybirdConfig } from "./config-types.js";
11
+ import type { DevMode, TinybirdConfig } from "./config-types.js";
31
12
 
32
13
  /**
33
14
  * Resolved configuration with all values expanded
@@ -64,9 +45,25 @@ const DEFAULT_BASE_URL = "https://api.tinybird.co";
64
45
  export const LOCAL_BASE_URL = "http://localhost:7181";
65
46
 
66
47
  /**
67
- * Config file name
48
+ * Config file names in priority order
49
+ * - tinybird.config.mjs: ESM config with dynamic logic
50
+ * - tinybird.config.cjs: CommonJS config with dynamic logic
51
+ * - tinybird.config.json: Standard JSON config (default for new projects)
52
+ * - tinybird.json: Legacy JSON config (backward compatible)
68
53
  */
69
- const CONFIG_FILE = "tinybird.json";
54
+ const CONFIG_FILES = [
55
+ "tinybird.config.mjs",
56
+ "tinybird.config.cjs",
57
+ "tinybird.config.json",
58
+ "tinybird.json",
59
+ ] as const;
60
+
61
+ type ConfigFileType = (typeof CONFIG_FILES)[number];
62
+
63
+ /**
64
+ * Default config file name for new projects
65
+ */
66
+ const DEFAULT_CONFIG_FILE = "tinybird.config.json";
70
67
 
71
68
  /**
72
69
  * Tinybird file path within lib folder
@@ -135,19 +132,33 @@ function interpolateEnvVars(value: string): string {
135
132
  });
136
133
  }
137
134
 
135
+ /**
136
+ * Result of finding a config file
137
+ */
138
+ export interface ConfigFileResult {
139
+ /** Full path to the config file */
140
+ path: string;
141
+ /** Type of config file found */
142
+ type: ConfigFileType;
143
+ }
144
+
138
145
  /**
139
146
  * Find the config file by walking up the directory tree
147
+ * Checks for all supported config file names in priority order
140
148
  *
141
149
  * @param startDir - Directory to start searching from
142
- * @returns Path to the config file, or null if not found
150
+ * @returns Path and type of the config file, or null if not found
143
151
  */
144
- export function findConfigFile(startDir: string): string | null {
152
+ export function findConfigFile(startDir: string): ConfigFileResult | null {
145
153
  let currentDir = startDir;
146
154
 
147
155
  while (true) {
148
- const configPath = path.join(currentDir, CONFIG_FILE);
149
- if (fs.existsSync(configPath)) {
150
- return configPath;
156
+ // Check each config file type in priority order
157
+ for (const configFile of CONFIG_FILES) {
158
+ const configPath = path.join(currentDir, configFile);
159
+ if (fs.existsSync(configPath)) {
160
+ return { path: configPath, type: configFile };
161
+ }
151
162
  }
152
163
 
153
164
  const parentDir = path.dirname(currentDir);
@@ -159,42 +170,13 @@ export function findConfigFile(startDir: string): string | null {
159
170
  }
160
171
  }
161
172
 
173
+ // Import the universal config loader
174
+ import { loadConfigFile } from "./config-loader.js";
175
+
162
176
  /**
163
- * Load and resolve the tinybird.json configuration
164
- *
165
- * @param cwd - Working directory to start searching from (defaults to process.cwd())
166
- * @returns Resolved configuration
167
- *
168
- * @example
169
- * ```ts
170
- * const config = loadConfig();
171
- * console.log(config.schema); // 'lib/tinybird.ts' or 'src/lib/tinybird.ts'
172
- * console.log(config.token); // 'p.xxx' (resolved from ${TINYBIRD_TOKEN})
173
- * ```
177
+ * Resolve a TinybirdConfig to a ResolvedConfig
174
178
  */
175
- export function loadConfig(cwd: string = process.cwd()): ResolvedConfig {
176
- const configPath = findConfigFile(cwd);
177
-
178
- if (!configPath) {
179
- throw new Error(
180
- `Could not find ${CONFIG_FILE}. Run 'npx tinybird init' to create one.`
181
- );
182
- }
183
-
184
- let rawContent: string;
185
- try {
186
- rawContent = fs.readFileSync(configPath, "utf-8");
187
- } catch (error) {
188
- throw new Error(`Failed to read ${configPath}: ${(error as Error).message}`);
189
- }
190
-
191
- let config: TinybirdConfig;
192
- try {
193
- config = JSON.parse(rawContent) as TinybirdConfig;
194
- } catch (error) {
195
- throw new Error(`Failed to parse ${configPath}: ${(error as Error).message}`);
196
- }
197
-
179
+ function resolveConfig(config: TinybirdConfig, configPath: string): ResolvedConfig {
198
180
  // Validate required fields - need either include or schema
199
181
  if (!config.include && !config.schema) {
200
182
  throw new Error(`Missing 'include' field in ${configPath}. Add an array of files to scan for datasources and pipes.`);
@@ -262,28 +244,140 @@ export function loadConfig(cwd: string = process.cwd()): ResolvedConfig {
262
244
  }
263
245
 
264
246
  /**
265
- * Check if a config file exists in the given directory
247
+ * Load and resolve the Tinybird configuration
248
+ *
249
+ * Supports the following config file formats (in priority order):
250
+ * - tinybird.config.mjs: ESM config with dynamic logic
251
+ * - tinybird.config.cjs: CommonJS config with dynamic logic
252
+ * - tinybird.config.json: Standard JSON config
253
+ * - tinybird.json: Legacy JSON config (backward compatible)
254
+ *
255
+ * @param cwd - Working directory to start searching from (defaults to process.cwd())
256
+ * @returns Resolved configuration
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const config = loadConfig();
261
+ * console.log(config.include); // ['lib/tinybird.ts']
262
+ * console.log(config.token); // 'p.xxx' (resolved from ${TINYBIRD_TOKEN})
263
+ * ```
264
+ */
265
+ export function loadConfig(cwd: string = process.cwd()): ResolvedConfig {
266
+ const configResult = findConfigFile(cwd);
267
+
268
+ if (!configResult) {
269
+ throw new Error(
270
+ `Could not find config file. Run 'npx tinybird init' to create one.\n` +
271
+ `Searched for: ${CONFIG_FILES.join(", ")}`
272
+ );
273
+ }
274
+
275
+ const { path: configPath, type: configType } = configResult;
276
+
277
+ // JSON files can be loaded synchronously
278
+ if (configType === "tinybird.config.json" || configType === "tinybird.json") {
279
+ let rawContent: string;
280
+ try {
281
+ rawContent = fs.readFileSync(configPath, "utf-8");
282
+ } catch (error) {
283
+ throw new Error(`Failed to read ${configPath}: ${(error as Error).message}`);
284
+ }
285
+
286
+ let config: TinybirdConfig;
287
+ try {
288
+ config = JSON.parse(rawContent) as TinybirdConfig;
289
+ } catch (error) {
290
+ throw new Error(`Failed to parse ${configPath}: ${(error as Error).message}`);
291
+ }
292
+
293
+ return resolveConfig(config, configPath);
294
+ }
295
+
296
+ // For JS files, we need to throw an error asking to use the async version
297
+ throw new Error(
298
+ `Config file ${configPath} is a JavaScript file. ` +
299
+ `Use loadConfigAsync() instead of loadConfig() to load .mjs/.cjs config files.`
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Load and resolve the Tinybird configuration (async version)
305
+ *
306
+ * This async version supports all config file formats including JS files
307
+ * that may contain dynamic logic.
308
+ *
309
+ * @param cwd - Working directory to start searching from (defaults to process.cwd())
310
+ * @returns Promise resolving to the configuration
311
+ */
312
+ export async function loadConfigAsync(cwd: string = process.cwd()): Promise<ResolvedConfig> {
313
+ const configResult = findConfigFile(cwd);
314
+
315
+ if (!configResult) {
316
+ throw new Error(
317
+ `Could not find config file. Run 'npx tinybird init' to create one.\n` +
318
+ `Searched for: ${CONFIG_FILES.join(", ")}`
319
+ );
320
+ }
321
+
322
+ const { path: configPath } = configResult;
323
+
324
+ // Use the universal config loader for all file types
325
+ const { config } = await loadConfigFile<TinybirdConfig>(configPath);
326
+
327
+ return resolveConfig(config, configPath);
328
+ }
329
+
330
+ /**
331
+ * Check if a config file exists in the given directory or its parents
266
332
  */
267
333
  export function configExists(cwd: string = process.cwd()): boolean {
268
334
  return findConfigFile(cwd) !== null;
269
335
  }
270
336
 
337
+ /**
338
+ * Get the path to an existing config file, or the default path for a new config
339
+ * This is useful for the init command which needs to either update an existing config
340
+ * or create a new one with the new default name
341
+ */
342
+ export function getExistingOrNewConfigPath(cwd: string = process.cwd()): string {
343
+ const existing = findExistingConfigPath(cwd);
344
+ return existing ?? path.join(cwd, DEFAULT_CONFIG_FILE);
345
+ }
346
+
271
347
  /**
272
348
  * Get the expected config file path for a directory
349
+ * Returns the path for the default config file name (tinybird.config.json)
273
350
  */
274
351
  export function getConfigPath(cwd: string = process.cwd()): string {
275
- return path.join(cwd, CONFIG_FILE);
352
+ return path.join(cwd, DEFAULT_CONFIG_FILE);
353
+ }
354
+
355
+ /**
356
+ * Find an existing config file in a directory
357
+ * Returns the path to the first matching config file, or null if none found
358
+ */
359
+ export function findExistingConfigPath(cwd: string = process.cwd()): string | null {
360
+ for (const configFile of CONFIG_FILES) {
361
+ const configPath = path.join(cwd, configFile);
362
+ if (fs.existsSync(configPath)) {
363
+ return configPath;
364
+ }
365
+ }
366
+ return null;
276
367
  }
277
368
 
278
369
  /**
279
- * Update specific fields in tinybird.json
370
+ * Update specific fields in a JSON config file
371
+ *
372
+ * Note: Only works with JSON config files (.json). For JS config files,
373
+ * the user needs to update them manually.
280
374
  *
281
375
  * Throws an error if the config file doesn't exist to prevent creating
282
376
  * partial config files that would break loadConfig.
283
377
  *
284
378
  * @param configPath - Path to the config file
285
379
  * @param updates - Fields to update
286
- * @throws Error if config file doesn't exist
380
+ * @throws Error if config file doesn't exist or is not a JSON file
287
381
  */
288
382
  export function updateConfig(
289
383
  configPath: string,
@@ -293,6 +387,12 @@ export function updateConfig(
293
387
  throw new Error(`Config not found at ${configPath}`);
294
388
  }
295
389
 
390
+ if (!configPath.endsWith(".json")) {
391
+ throw new Error(
392
+ `Cannot update ${configPath}. Only JSON config files can be updated programmatically.`
393
+ );
394
+ }
395
+
296
396
  const content = fs.readFileSync(configPath, "utf-8");
297
397
  const config = JSON.parse(content) as TinybirdConfig;
298
398
 
@@ -305,17 +405,26 @@ export function updateConfig(
305
405
  /**
306
406
  * Check if a valid token is configured (either in file or via env var)
307
407
  *
408
+ * Note: For JS config files, this only works if the token is a static value
409
+ * or environment variable reference in the file.
410
+ *
308
411
  * @param cwd - Working directory to search from
309
412
  * @returns true if a valid token exists
310
413
  */
311
414
  export function hasValidToken(cwd: string = process.cwd()): boolean {
312
415
  try {
313
- const configPath = findConfigFile(cwd);
314
- if (!configPath) {
416
+ const configResult = findConfigFile(cwd);
417
+ if (!configResult) {
315
418
  return false;
316
419
  }
317
420
 
318
- const content = fs.readFileSync(configPath, "utf-8");
421
+ // For JS files, we can't easily check without loading them
422
+ // Return true and let loadConfig handle validation
423
+ if (!configResult.path.endsWith(".json")) {
424
+ return true;
425
+ }
426
+
427
+ const content = fs.readFileSync(configResult.path, "utf-8");
319
428
  const config = JSON.parse(content) as TinybirdConfig;
320
429
 
321
430
  if (!config.token) {
package/src/cli/index.ts CHANGED
@@ -27,6 +27,8 @@ import {
27
27
  runBranchDelete,
28
28
  } from "./commands/branch.js";
29
29
  import { runClear } from "./commands/clear.js";
30
+ import { runInfo } from "./commands/info.js";
31
+ import { runOpenDashboard, type Environment } from "./commands/open-dashboard.js";
30
32
  import {
31
33
  detectPackageManagerInstallCmd,
32
34
  detectPackageManagerRunCmd,
@@ -170,6 +172,74 @@ function createCli(): Command {
170
172
  }
171
173
  });
172
174
 
175
+ // Info command
176
+ program
177
+ .command("info")
178
+ .description("Show information about the current project and workspace")
179
+ .option("--json", "Output as JSON")
180
+ .action(async (options) => {
181
+ const result = await runInfo({ json: options.json });
182
+
183
+ if (!result.success) {
184
+ console.error(`Error: ${result.error}`);
185
+ process.exit(1);
186
+ }
187
+
188
+ if (options.json) {
189
+ // JSON output
190
+ const jsonOutput = {
191
+ cloud: result.cloud,
192
+ local: result.local,
193
+ branch: result.branch,
194
+ project: result.project,
195
+ branches: result.branches,
196
+ };
197
+ console.log(JSON.stringify(jsonOutput, null, 2));
198
+ } else {
199
+ // Human-readable output
200
+ output.showInfo({
201
+ cloud: result.cloud,
202
+ local: result.local,
203
+ branch: result.branch,
204
+ project: result.project,
205
+ });
206
+ }
207
+ });
208
+
209
+ // Open command
210
+ program
211
+ .command("open")
212
+ .description("Open the Tinybird dashboard in the default browser")
213
+ .option(
214
+ "-e, --env <env>",
215
+ "Which environment to open: 'cloud', 'local', or 'branch'"
216
+ )
217
+ .action(async (options) => {
218
+ const validEnvs = ["cloud", "local", "branch"];
219
+ if (options.env && !validEnvs.includes(options.env)) {
220
+ console.error(
221
+ `Error: Invalid environment '${options.env}'. Use one of: ${validEnvs.join(", ")}`
222
+ );
223
+ process.exit(1);
224
+ }
225
+
226
+ const result = await runOpenDashboard({
227
+ environment: options.env as Environment | undefined,
228
+ });
229
+
230
+ if (!result.success) {
231
+ console.error(`Error: ${result.error}`);
232
+ process.exit(1);
233
+ }
234
+
235
+ console.log(`Opening ${result.environment} dashboard...`);
236
+ if (result.browserOpened) {
237
+ console.log(`Dashboard: ${result.url}`);
238
+ } else {
239
+ console.log(`Could not open browser. Please visit: ${result.url}`);
240
+ }
241
+ });
242
+
173
243
  // Build command
174
244
  program
175
245
  .command("build")
@@ -177,6 +247,7 @@ function createCli(): Command {
177
247
  .option("--dry-run", "Generate without pushing to API")
178
248
  .option("--debug", "Show debug output including API requests/responses")
179
249
  .option("--local", "Use local Tinybird container")
250
+ .option("--branch", "Use Tinybird cloud with branches")
180
251
  .action(async (options) => {
181
252
  if (options.debug) {
182
253
  process.env.TINYBIRD_DEBUG = "1";
@@ -186,6 +257,8 @@ function createCli(): Command {
186
257
  let devModeOverride: DevMode | undefined;
187
258
  if (options.local) {
188
259
  devModeOverride = "local";
260
+ } else if (options.branch) {
261
+ devModeOverride = "branch";
189
262
  }
190
263
 
191
264
  const result = await runBuild({