@tinybirdco/sdk 0.0.4 → 0.0.6

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 (103) hide show
  1. package/README.md +52 -13
  2. package/dist/api/deploy.d.ts +41 -3
  3. package/dist/api/deploy.d.ts.map +1 -1
  4. package/dist/api/deploy.js +141 -19
  5. package/dist/api/deploy.js.map +1 -1
  6. package/dist/api/deploy.test.js +77 -29
  7. package/dist/api/deploy.test.js.map +1 -1
  8. package/dist/api/resources.d.ts +178 -0
  9. package/dist/api/resources.d.ts.map +1 -0
  10. package/dist/api/resources.js +244 -0
  11. package/dist/api/resources.js.map +1 -0
  12. package/dist/api/resources.test.d.ts +2 -0
  13. package/dist/api/resources.test.d.ts.map +1 -0
  14. package/dist/api/resources.test.js +255 -0
  15. package/dist/api/resources.test.js.map +1 -0
  16. package/dist/cli/commands/build.d.ts +3 -4
  17. package/dist/cli/commands/build.d.ts.map +1 -1
  18. package/dist/cli/commands/build.js +23 -25
  19. package/dist/cli/commands/build.js.map +1 -1
  20. package/dist/cli/commands/deploy.d.ts +39 -0
  21. package/dist/cli/commands/deploy.d.ts.map +1 -0
  22. package/dist/cli/commands/deploy.js +90 -0
  23. package/dist/cli/commands/deploy.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts.map +1 -1
  25. package/dist/cli/commands/dev.js +7 -3
  26. package/dist/cli/commands/dev.js.map +1 -1
  27. package/dist/cli/commands/init.d.ts +24 -1
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +174 -23
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/init.test.js +190 -30
  32. package/dist/cli/commands/init.test.js.map +1 -1
  33. package/dist/cli/index.js +72 -12
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/cli/utils/package-manager.d.ts +8 -0
  36. package/dist/cli/utils/package-manager.d.ts.map +1 -0
  37. package/dist/cli/utils/package-manager.js +45 -0
  38. package/dist/cli/utils/package-manager.js.map +1 -0
  39. package/dist/cli/utils/package-manager.test.d.ts +2 -0
  40. package/dist/cli/utils/package-manager.test.d.ts.map +1 -0
  41. package/dist/cli/utils/package-manager.test.js +85 -0
  42. package/dist/cli/utils/package-manager.test.js.map +1 -0
  43. package/dist/codegen/index.d.ts +39 -0
  44. package/dist/codegen/index.d.ts.map +1 -0
  45. package/dist/codegen/index.js +300 -0
  46. package/dist/codegen/index.js.map +1 -0
  47. package/dist/codegen/index.test.d.ts +2 -0
  48. package/dist/codegen/index.test.d.ts.map +1 -0
  49. package/dist/codegen/index.test.js +310 -0
  50. package/dist/codegen/index.test.js.map +1 -0
  51. package/dist/codegen/type-mapper.d.ts +20 -0
  52. package/dist/codegen/type-mapper.d.ts.map +1 -0
  53. package/dist/codegen/type-mapper.js +238 -0
  54. package/dist/codegen/type-mapper.js.map +1 -0
  55. package/dist/codegen/type-mapper.test.d.ts +2 -0
  56. package/dist/codegen/type-mapper.test.d.ts.map +1 -0
  57. package/dist/codegen/type-mapper.test.js +167 -0
  58. package/dist/codegen/type-mapper.test.js.map +1 -0
  59. package/dist/codegen/utils.d.ts +46 -0
  60. package/dist/codegen/utils.d.ts.map +1 -0
  61. package/dist/codegen/utils.js +141 -0
  62. package/dist/codegen/utils.js.map +1 -0
  63. package/dist/codegen/utils.test.d.ts +2 -0
  64. package/dist/codegen/utils.test.d.ts.map +1 -0
  65. package/dist/codegen/utils.test.js +178 -0
  66. package/dist/codegen/utils.test.js.map +1 -0
  67. package/dist/generator/index.d.ts +3 -0
  68. package/dist/generator/index.d.ts.map +1 -1
  69. package/dist/generator/index.js +17 -1
  70. package/dist/generator/index.js.map +1 -1
  71. package/dist/generator/index.test.js +104 -1
  72. package/dist/generator/index.test.js.map +1 -1
  73. package/dist/generator/loader.d.ts +15 -0
  74. package/dist/generator/loader.d.ts.map +1 -1
  75. package/dist/generator/loader.js +24 -0
  76. package/dist/generator/loader.js.map +1 -1
  77. package/dist/test/handlers.d.ts +49 -0
  78. package/dist/test/handlers.d.ts.map +1 -1
  79. package/dist/test/handlers.js +45 -0
  80. package/dist/test/handlers.js.map +1 -1
  81. package/package.json +4 -2
  82. package/src/api/deploy.test.ts +135 -34
  83. package/src/api/deploy.ts +203 -23
  84. package/src/api/resources.test.ts +332 -0
  85. package/src/api/resources.ts +554 -0
  86. package/src/cli/commands/build.ts +29 -33
  87. package/src/cli/commands/deploy.ts +126 -0
  88. package/src/cli/commands/dev.ts +10 -3
  89. package/src/cli/commands/init.test.ts +239 -30
  90. package/src/cli/commands/init.ts +243 -26
  91. package/src/cli/index.ts +84 -11
  92. package/src/cli/utils/package-manager.test.ts +118 -0
  93. package/src/cli/utils/package-manager.ts +44 -0
  94. package/src/codegen/index.test.ts +367 -0
  95. package/src/codegen/index.ts +379 -0
  96. package/src/codegen/type-mapper.test.ts +224 -0
  97. package/src/codegen/type-mapper.ts +265 -0
  98. package/src/codegen/utils.test.ts +221 -0
  99. package/src/codegen/utils.ts +174 -0
  100. package/src/generator/index.test.ts +121 -1
  101. package/src/generator/index.ts +19 -1
  102. package/src/generator/loader.ts +43 -0
  103. package/src/test/handlers.ts +58 -0
@@ -4,12 +4,14 @@
4
4
 
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
+ import * as p from "@clack/prompts";
8
+ import pc from "picocolors";
7
9
  import {
8
10
  hasValidToken,
9
- getTinybirdDir,
10
11
  getRelativeTinybirdDir,
11
12
  getConfigPath,
12
13
  updateConfig,
14
+ type DevMode,
13
15
  } from "../config.js";
14
16
  import { browserLogin } from "../auth.js";
15
17
  import { saveTinybirdToken } from "../env.js";
@@ -40,9 +42,9 @@ export type PageViewsRow = InferRow<typeof pageViews>;
40
42
  `;
41
43
 
42
44
  /**
43
- * Default starter content for pipes.ts
45
+ * Default starter content for endpoints.ts
44
46
  */
45
- const PIPES_CONTENT = `import { defineEndpoint, node, t, p, type InferParams, type InferOutputRow } from "@tinybirdco/sdk";
47
+ const ENDPOINTS_CONTENT = `import { defineEndpoint, node, t, p, type InferParams, type InferOutputRow } from "@tinybirdco/sdk";
46
48
 
47
49
  /**
48
50
  * Top pages endpoint - get the most visited pages
@@ -88,7 +90,7 @@ const CLIENT_CONTENT = `/**
88
90
  * Tinybird Client
89
91
  *
90
92
  * This file defines the typed Tinybird client for your project.
91
- * Add your datasources and pipes here as you create them.
93
+ * Add your datasources and endpoints here as you create them.
92
94
  */
93
95
 
94
96
  import { createTinybirdClient } from "@tinybirdco/sdk";
@@ -97,7 +99,7 @@ import { createTinybirdClient } from "@tinybirdco/sdk";
97
99
  import { pageViews, type PageViewsRow } from "./datasources";
98
100
 
99
101
  // Import endpoints and their types
100
- import { topPages, type TopPagesParams, type TopPagesOutput } from "./pipes";
102
+ import { topPages, type TopPagesParams, type TopPagesOutput } from "./endpoints";
101
103
 
102
104
  // Create the typed Tinybird client
103
105
  export const tinybird = createTinybirdClient({
@@ -115,14 +117,21 @@ export { pageViews, topPages };
115
117
  /**
116
118
  * Default config content generator
117
119
  */
118
- function createDefaultConfig(tinybirdDir: string) {
120
+ function createDefaultConfig(
121
+ tinybirdDir: string,
122
+ devMode: DevMode,
123
+ additionalIncludes: string[] = []
124
+ ) {
125
+ const include = [
126
+ `${tinybirdDir}/datasources.ts`,
127
+ `${tinybirdDir}/endpoints.ts`,
128
+ ...additionalIncludes,
129
+ ];
119
130
  return {
120
- include: [
121
- `${tinybirdDir}/datasources.ts`,
122
- `${tinybirdDir}/pipes.ts`,
123
- ],
131
+ include,
124
132
  token: "${TINYBIRD_TOKEN}",
125
133
  baseUrl: "https://api.tinybird.co",
134
+ devMode,
126
135
  };
127
136
  }
128
137
 
@@ -136,6 +145,14 @@ export interface InitOptions {
136
145
  force?: boolean;
137
146
  /** Skip the login flow */
138
147
  skipLogin?: boolean;
148
+ /** Development mode - if provided, skip interactive prompt */
149
+ devMode?: DevMode;
150
+ /** Client path - if provided, skip interactive prompt */
151
+ clientPath?: string;
152
+ /** Skip prompts for existing datafiles - for testing */
153
+ skipDatafilePrompt?: boolean;
154
+ /** Auto-include existing datafiles without prompting - for testing */
155
+ includeExistingDatafiles?: boolean;
139
156
  }
140
157
 
141
158
  /**
@@ -156,6 +173,66 @@ export interface InitResult {
156
173
  workspaceName?: string;
157
174
  /** User email after login */
158
175
  userEmail?: string;
176
+ /** Selected development mode */
177
+ devMode?: DevMode;
178
+ /** Selected client path */
179
+ clientPath?: string;
180
+ /** Existing datafiles that were added to config */
181
+ existingDatafiles?: string[];
182
+ }
183
+
184
+ /**
185
+ * Find existing .datasource and .pipe files in the repository
186
+ *
187
+ * @param cwd - Working directory to search from
188
+ * @param maxDepth - Maximum directory depth to search (default: 5)
189
+ * @returns Array of relative file paths
190
+ */
191
+ export function findExistingDatafiles(
192
+ cwd: string,
193
+ maxDepth: number = 5
194
+ ): string[] {
195
+ const files: string[] = [];
196
+
197
+ function searchDir(dir: string, depth: number): void {
198
+ if (depth > maxDepth) return;
199
+
200
+ let entries: fs.Dirent[];
201
+ try {
202
+ entries = fs.readdirSync(dir, { withFileTypes: true });
203
+ } catch {
204
+ return; // Skip directories we can't read
205
+ }
206
+
207
+ for (const entry of entries) {
208
+ const fullPath = path.join(dir, entry.name);
209
+
210
+ // Skip node_modules and hidden directories
211
+ if (
212
+ entry.isDirectory() &&
213
+ (entry.name === "node_modules" ||
214
+ entry.name.startsWith(".") ||
215
+ entry.name === "dist" ||
216
+ entry.name === "build")
217
+ ) {
218
+ continue;
219
+ }
220
+
221
+ if (entry.isDirectory()) {
222
+ searchDir(fullPath, depth + 1);
223
+ } else if (
224
+ entry.isFile() &&
225
+ (entry.name.endsWith(".datasource") || entry.name.endsWith(".pipe"))
226
+ ) {
227
+ // Convert to relative path
228
+ const relativePath = path.relative(cwd, fullPath);
229
+ files.push(relativePath);
230
+ }
231
+ }
232
+ }
233
+
234
+ searchDir(cwd, 0);
235
+ return files.sort();
159
236
  }
160
237
 
161
238
  /**
@@ -163,7 +240,7 @@ export interface InitResult {
163
240
  *
164
241
  * Creates:
165
242
  * - tinybird.json in the project root
166
- * - src/tinybird/ folder with datasources.ts, pipes.ts, and client.ts
243
+ * - src/tinybird/ folder with datasources.ts, endpoints.ts, and client.ts
167
244
  *
168
245
  * @param options - Init options
169
246
  * @returns Init result
@@ -175,14 +252,90 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
175
252
 
176
253
  const created: string[] = [];
177
254
  const skipped: string[] = [];
255
+ let existingDatafiles: string[] = [];
256
+
257
+ // Check for existing .datasource and .pipe files
258
+ const foundDatafiles = findExistingDatafiles(cwd);
259
+
260
+ // Determine devMode - prompt if not provided
261
+ let devMode: DevMode = options.devMode ?? "branch";
262
+
263
+ if (!options.devMode) {
264
+ // Show interactive prompt for workflow selection
265
+ p.intro(pc.cyan("tinybird init"));
266
+
267
+ const workflowChoice = await p.select({
268
+ message: "How do you want to develop with Tinybird?",
269
+ options: [
270
+ {
271
+ value: "branch",
272
+ label: "Branches",
273
+ hint: "Use Tinybird Cloud with git-based branching",
274
+ },
275
+ {
276
+ value: "local",
277
+ label: "Tinybird Local",
278
+ hint: "Run your own Tinybird instance locally",
279
+ },
280
+ ],
281
+ });
282
+
283
+ if (p.isCancel(workflowChoice)) {
284
+ p.cancel("Init cancelled.");
285
+ return {
286
+ success: false,
287
+ created: [],
288
+ skipped: [],
289
+ error: "Cancelled by user",
290
+ };
291
+ }
292
+
293
+ devMode = workflowChoice as DevMode;
294
+ }
178
295
 
179
296
  // Determine tinybird folder path based on project structure
180
- const tinybirdDir = getTinybirdDir(cwd);
181
- const relativeTinybirdDir = getRelativeTinybirdDir(cwd);
297
+ const defaultRelativePath = getRelativeTinybirdDir(cwd);
298
+ let relativeTinybirdDir = options.clientPath ?? defaultRelativePath;
299
+
300
+ if (!options.clientPath && !options.devMode) {
301
+ // Ask user to confirm or change the client path
302
+ const clientPathChoice = await p.text({
303
+ message: "Where should we generate the Tinybird client?",
304
+ placeholder: defaultRelativePath,
305
+ defaultValue: defaultRelativePath,
306
+ });
307
+
308
+ if (p.isCancel(clientPathChoice)) {
309
+ p.cancel("Init cancelled.");
310
+ return {
311
+ success: false,
312
+ created: [],
313
+ skipped: [],
314
+ error: "Cancelled by user",
315
+ };
316
+ }
317
+
318
+ relativeTinybirdDir = clientPathChoice || defaultRelativePath;
319
+ }
320
+
321
+ // Ask about existing datafiles if found
322
+ if (foundDatafiles.length > 0 && !options.skipDatafilePrompt) {
323
+ const includeDatafiles =
324
+ options.includeExistingDatafiles ??
325
+ (await promptForExistingDatafiles(foundDatafiles));
326
+
327
+ if (includeDatafiles) {
328
+ existingDatafiles = foundDatafiles;
329
+ }
330
+ } else if (options.includeExistingDatafiles && foundDatafiles.length > 0) {
331
+ existingDatafiles = foundDatafiles;
332
+ }
333
+
334
+ const tinybirdDir = path.join(cwd, relativeTinybirdDir);
182
335
 
183
336
  // File paths
184
337
  const datasourcesPath = path.join(tinybirdDir, "datasources.ts");
185
- const pipesPath = path.join(tinybirdDir, "pipes.ts");
338
+ const endpointsPath = path.join(tinybirdDir, "endpoints.ts");
186
339
  const clientPath = path.join(tinybirdDir, "client.ts");
187
340
 
188
341
  // Create config file (tinybird.json)
@@ -191,7 +344,11 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
191
344
  skipped.push("tinybird.json");
192
345
  } else {
193
346
  try {
194
- const config = createDefaultConfig(relativeTinybirdDir);
347
+ const config = createDefaultConfig(
348
+ relativeTinybirdDir,
349
+ devMode,
350
+ existingDatafiles
351
+ );
195
352
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
196
353
  created.push("tinybird.json");
197
354
  } catch (error) {
@@ -212,7 +369,9 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
212
369
  success: false,
213
370
  created,
214
371
  skipped,
215
- error: `Failed to create ${relativeTinybirdDir} folder: ${(error as Error).message}`,
372
+ error: `Failed to create ${relativeTinybirdDir} folder: ${
373
+ (error as Error).message
374
+ }`,
216
375
  };
217
376
  }
218
377
 
@@ -233,19 +392,19 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
233
392
  }
234
393
  }
235
394
 
236
- // Create pipes.ts
237
- if (fs.existsSync(pipesPath) && !force) {
238
- skipped.push(`${relativeTinybirdDir}/pipes.ts`);
395
+ // Create endpoints.ts
396
+ if (fs.existsSync(endpointsPath) && !force) {
397
+ skipped.push(`${relativeTinybirdDir}/endpoints.ts`);
239
398
  } else {
240
399
  try {
241
- fs.writeFileSync(pipesPath, PIPES_CONTENT);
242
- created.push(`${relativeTinybirdDir}/pipes.ts`);
400
+ fs.writeFileSync(endpointsPath, ENDPOINTS_CONTENT);
401
+ created.push(`${relativeTinybirdDir}/endpoints.ts`);
243
402
  } catch (error) {
244
403
  return {
245
404
  success: false,
246
405
  created,
247
406
  skipped,
248
- error: `Failed to create pipes.ts: ${(error as Error).message}`,
407
+ error: `Failed to create endpoints.ts: ${(error as Error).message}`,
249
408
  };
250
409
  }
251
410
  }
@@ -288,8 +447,16 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
288
447
  modified = true;
289
448
  }
290
449
 
450
+ if (!packageJson.scripts["tinybird:deploy"]) {
451
+ packageJson.scripts["tinybird:deploy"] = "tinybird deploy";
452
+ modified = true;
453
+ }
454
+
291
455
  if (modified) {
292
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
456
+ fs.writeFileSync(
457
+ packageJsonPath,
458
+ JSON.stringify(packageJson, null, 2) + "\n"
459
+ );
293
460
  created.push("package.json (added tinybird scripts)");
294
461
  }
295
462
  } catch {
@@ -312,8 +479,9 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
312
479
  }
313
480
 
314
481
  // If custom base URL, update tinybird.json
315
- if (authResult.baseUrl && authResult.baseUrl !== "https://api.tinybird.co") {
316
- updateConfig(configPath, { baseUrl: authResult.baseUrl });
482
+ const baseUrl = authResult.baseUrl ?? "https://api.tinybird.co";
483
+ if (baseUrl !== "https://api.tinybird.co") {
484
+ updateConfig(configPath, { baseUrl });
317
485
  }
318
486
 
319
487
  return {
@@ -323,15 +491,25 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
323
491
  loggedIn: true,
324
492
  workspaceName: authResult.workspaceName,
325
493
  userEmail: authResult.userEmail,
494
+ devMode,
495
+ clientPath: relativeTinybirdDir,
496
+ existingDatafiles:
497
+ existingDatafiles.length > 0 ? existingDatafiles : undefined,
326
498
  };
327
499
  } catch (error) {
328
500
  // Login succeeded but saving credentials failed
329
- console.error(`Warning: Failed to save credentials: ${(error as Error).message}`);
501
+ console.error(
502
+ `Warning: Failed to save credentials: ${(error as Error).message}`
503
+ );
330
504
  return {
331
505
  success: true,
332
506
  created,
333
507
  skipped,
334
508
  loggedIn: false,
509
+ devMode,
510
+ clientPath: relativeTinybirdDir,
511
+ existingDatafiles:
512
+ existingDatafiles.length > 0 ? existingDatafiles : undefined,
335
513
  };
336
514
  }
337
515
  } else {
@@ -341,6 +519,10 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
341
519
  created,
342
520
  skipped,
343
521
  loggedIn: false,
522
+ devMode,
523
+ clientPath: relativeTinybirdDir,
524
+ existingDatafiles:
525
+ existingDatafiles.length > 0 ? existingDatafiles : undefined,
344
526
  };
345
527
  }
346
528
  }
@@ -349,5 +531,40 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
349
531
  success: true,
350
532
  created,
351
533
  skipped,
534
+ devMode,
535
+ clientPath: relativeTinybirdDir,
536
+ existingDatafiles:
537
+ existingDatafiles.length > 0 ? existingDatafiles : undefined,
352
538
  };
353
539
  }
540
+
541
+ /**
542
+ * Prompt user about including existing datafiles
543
+ */
544
+ async function promptForExistingDatafiles(
545
+ datafiles: string[]
546
+ ): Promise<boolean> {
547
+ const datasourceCount = datafiles.filter((f) =>
548
+ f.endsWith(".datasource")
549
+ ).length;
550
+ const pipeCount = datafiles.filter((f) => f.endsWith(".pipe")).length;
551
+
552
+ const parts: string[] = [];
553
+ if (datasourceCount > 0) {
554
+ parts.push(`${datasourceCount} .datasource file${datasourceCount > 1 ? "s" : ""}`);
555
+ }
556
+ if (pipeCount > 0) {
557
+ parts.push(`${pipeCount} .pipe file${pipeCount > 1 ? "s" : ""}`);
558
+ }
559
+
560
+ const confirmInclude = await p.confirm({
561
+ message: `Found ${parts.join(" and ")} in your project. Include them in tinybird.json?`,
562
+ initialValue: true,
563
+ });
564
+
565
+ if (p.isCancel(confirmInclude)) {
566
+ return false;
567
+ }
568
+
569
+ return confirmInclude;
570
+ }
package/src/cli/index.ts CHANGED
@@ -16,6 +16,7 @@ import { dirname, resolve } from "node:path";
16
16
  import { Command } from "commander";
17
17
  import { runInit } from "./commands/init.js";
18
18
  import { runBuild } from "./commands/build.js";
19
+ import { runDeploy } from "./commands/deploy.js";
19
20
  import { runDev } from "./commands/dev.js";
20
21
  import { runLogin } from "./commands/login.js";
21
22
  import {
@@ -23,6 +24,7 @@ import {
23
24
  runBranchStatus,
24
25
  runBranchDelete,
25
26
  } from "./commands/branch.js";
27
+ import { detectPackageManagerRunCmd } from "./utils/package-manager.js";
26
28
  import type { DevMode } from "./config.js";
27
29
 
28
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -55,12 +57,20 @@ function createCli(): Command {
55
57
  .description("Initialize a new Tinybird TypeScript project")
56
58
  .option("-f, --force", "Overwrite existing files")
57
59
  .option("--skip-login", "Skip browser login flow")
60
+ .option("-m, --mode <mode>", "Development mode: 'branch' or 'local'")
61
+ .option("-p, --path <path>", "Path for Tinybird client files")
58
62
  .action(async (options) => {
59
- console.log("Initializing Tinybird project...\n");
63
+ // Validate mode if provided
64
+ if (options.mode && !["branch", "local"].includes(options.mode)) {
65
+ console.error(`Error: Invalid mode '${options.mode}'. Use 'branch' or 'local'.`);
66
+ process.exit(1);
67
+ }
60
68
 
61
69
  const result = await runInit({
62
70
  force: options.force,
63
71
  skipLogin: options.skipLogin,
72
+ devMode: options.mode,
73
+ clientPath: options.path,
64
74
  });
65
75
 
66
76
  if (!result.success) {
@@ -82,6 +92,10 @@ function createCli(): Command {
82
92
  });
83
93
  }
84
94
 
95
+ // Detect package manager for run command
96
+ const runCmd = detectPackageManagerRunCmd();
97
+ const clientPath = result.clientPath ?? "tinybird";
98
+
85
99
  if (result.loggedIn) {
86
100
  console.log(`\nLogged in successfully!`);
87
101
  if (result.workspaceName) {
@@ -90,19 +104,25 @@ function createCli(): Command {
90
104
  if (result.userEmail) {
91
105
  console.log(` User: ${result.userEmail}`);
92
106
  }
107
+
108
+ if (result.existingDatafiles && result.existingDatafiles.length > 0) {
109
+ console.log(
110
+ `\nAdded ${result.existingDatafiles.length} existing datafile(s) to tinybird.json.`
111
+ );
112
+ }
93
113
  console.log("\nDone! Next steps:");
94
- console.log(" 1. Edit src/tinybird/schema.ts with your schema");
95
- console.log(" 2. Run 'npx tinybird dev' to start development");
114
+ console.log(` 1. Edit your schema in ${clientPath}/`);
115
+ console.log(` 2. Run '${runCmd} tinybird:dev' to start development`);
96
116
  } else if (result.loggedIn === false) {
97
117
  console.log("\nLogin was skipped or failed.");
98
118
  console.log("\nDone! Next steps:");
99
119
  console.log(" 1. Run 'npx tinybird login' to authenticate");
100
- console.log(" 2. Edit src/tinybird/schema.ts with your schema");
101
- console.log(" 3. Run 'npx tinybird dev' to start development");
120
+ console.log(` 2. Edit your schema in ${clientPath}/`);
121
+ console.log(` 3. Run '${runCmd} tinybird:dev' to start development`);
102
122
  } else {
103
123
  console.log("\nDone! Next steps:");
104
- console.log(" 1. Edit src/tinybird/schema.ts with your schema");
105
- console.log(" 2. Run 'npx tinybird dev' to start development");
124
+ console.log(` 1. Edit your schema in ${clientPath}/`);
125
+ console.log(` 2. Run '${runCmd} tinybird:dev' to start development`);
106
126
  }
107
127
  });
108
128
 
@@ -135,11 +155,10 @@ function createCli(): Command {
135
155
  // Build command
136
156
  program
137
157
  .command("build")
138
- .description("Build and push resources to Tinybird")
158
+ .description("Build and push resources to a Tinybird branch (not main)")
139
159
  .option("--dry-run", "Generate without pushing to API")
140
160
  .option("--debug", "Show debug output including API requests/responses")
141
161
  .option("--local", "Use local Tinybird container")
142
- .option("--branch", "Use Tinybird cloud with branches")
143
162
  .action(async (options) => {
144
163
  if (options.debug) {
145
164
  process.env.TINYBIRD_DEBUG = "1";
@@ -149,8 +168,6 @@ function createCli(): Command {
149
168
  let devModeOverride: DevMode | undefined;
150
169
  if (options.local) {
151
170
  devModeOverride = "local";
152
- } else if (options.branch) {
153
- devModeOverride = "branch";
154
171
  }
155
172
 
156
173
  const modeLabel = devModeOverride === "local" ? " (local)" : "";
@@ -200,6 +217,62 @@ function createCli(): Command {
200
217
  console.log(`\n[${formatTime()}] Done in ${result.durationMs}ms`);
201
218
  });
202
219
 
220
+ // Deploy command
221
+ program
222
+ .command("deploy")
223
+ .description("Deploy resources to main Tinybird workspace (production)")
224
+ .option("--dry-run", "Generate without pushing to API")
225
+ .option("--debug", "Show debug output including API requests/responses")
226
+ .action(async (options) => {
227
+ if (options.debug) {
228
+ process.env.TINYBIRD_DEBUG = "1";
229
+ }
230
+
231
+ console.log(`[${formatTime()}] Deploying to main workspace...\n`);
232
+
233
+ const result = await runDeploy({
234
+ dryRun: options.dryRun,
235
+ });
236
+
237
+ if (!result.success) {
238
+ console.error(`Error: ${result.error}`);
239
+ process.exit(1);
240
+ }
241
+
242
+ const { build, deploy } = result;
243
+
244
+ if (build) {
245
+ console.log(`Generated ${build.stats.datasourceCount} datasource(s), ${build.stats.pipeCount} pipe(s)`);
246
+ }
247
+
248
+ if (options.dryRun) {
249
+ console.log("\n[Dry run] Resources not deployed to API");
250
+
251
+ // Show generated content
252
+ if (build) {
253
+ console.log("\n--- Generated Datasources ---");
254
+ build.resources.datasources.forEach((ds) => {
255
+ console.log(`\n${ds.name}.datasource:`);
256
+ console.log(ds.content);
257
+ });
258
+
259
+ console.log("\n--- Generated Pipes ---");
260
+ build.resources.pipes.forEach((pipe) => {
261
+ console.log(`\n${pipe.name}.pipe:`);
262
+ console.log(pipe.content);
263
+ });
264
+ }
265
+ } else if (deploy) {
266
+ if (deploy.result === "no_changes") {
267
+ console.log("No changes detected - already up to date");
268
+ } else {
269
+ console.log(`Deployed to main workspace successfully`);
270
+ }
271
+ }
272
+
273
+ console.log(`\n[${formatTime()}] Done in ${result.durationMs}ms`);
274
+ });
275
+
203
276
  // Dev command
204
277
  program
205
278
  .command("dev")
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { detectPackageManagerRunCmd } from "./package-manager.js";
6
+
7
+ describe("detectPackageManagerRunCmd", () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(() => {
11
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pkg-manager-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ try {
16
+ fs.rmSync(tempDir, { recursive: true });
17
+ } catch {
18
+ // Ignore cleanup errors
19
+ }
20
+ });
21
+
22
+ describe("lockfile detection", () => {
23
+ it("detects pnpm from pnpm-lock.yaml", () => {
24
+ fs.writeFileSync(path.join(tempDir, "pnpm-lock.yaml"), "");
25
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("pnpm");
26
+ });
27
+
28
+ it("detects yarn from yarn.lock", () => {
29
+ fs.writeFileSync(path.join(tempDir, "yarn.lock"), "");
30
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("yarn");
31
+ });
32
+
33
+ it("detects bun from bun.lockb", () => {
34
+ fs.writeFileSync(path.join(tempDir, "bun.lockb"), "");
35
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("bun run");
36
+ });
37
+
38
+ it("detects npm from package-lock.json", () => {
39
+ fs.writeFileSync(path.join(tempDir, "package-lock.json"), "{}");
40
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("npm run");
41
+ });
42
+
43
+ it("prioritizes pnpm lockfile over others", () => {
44
+ fs.writeFileSync(path.join(tempDir, "pnpm-lock.yaml"), "");
45
+ fs.writeFileSync(path.join(tempDir, "yarn.lock"), "");
46
+ fs.writeFileSync(path.join(tempDir, "package-lock.json"), "{}");
47
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("pnpm");
48
+ });
49
+
50
+ it("prioritizes yarn lockfile over npm", () => {
51
+ fs.writeFileSync(path.join(tempDir, "yarn.lock"), "");
52
+ fs.writeFileSync(path.join(tempDir, "package-lock.json"), "{}");
53
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("yarn");
54
+ });
55
+ });
56
+
57
+ describe("packageManager field detection", () => {
58
+ it("detects pnpm from packageManager field", () => {
59
+ fs.writeFileSync(
60
+ path.join(tempDir, "package.json"),
61
+ JSON.stringify({ packageManager: "pnpm@9.0.0" })
62
+ );
63
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("pnpm");
64
+ });
65
+
66
+ it("detects yarn from packageManager field", () => {
67
+ fs.writeFileSync(
68
+ path.join(tempDir, "package.json"),
69
+ JSON.stringify({ packageManager: "yarn@4.0.0" })
70
+ );
71
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("yarn");
72
+ });
73
+
74
+ it("detects bun from packageManager field", () => {
75
+ fs.writeFileSync(
76
+ path.join(tempDir, "package.json"),
77
+ JSON.stringify({ packageManager: "bun@1.0.0" })
78
+ );
79
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("bun run");
80
+ });
81
+
82
+ it("prioritizes lockfile over packageManager field", () => {
83
+ fs.writeFileSync(path.join(tempDir, "yarn.lock"), "");
84
+ fs.writeFileSync(
85
+ path.join(tempDir, "package.json"),
86
+ JSON.stringify({ packageManager: "pnpm@9.0.0" })
87
+ );
88
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("yarn");
89
+ });
90
+ });
91
+
92
+ describe("default behavior", () => {
93
+ it("defaults to npm run when no indicators found", () => {
94
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("npm run");
95
+ });
96
+
97
+ it("defaults to npm run when package.json has no packageManager field", () => {
98
+ fs.writeFileSync(
99
+ path.join(tempDir, "package.json"),
100
+ JSON.stringify({ name: "test-project" })
101
+ );
102
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("npm run");
103
+ });
104
+
105
+ it("defaults to npm run when package.json is invalid JSON", () => {
106
+ fs.writeFileSync(path.join(tempDir, "package.json"), "not json");
107
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("npm run");
108
+ });
109
+
110
+ it("defaults to npm run when packageManager is not a string", () => {
111
+ fs.writeFileSync(
112
+ path.join(tempDir, "package.json"),
113
+ JSON.stringify({ packageManager: 123 })
114
+ );
115
+ expect(detectPackageManagerRunCmd(tempDir)).toBe("npm run");
116
+ });
117
+ });
118
+ });