@tinybirdco/sdk 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/api/deploy.d.ts +80 -2
  2. package/dist/api/deploy.d.ts.map +1 -1
  3. package/dist/api/deploy.js +82 -38
  4. package/dist/api/deploy.js.map +1 -1
  5. package/dist/cli/commands/deploy.d.ts +3 -0
  6. package/dist/cli/commands/deploy.d.ts.map +1 -1
  7. package/dist/cli/commands/deploy.js +1 -0
  8. package/dist/cli/commands/deploy.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts.map +1 -1
  10. package/dist/cli/commands/init.js +24 -2
  11. package/dist/cli/commands/init.js.map +1 -1
  12. package/dist/cli/commands/preview.d.ts +65 -0
  13. package/dist/cli/commands/preview.d.ts.map +1 -0
  14. package/dist/cli/commands/preview.js +234 -0
  15. package/dist/cli/commands/preview.js.map +1 -0
  16. package/dist/cli/commands/preview.test.d.ts +2 -0
  17. package/dist/cli/commands/preview.test.d.ts.map +1 -0
  18. package/dist/cli/commands/preview.test.js +36 -0
  19. package/dist/cli/commands/preview.test.js.map +1 -0
  20. package/dist/cli/index.js +135 -36
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/output.d.ts +45 -0
  23. package/dist/cli/output.d.ts.map +1 -1
  24. package/dist/cli/output.js +73 -1
  25. package/dist/cli/output.js.map +1 -1
  26. package/dist/cli/output.test.js +98 -2
  27. package/dist/cli/output.test.js.map +1 -1
  28. package/dist/client/preview.d.ts +36 -0
  29. package/dist/client/preview.d.ts.map +1 -0
  30. package/dist/client/preview.js +161 -0
  31. package/dist/client/preview.js.map +1 -0
  32. package/dist/client/preview.test.d.ts +2 -0
  33. package/dist/client/preview.test.d.ts.map +1 -0
  34. package/dist/client/preview.test.js +137 -0
  35. package/dist/client/preview.test.js.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/schema/project.d.ts.map +1 -1
  41. package/dist/schema/project.js +7 -3
  42. package/dist/schema/project.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/api/deploy.ts +170 -10
  45. package/src/cli/commands/deploy.ts +4 -1
  46. package/src/cli/commands/init.ts +24 -2
  47. package/src/cli/commands/preview.test.ts +42 -0
  48. package/src/cli/commands/preview.ts +313 -0
  49. package/src/cli/index.ts +147 -37
  50. package/src/cli/output.test.ts +116 -1
  51. package/src/cli/output.ts +96 -1
  52. package/src/client/preview.test.ts +168 -0
  53. package/src/client/preview.ts +210 -0
  54. package/src/index.ts +8 -0
  55. package/src/schema/project.ts +9 -3
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { generatePreviewBranchName } from "./preview.js";
3
+
4
+ describe("Preview command", () => {
5
+ describe("generatePreviewBranchName", () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ vi.setSystemTime(new Date("2024-02-06T12:00:00Z"));
9
+ });
10
+
11
+ it("generates name with branch and timestamp", () => {
12
+ const result = generatePreviewBranchName("feature-branch");
13
+ expect(result).toBe("tmp_ci_feature_branch_1707220800");
14
+ });
15
+
16
+ it("sanitizes branch name with slashes", () => {
17
+ const result = generatePreviewBranchName("feature/add-login");
18
+ expect(result).toBe("tmp_ci_feature_add_login_1707220800");
19
+ });
20
+
21
+ it("sanitizes branch name with dots", () => {
22
+ const result = generatePreviewBranchName("release.1.0");
23
+ expect(result).toBe("tmp_ci_release_1_0_1707220800");
24
+ });
25
+
26
+ it("handles complex branch names", () => {
27
+ const result = generatePreviewBranchName("feature/JIRA-123/add-user-auth");
28
+ expect(result).toBe("tmp_ci_feature_JIRA_123_add_user_auth_1707220800");
29
+ });
30
+
31
+ it("uses 'unknown' when branch is null", () => {
32
+ const result = generatePreviewBranchName(null);
33
+ expect(result).toBe("tmp_ci_unknown_1707220800");
34
+ });
35
+
36
+ it("uses unix timestamp in seconds", () => {
37
+ // 1707220800 is 2024-02-06T12:00:00Z in seconds
38
+ const result = generatePreviewBranchName("test");
39
+ expect(result).toMatch(/_1707220800$/);
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Preview command - creates ephemeral preview branch and deploys resources
3
+ */
4
+
5
+ import { loadConfig, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js";
6
+ import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js";
7
+ import { createBranch, type TinybirdBranch } from "../../api/branches.js";
8
+ import { deployToMain } from "../../api/deploy.js";
9
+ import { buildToTinybird } from "../../api/build.js";
10
+ import {
11
+ getLocalTokens,
12
+ getOrCreateLocalWorkspace,
13
+ LocalNotRunningError,
14
+ } from "../../api/local.js";
15
+ import { sanitizeBranchName, getCurrentGitBranch } from "../git.js";
16
+ import type { BuildApiResult } from "../../api/build.js";
17
+
18
+ /**
19
+ * Preview command options
20
+ */
21
+ export interface PreviewCommandOptions {
22
+ /** Working directory (defaults to cwd) */
23
+ cwd?: string;
24
+ /** Skip pushing to API (just generate) */
25
+ dryRun?: boolean;
26
+ /** Validate deploy with Tinybird API without applying */
27
+ check?: boolean;
28
+ /** Override preview branch name */
29
+ name?: string;
30
+ /** Override the devMode from config */
31
+ devModeOverride?: DevMode;
32
+ }
33
+
34
+ /**
35
+ * Preview command result
36
+ */
37
+ export interface PreviewCommandResult {
38
+ /** Whether the preview was successful */
39
+ success: boolean;
40
+ /** Branch information */
41
+ branch?: {
42
+ name: string;
43
+ id: string;
44
+ token: string;
45
+ url: string;
46
+ created_at: string;
47
+ };
48
+ /** Build statistics */
49
+ build?: {
50
+ datasourceCount: number;
51
+ pipeCount: number;
52
+ };
53
+ /** Deploy result */
54
+ deploy?: {
55
+ result: string;
56
+ };
57
+ /** Error message if failed */
58
+ error?: string;
59
+ /** Duration in milliseconds */
60
+ durationMs: number;
61
+ }
62
+
63
+ /**
64
+ * Generate preview branch name with format: tmp_ci_${branch}_${timestamp}
65
+ *
66
+ * @param gitBranch - Current git branch name (or null)
67
+ * @returns Preview branch name
68
+ */
69
+ export function generatePreviewBranchName(gitBranch: string | null): string {
70
+ const timestamp = Math.floor(Date.now() / 1000);
71
+ const branchPart = gitBranch ? sanitizeBranchName(gitBranch) : "unknown";
72
+ return `tmp_ci_${branchPart}_${timestamp}`;
73
+ }
74
+
75
+ /**
76
+ * Run the preview command
77
+ *
78
+ * Creates an ephemeral preview branch and deploys resources to it.
79
+ * Preview branches are not cached and are meant for CI/testing.
80
+ *
81
+ * @param options - Preview options
82
+ * @returns Preview command result
83
+ */
84
+ export async function runPreview(options: PreviewCommandOptions = {}): Promise<PreviewCommandResult> {
85
+ const startTime = Date.now();
86
+ const cwd = options.cwd ?? process.cwd();
87
+
88
+ // Load config
89
+ let config: ResolvedConfig;
90
+ try {
91
+ config = loadConfig(cwd);
92
+ } catch (error) {
93
+ return {
94
+ success: false,
95
+ error: (error as Error).message,
96
+ durationMs: Date.now() - startTime,
97
+ };
98
+ }
99
+
100
+ // Get current git branch and generate preview branch name
101
+ const gitBranch = getCurrentGitBranch();
102
+ const previewBranchName = options.name ?? generatePreviewBranchName(gitBranch);
103
+
104
+ // Build resources from include paths
105
+ let buildResult: BuildFromIncludeResult;
106
+ try {
107
+ buildResult = await buildFromInclude({
108
+ includePaths: config.include,
109
+ cwd: config.cwd,
110
+ });
111
+ } catch (error) {
112
+ return {
113
+ success: false,
114
+ error: `Build failed: ${(error as Error).message}`,
115
+ durationMs: Date.now() - startTime,
116
+ };
117
+ }
118
+
119
+ const buildStats = {
120
+ datasourceCount: buildResult.stats.datasourceCount,
121
+ pipeCount: buildResult.stats.pipeCount,
122
+ };
123
+
124
+ // If dry run, return without creating branch or deploying
125
+ if (options.dryRun) {
126
+ return {
127
+ success: true,
128
+ branch: {
129
+ name: previewBranchName,
130
+ id: "(dry-run)",
131
+ token: "(dry-run)",
132
+ url: config.baseUrl,
133
+ created_at: new Date().toISOString(),
134
+ },
135
+ build: buildStats,
136
+ durationMs: Date.now() - startTime,
137
+ };
138
+ }
139
+
140
+ const debug = !!process.env.TINYBIRD_DEBUG;
141
+ const devMode = options.devModeOverride ?? config.devMode;
142
+
143
+ if (debug) {
144
+ console.log(`[debug] devMode: ${devMode}`);
145
+ console.log(`[debug] previewBranchName: ${previewBranchName}`);
146
+ }
147
+
148
+ // Handle local mode
149
+ if (devMode === "local") {
150
+ try {
151
+ if (debug) {
152
+ console.log(`[debug] Getting local tokens from ${LOCAL_BASE_URL}/tokens`);
153
+ }
154
+
155
+ const localTokens = await getLocalTokens();
156
+
157
+ // Create workspace with preview branch name
158
+ if (debug) {
159
+ console.log(`[debug] Creating local workspace: ${previewBranchName}`);
160
+ }
161
+
162
+ const { workspace, wasCreated } = await getOrCreateLocalWorkspace(localTokens, previewBranchName);
163
+ if (debug) {
164
+ console.log(`[debug] Workspace ${wasCreated ? "created" : "found"}: ${workspace.name}`);
165
+ }
166
+
167
+ // Use /v1/build for local (no deploy endpoint in local)
168
+ const deployResult = await buildToTinybird(
169
+ {
170
+ baseUrl: LOCAL_BASE_URL,
171
+ token: workspace.token,
172
+ },
173
+ buildResult.resources
174
+ );
175
+
176
+ if (!deployResult.success) {
177
+ return {
178
+ success: false,
179
+ branch: {
180
+ name: previewBranchName,
181
+ id: workspace.id,
182
+ token: workspace.token,
183
+ url: LOCAL_BASE_URL,
184
+ created_at: new Date().toISOString(),
185
+ },
186
+ build: buildStats,
187
+ error: deployResult.error,
188
+ durationMs: Date.now() - startTime,
189
+ };
190
+ }
191
+
192
+ return {
193
+ success: true,
194
+ branch: {
195
+ name: previewBranchName,
196
+ id: workspace.id,
197
+ token: workspace.token,
198
+ url: LOCAL_BASE_URL,
199
+ created_at: new Date().toISOString(),
200
+ },
201
+ build: buildStats,
202
+ deploy: {
203
+ result: deployResult.result,
204
+ },
205
+ durationMs: Date.now() - startTime,
206
+ };
207
+ } catch (error) {
208
+ if (error instanceof LocalNotRunningError) {
209
+ return {
210
+ success: false,
211
+ error: error.message,
212
+ durationMs: Date.now() - startTime,
213
+ };
214
+ }
215
+ return {
216
+ success: false,
217
+ error: `Local preview failed: ${(error as Error).message}`,
218
+ durationMs: Date.now() - startTime,
219
+ };
220
+ }
221
+ }
222
+
223
+ // Cloud mode - create branch and deploy using /v1/deploy
224
+ let branch: TinybirdBranch;
225
+ try {
226
+ if (debug) {
227
+ console.log(`[debug] Creating preview branch: ${previewBranchName}`);
228
+ }
229
+
230
+ branch = await createBranch(
231
+ { baseUrl: config.baseUrl, token: config.token },
232
+ previewBranchName
233
+ );
234
+
235
+ if (debug) {
236
+ console.log(`[debug] Branch created: ${branch.name} (${branch.id})`);
237
+ }
238
+ } catch (error) {
239
+ return {
240
+ success: false,
241
+ error: `Failed to create preview branch: ${(error as Error).message}`,
242
+ durationMs: Date.now() - startTime,
243
+ };
244
+ }
245
+
246
+ if (!branch.token) {
247
+ return {
248
+ success: false,
249
+ error: `Preview branch created but no token returned`,
250
+ durationMs: Date.now() - startTime,
251
+ };
252
+ }
253
+
254
+ // Deploy to branch using /v1/deploy (production-like experience)
255
+ let deployResult: BuildApiResult;
256
+ try {
257
+ if (debug) {
258
+ console.log(`[debug] Deploying to preview branch using branch token`);
259
+ }
260
+
261
+ deployResult = await deployToMain(
262
+ { baseUrl: config.baseUrl, token: branch.token },
263
+ buildResult.resources,
264
+ { check: options.check, allowDestructiveOperations: true }
265
+ );
266
+ } catch (error) {
267
+ return {
268
+ success: false,
269
+ branch: {
270
+ name: branch.name,
271
+ id: branch.id,
272
+ token: branch.token,
273
+ url: config.baseUrl,
274
+ created_at: branch.created_at,
275
+ },
276
+ build: buildStats,
277
+ error: `Deploy failed: ${(error as Error).message}`,
278
+ durationMs: Date.now() - startTime,
279
+ };
280
+ }
281
+
282
+ if (!deployResult.success) {
283
+ return {
284
+ success: false,
285
+ branch: {
286
+ name: branch.name,
287
+ id: branch.id,
288
+ token: branch.token,
289
+ url: config.baseUrl,
290
+ created_at: branch.created_at,
291
+ },
292
+ build: buildStats,
293
+ error: deployResult.error,
294
+ durationMs: Date.now() - startTime,
295
+ };
296
+ }
297
+
298
+ return {
299
+ success: true,
300
+ branch: {
301
+ name: branch.name,
302
+ id: branch.id,
303
+ token: branch.token,
304
+ url: config.baseUrl,
305
+ created_at: branch.created_at,
306
+ },
307
+ build: buildStats,
308
+ deploy: {
309
+ result: deployResult.result,
310
+ },
311
+ durationMs: Date.now() - startTime,
312
+ };
313
+ }
package/src/cli/index.ts CHANGED
@@ -14,9 +14,11 @@ import { readFileSync } from "node:fs";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { dirname, join, resolve } from "node:path";
16
16
  import { Command } from "commander";
17
+ import pc from "picocolors";
17
18
  import { runInit } from "./commands/init.js";
18
19
  import { runBuild } from "./commands/build.js";
19
20
  import { runDeploy } from "./commands/deploy.js";
21
+ import { runPreview } from "./commands/preview.js";
20
22
  import { runDev } from "./commands/dev.js";
21
23
  import { runLogin } from "./commands/login.js";
22
24
  import {
@@ -31,7 +33,7 @@ import {
31
33
  hasTinybirdSdkDependency,
32
34
  } from "./utils/package-manager.js";
33
35
  import type { DevMode } from "./config.js";
34
- import { output } from "./output.js";
36
+ import { output, type ResourceChange } from "./output.js";
35
37
 
36
38
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
39
  const packageJson = JSON.parse(
@@ -229,32 +231,34 @@ function createCli(): Command {
229
231
  if (deploy.result === "no_changes") {
230
232
  output.showNoChanges();
231
233
  } else {
232
- // Show datasource changes
234
+ // Collect all changes for table display
235
+ const changes: ResourceChange[] = [];
236
+
233
237
  if (deploy.datasources) {
234
238
  for (const name of deploy.datasources.created) {
235
- output.showResourceChange(`${name}.datasource`, "created");
239
+ changes.push({ status: "new", name, type: "datasource" });
236
240
  }
237
241
  for (const name of deploy.datasources.changed) {
238
- output.showResourceChange(`${name}.datasource`, "changed");
242
+ changes.push({ status: "modified", name, type: "datasource" });
239
243
  }
240
244
  for (const name of deploy.datasources.deleted) {
241
- output.showResourceChange(`${name}.datasource`, "deleted");
245
+ changes.push({ status: "deleted", name, type: "datasource" });
242
246
  }
243
247
  }
244
248
 
245
- // Show pipe changes
246
249
  if (deploy.pipes) {
247
250
  for (const name of deploy.pipes.created) {
248
- output.showResourceChange(`${name}.pipe`, "created");
251
+ changes.push({ status: "new", name, type: "pipe" });
249
252
  }
250
253
  for (const name of deploy.pipes.changed) {
251
- output.showResourceChange(`${name}.pipe`, "changed");
254
+ changes.push({ status: "modified", name, type: "pipe" });
252
255
  }
253
256
  for (const name of deploy.pipes.deleted) {
254
- output.showResourceChange(`${name}.pipe`, "deleted");
257
+ changes.push({ status: "deleted", name, type: "pipe" });
255
258
  }
256
259
  }
257
260
 
261
+ output.showChangesTable(changes);
258
262
  output.showBuildSuccess(result.durationMs);
259
263
  }
260
264
  }
@@ -277,6 +281,48 @@ function createCli(): Command {
277
281
  const result = await runDeploy({
278
282
  dryRun: options.dryRun,
279
283
  check: options.check,
284
+ callbacks: {
285
+ onChanges: (deployChanges) => {
286
+ // Show changes table immediately after deployment is created
287
+ const changes: ResourceChange[] = [];
288
+
289
+ for (const name of deployChanges.datasources.created) {
290
+ changes.push({ status: "new", name, type: "datasource" });
291
+ }
292
+ for (const name of deployChanges.datasources.changed) {
293
+ changes.push({ status: "modified", name, type: "datasource" });
294
+ }
295
+ for (const name of deployChanges.datasources.deleted) {
296
+ changes.push({ status: "deleted", name, type: "datasource" });
297
+ }
298
+
299
+ for (const name of deployChanges.pipes.created) {
300
+ changes.push({ status: "new", name, type: "pipe" });
301
+ }
302
+ for (const name of deployChanges.pipes.changed) {
303
+ changes.push({ status: "modified", name, type: "pipe" });
304
+ }
305
+ for (const name of deployChanges.pipes.deleted) {
306
+ changes.push({ status: "deleted", name, type: "pipe" });
307
+ }
308
+
309
+ for (const name of deployChanges.connections.created) {
310
+ changes.push({ status: "new", name, type: "connection" });
311
+ }
312
+ for (const name of deployChanges.connections.changed) {
313
+ changes.push({ status: "modified", name, type: "connection" });
314
+ }
315
+ for (const name of deployChanges.connections.deleted) {
316
+ changes.push({ status: "deleted", name, type: "connection" });
317
+ }
318
+
319
+ output.showChangesTable(changes);
320
+ },
321
+ onWaitingForReady: () => output.showWaitingForDeployment(),
322
+ onDeploymentReady: () => output.showDeploymentReady(),
323
+ onDeploymentLive: (id) => output.showDeploymentLive(id),
324
+ onValidating: () => output.showValidatingDeployment(),
325
+ },
280
326
  });
281
327
 
282
328
  const { build, deploy } = result;
@@ -288,7 +334,7 @@ function createCli(): Command {
288
334
  } else if (result.error) {
289
335
  output.error(result.error);
290
336
  }
291
- output.showBuildFailure();
337
+ output.showDeployFailure();
292
338
  process.exit(1);
293
339
  }
294
340
 
@@ -309,43 +355,107 @@ function createCli(): Command {
309
355
  console.log(pipe.content);
310
356
  });
311
357
  }
312
- output.showBuildSuccess(result.durationMs);
358
+ output.showDeploySuccess(result.durationMs);
313
359
  } else if (options.check) {
314
360
  console.log("\n[Check] Resources validated with Tinybird API");
315
- output.showBuildSuccess(result.durationMs);
361
+ output.showDeploySuccess(result.durationMs);
316
362
  } else if (deploy) {
317
363
  if (deploy.result === "no_changes") {
318
364
  output.showNoChanges();
319
365
  } else {
320
- // Show datasource changes
321
- if (deploy.datasources) {
322
- for (const name of deploy.datasources.created) {
323
- output.showResourceChange(`${name}.datasource`, "created");
324
- }
325
- for (const name of deploy.datasources.changed) {
326
- output.showResourceChange(`${name}.datasource`, "changed");
327
- }
328
- for (const name of deploy.datasources.deleted) {
329
- output.showResourceChange(`${name}.datasource`, "deleted");
330
- }
331
- }
366
+ // Changes table was already shown via onChanges callback
367
+ output.showDeploySuccess(result.durationMs);
368
+ }
369
+ }
370
+ });
332
371
 
333
- // Show pipe changes
334
- if (deploy.pipes) {
335
- for (const name of deploy.pipes.created) {
336
- output.showResourceChange(`${name}.pipe`, "created");
337
- }
338
- for (const name of deploy.pipes.changed) {
339
- output.showResourceChange(`${name}.pipe`, "changed");
340
- }
341
- for (const name of deploy.pipes.deleted) {
342
- output.showResourceChange(`${name}.pipe`, "deleted");
343
- }
344
- }
372
+ // Preview command
373
+ program
374
+ .command("preview")
375
+ .description("Create a preview branch and deploy resources (for CI/testing)")
376
+ .option("--dry-run", "Generate without creating branch or deploying")
377
+ .option("--check", "Validate deploy with Tinybird API without applying")
378
+ .option("--debug", "Show debug output including API requests/responses")
379
+ .option("--json", "Output JSON instead of human-readable format")
380
+ .option("-n, --name <name>", "Override preview branch name")
381
+ .option("--local", "Use local Tinybird container")
382
+ .action(async (options) => {
383
+ if (options.debug) {
384
+ process.env.TINYBIRD_DEBUG = "1";
385
+ }
345
386
 
346
- output.showBuildSuccess(result.durationMs);
387
+ // Determine devMode override
388
+ let devModeOverride: DevMode | undefined;
389
+ if (options.local) {
390
+ devModeOverride = "local";
391
+ }
392
+
393
+ if (!options.json) {
394
+ const modeLabel = devModeOverride === "local" ? " (local)" : "";
395
+ console.log(`Creating preview branch${modeLabel}...\n`);
396
+ }
397
+
398
+ const result = await runPreview({
399
+ dryRun: options.dryRun,
400
+ check: options.check,
401
+ name: options.name,
402
+ devModeOverride,
403
+ });
404
+
405
+ // JSON output mode
406
+ if (options.json) {
407
+ console.log(JSON.stringify(result, null, 2));
408
+ if (!result.success) {
409
+ process.exit(1);
410
+ }
411
+ return;
412
+ }
413
+
414
+ // Human-readable output mode
415
+ if (!result.success) {
416
+ // Parse error message for individual errors (one per line)
417
+ const errorLines = result.error?.split("\n") ?? ["Unknown error"];
418
+ for (const line of errorLines) {
419
+ console.log(pc.red(`- ${line}`));
420
+ }
421
+ console.log("");
422
+ console.log(pc.red(`✗ Preview failed`));
423
+ process.exit(1);
424
+ }
425
+
426
+ // Success output
427
+ const durationSec = (result.durationMs / 1000).toFixed(1);
428
+ if (result.branch) {
429
+ if (options.dryRun) {
430
+ console.log(pc.green(`✓ Preview branch: ${result.branch.name}`));
431
+ console.log(pc.dim(" (dry run - branch not created)"));
432
+ } else {
433
+ console.log(pc.green(`✓ Preview branch: ${result.branch.name}`));
434
+ console.log(pc.dim(` ID: ${result.branch.id}`));
435
+ console.log(pc.dim(` (use --json to get branch token)`));
436
+ }
437
+ }
438
+
439
+ if (result.build) {
440
+ console.log(
441
+ pc.green(`✓ Generated ${result.build.datasourceCount} datasource(s), ${result.build.pipeCount} pipe(s)`)
442
+ );
443
+ }
444
+
445
+ if (options.dryRun) {
446
+ console.log(pc.dim("\n[Dry run] Resources not deployed to API"));
447
+ } else if (options.check) {
448
+ console.log(pc.green("\n✓ Resources validated with Tinybird API"));
449
+ } else if (result.deploy) {
450
+ if (result.deploy.result === "no_changes") {
451
+ console.log(pc.green("✓ No changes detected - already up to date"));
452
+ } else {
453
+ console.log(pc.green("✓ Deployed to preview branch"));
347
454
  }
348
455
  }
456
+
457
+ console.log("");
458
+ console.log(pc.green(`✓ Preview completed in ${durationSec}s`));
349
459
  });
350
460
 
351
461
  // Dev command