@tinybirdco/sdk 0.0.11 → 0.0.13

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 (40) hide show
  1. package/dist/api/branches.d.ts +12 -1
  2. package/dist/api/branches.d.ts.map +1 -1
  3. package/dist/api/branches.js +21 -2
  4. package/dist/api/branches.js.map +1 -1
  5. package/dist/api/branches.test.js +95 -5
  6. package/dist/api/branches.test.js.map +1 -1
  7. package/dist/api/build.d.ts +3 -1
  8. package/dist/api/build.d.ts.map +1 -1
  9. package/dist/api/build.js +2 -0
  10. package/dist/api/build.js.map +1 -1
  11. package/dist/api/local.d.ts +15 -0
  12. package/dist/api/local.d.ts.map +1 -1
  13. package/dist/api/local.js +52 -0
  14. package/dist/api/local.js.map +1 -1
  15. package/dist/api/local.test.js +80 -1
  16. package/dist/api/local.test.js.map +1 -1
  17. package/dist/cli/commands/clear.d.ts +37 -0
  18. package/dist/cli/commands/clear.d.ts.map +1 -0
  19. package/dist/cli/commands/clear.js +141 -0
  20. package/dist/cli/commands/clear.js.map +1 -0
  21. package/dist/cli/index.js +144 -41
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/output.d.ts +88 -0
  24. package/dist/cli/output.d.ts.map +1 -0
  25. package/dist/cli/output.js +150 -0
  26. package/dist/cli/output.js.map +1 -0
  27. package/dist/cli/output.test.d.ts +5 -0
  28. package/dist/cli/output.test.d.ts.map +1 -0
  29. package/dist/cli/output.test.js +119 -0
  30. package/dist/cli/output.test.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/api/branches.test.ts +116 -4
  33. package/src/api/branches.ts +28 -2
  34. package/src/api/build.ts +5 -1
  35. package/src/api/local.test.ts +106 -0
  36. package/src/api/local.ts +77 -0
  37. package/src/cli/commands/clear.ts +194 -0
  38. package/src/cli/index.ts +159 -58
  39. package/src/cli/output.test.ts +144 -0
  40. package/src/cli/output.ts +173 -0
package/src/cli/index.ts CHANGED
@@ -24,12 +24,14 @@ import {
24
24
  runBranchStatus,
25
25
  runBranchDelete,
26
26
  } from "./commands/branch.js";
27
+ import { runClear } from "./commands/clear.js";
27
28
  import {
28
29
  detectPackageManagerInstallCmd,
29
30
  detectPackageManagerRunCmd,
30
31
  hasTinybirdSdkDependency,
31
32
  } from "./utils/package-manager.js";
32
33
  import type { DevMode } from "./config.js";
34
+ import { output } from "./output.js";
33
35
 
34
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
35
37
  const packageJson = JSON.parse(
@@ -37,13 +39,6 @@ const packageJson = JSON.parse(
37
39
  ) as { version: string };
38
40
  const VERSION = packageJson.version;
39
41
 
40
- /**
41
- * Format timestamp for console output
42
- */
43
- function formatTime(): string {
44
- return new Date().toLocaleTimeString("en-US", { hour12: false });
45
- }
46
-
47
42
  /**
48
43
  * Create and configure the CLI
49
44
  */
@@ -192,24 +187,24 @@ function createCli(): Command {
192
187
  }
193
188
 
194
189
  const modeLabel = devModeOverride === "local" ? " (local)" : "";
195
- console.log(`[${formatTime()}] Building${modeLabel}...\n`);
190
+ output.highlight(`Building${modeLabel}...`);
196
191
 
197
192
  const result = await runBuild({
198
193
  dryRun: options.dryRun,
199
194
  devModeOverride,
200
195
  });
201
196
 
202
- if (!result.success) {
203
- console.error(`Error: ${result.error}`);
204
- process.exit(1);
205
- }
206
-
207
197
  const { build, deploy } = result;
208
198
 
209
- if (build) {
210
- console.log(
211
- `Generated ${build.stats.datasourceCount} datasource(s), ${build.stats.pipeCount} pipe(s)`
212
- );
199
+ if (!result.success) {
200
+ // Show detailed errors if available
201
+ if (deploy?.errors && deploy.errors.length > 0) {
202
+ output.showBuildErrors(deploy.errors);
203
+ } else if (result.error) {
204
+ output.error(result.error);
205
+ }
206
+ output.showBuildFailure();
207
+ process.exit(1);
213
208
  }
214
209
 
215
210
  if (options.dryRun) {
@@ -229,15 +224,40 @@ function createCli(): Command {
229
224
  console.log(pipe.content);
230
225
  });
231
226
  }
227
+ output.showBuildSuccess(result.durationMs);
232
228
  } else if (deploy) {
233
229
  if (deploy.result === "no_changes") {
234
- console.log("No changes detected - already up to date");
230
+ output.showNoChanges();
235
231
  } else {
236
- console.log(`Deployed to Tinybird successfully`);
232
+ // Show datasource changes
233
+ if (deploy.datasources) {
234
+ for (const name of deploy.datasources.created) {
235
+ output.showResourceChange(`${name}.datasource`, "created");
236
+ }
237
+ for (const name of deploy.datasources.changed) {
238
+ output.showResourceChange(`${name}.datasource`, "changed");
239
+ }
240
+ for (const name of deploy.datasources.deleted) {
241
+ output.showResourceChange(`${name}.datasource`, "deleted");
242
+ }
243
+ }
244
+
245
+ // Show pipe changes
246
+ if (deploy.pipes) {
247
+ for (const name of deploy.pipes.created) {
248
+ output.showResourceChange(`${name}.pipe`, "created");
249
+ }
250
+ for (const name of deploy.pipes.changed) {
251
+ output.showResourceChange(`${name}.pipe`, "changed");
252
+ }
253
+ for (const name of deploy.pipes.deleted) {
254
+ output.showResourceChange(`${name}.pipe`, "deleted");
255
+ }
256
+ }
257
+
258
+ output.showBuildSuccess(result.durationMs);
237
259
  }
238
260
  }
239
-
240
- console.log(`\n[${formatTime()}] Done in ${result.durationMs}ms`);
241
261
  });
242
262
 
243
263
  // Deploy command
@@ -252,24 +272,24 @@ function createCli(): Command {
252
272
  process.env.TINYBIRD_DEBUG = "1";
253
273
  }
254
274
 
255
- console.log(`[${formatTime()}] Deploying to main workspace...\n`);
275
+ output.highlight("Deploying to main workspace...");
256
276
 
257
277
  const result = await runDeploy({
258
278
  dryRun: options.dryRun,
259
279
  check: options.check,
260
280
  });
261
281
 
262
- if (!result.success) {
263
- console.error(`Error: ${result.error}`);
264
- process.exit(1);
265
- }
266
-
267
282
  const { build, deploy } = result;
268
283
 
269
- if (build) {
270
- console.log(
271
- `Generated ${build.stats.datasourceCount} datasource(s), ${build.stats.pipeCount} pipe(s)`
272
- );
284
+ if (!result.success) {
285
+ // Show detailed errors if available
286
+ if (deploy?.errors && deploy.errors.length > 0) {
287
+ output.showBuildErrors(deploy.errors);
288
+ } else if (result.error) {
289
+ output.error(result.error);
290
+ }
291
+ output.showBuildFailure();
292
+ process.exit(1);
273
293
  }
274
294
 
275
295
  if (options.dryRun) {
@@ -289,17 +309,43 @@ function createCli(): Command {
289
309
  console.log(pipe.content);
290
310
  });
291
311
  }
312
+ output.showBuildSuccess(result.durationMs);
292
313
  } else if (options.check) {
293
314
  console.log("\n[Check] Resources validated with Tinybird API");
315
+ output.showBuildSuccess(result.durationMs);
294
316
  } else if (deploy) {
295
317
  if (deploy.result === "no_changes") {
296
- console.log("No changes detected - already up to date");
318
+ output.showNoChanges();
297
319
  } else {
298
- console.log(`Deployed to main workspace successfully`);
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
+ }
332
+
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
+ }
345
+
346
+ output.showBuildSuccess(result.durationMs);
299
347
  }
300
348
  }
301
-
302
- console.log(`\n[${formatTime()}] Done in ${result.durationMs}ms`);
303
349
  });
304
350
 
305
351
  // Dev command
@@ -317,9 +363,6 @@ function createCli(): Command {
317
363
  devModeOverride = "branch";
318
364
  }
319
365
 
320
- console.log(`tinybird dev v${VERSION}`);
321
- console.log("Loading config from tinybird.json...\n");
322
-
323
366
  try {
324
367
  const controller = await runDev({
325
368
  devModeOverride,
@@ -366,11 +409,18 @@ function createCli(): Command {
366
409
  }
367
410
  },
368
411
  onBuildStart: () => {
369
- console.log(`[${formatTime()}] Building...`);
412
+ output.highlight("Building...");
370
413
  },
371
414
  onBuildComplete: (result) => {
372
415
  if (!result.success) {
373
- console.error(`[${formatTime()}] Build failed: ${result.error}`);
416
+ // Show detailed errors if available
417
+ const { deploy } = result;
418
+ if (deploy?.errors && deploy.errors.length > 0) {
419
+ output.showBuildErrors(deploy.errors);
420
+ } else if (result.error) {
421
+ output.error(result.error);
422
+ }
423
+ output.showBuildFailure(true);
374
424
  return;
375
425
  }
376
426
 
@@ -378,56 +428,52 @@ function createCli(): Command {
378
428
 
379
429
  if (deploy) {
380
430
  if (deploy.result === "no_changes") {
381
- console.log(`[${formatTime()}] No changes detected`);
431
+ output.showNoChanges();
382
432
  } else {
383
- console.log(
384
- `[${formatTime()}] Built in ${result.durationMs}ms`
385
- );
386
-
387
433
  // Show datasource changes
388
434
  if (deploy.datasources) {
389
435
  for (const name of deploy.datasources.created) {
390
- console.log(` + datasource ${name} (created)`);
436
+ output.showResourceChange(`${name}.datasource`, "created");
391
437
  }
392
438
  for (const name of deploy.datasources.changed) {
393
- console.log(` ~ datasource ${name} (changed)`);
439
+ output.showResourceChange(`${name}.datasource`, "changed");
394
440
  }
395
441
  for (const name of deploy.datasources.deleted) {
396
- console.log(` - datasource ${name} (deleted)`);
442
+ output.showResourceChange(`${name}.datasource`, "deleted");
397
443
  }
398
444
  }
399
445
 
400
446
  // Show pipe changes
401
447
  if (deploy.pipes) {
402
448
  for (const name of deploy.pipes.created) {
403
- console.log(` + pipe ${name} (created)`);
449
+ output.showResourceChange(`${name}.pipe`, "created");
404
450
  }
405
451
  for (const name of deploy.pipes.changed) {
406
- console.log(` ~ pipe ${name} (changed)`);
452
+ output.showResourceChange(`${name}.pipe`, "changed");
407
453
  }
408
454
  for (const name of deploy.pipes.deleted) {
409
- console.log(` - pipe ${name} (deleted)`);
455
+ output.showResourceChange(`${name}.pipe`, "deleted");
410
456
  }
411
457
  }
458
+
459
+ output.showBuildSuccess(result.durationMs, true);
412
460
  }
413
461
  }
414
462
  },
415
463
  onSchemaValidation: (validation) => {
416
464
  if (validation.issues.length > 0) {
417
- console.log(`[${formatTime()}] Schema validation:`);
465
+ output.info("Schema validation:");
418
466
  for (const issue of validation.issues) {
419
467
  if (issue.type === "error") {
420
- console.error(
421
- ` ERROR [${issue.pipeName}]: ${issue.message}`
422
- );
468
+ output.error(` ERROR [${issue.pipeName}]: ${issue.message}`);
423
469
  } else {
424
- console.warn(` WARN [${issue.pipeName}]: ${issue.message}`);
470
+ output.warning(` WARN [${issue.pipeName}]: ${issue.message}`);
425
471
  }
426
472
  }
427
473
  }
428
474
  },
429
- onError: (error) => {
430
- console.error(`[${formatTime()}] Error: ${error.message}`);
475
+ onError: (err) => {
476
+ output.error(err.message);
431
477
  },
432
478
  });
433
479
 
@@ -532,6 +578,61 @@ function createCli(): Command {
532
578
 
533
579
  program.addCommand(branchCommand);
534
580
 
581
+ // Clear command
582
+ program
583
+ .command("clear")
584
+ .description("Clear the workspace or branch by deleting and recreating it")
585
+ .option("-y, --yes", "Skip confirmation prompt")
586
+ .option("--local", "Use local Tinybird container")
587
+ .option("--branch", "Use Tinybird cloud with branches")
588
+ .action(async (options) => {
589
+ // Determine devMode override
590
+ let devModeOverride: DevMode | undefined;
591
+ if (options.local) {
592
+ devModeOverride = "local";
593
+ } else if (options.branch) {
594
+ devModeOverride = "branch";
595
+ }
596
+
597
+ const modeLabel = devModeOverride === "local" ? "local workspace" : "branch";
598
+
599
+ // Confirmation prompt unless --yes is passed
600
+ if (!options.yes) {
601
+ const readline = await import("readline");
602
+ const rl = readline.createInterface({
603
+ input: process.stdin,
604
+ output: process.stdout,
605
+ });
606
+
607
+ const answer = await new Promise<string>((resolve) => {
608
+ rl.question(
609
+ `Are you sure you want to clear the ${modeLabel}? This will delete all resources. [y/N]: `,
610
+ (ans) => {
611
+ rl.close();
612
+ resolve(ans);
613
+ }
614
+ );
615
+ });
616
+
617
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
618
+ console.log("Aborted.");
619
+ return;
620
+ }
621
+ }
622
+
623
+ console.log(`Clearing ${modeLabel}...`);
624
+
625
+ const result = await runClear({ devModeOverride });
626
+
627
+ if (!result.success) {
628
+ console.error(`Error: ${result.error}`);
629
+ process.exit(1);
630
+ }
631
+
632
+ const typeLabel = result.isLocal ? "Workspace" : "Branch";
633
+ console.log(`${typeLabel} '${result.name}' cleared successfully.`);
634
+ });
635
+
535
636
  return program;
536
637
  }
537
638
 
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Tests for CLI output utilities
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
6
+ import {
7
+ formatDuration,
8
+ showResourceChange,
9
+ showBuildErrors,
10
+ showBuildSuccess,
11
+ showBuildFailure,
12
+ showNoChanges,
13
+ } from "./output.js";
14
+
15
+ describe("output utilities", () => {
16
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
17
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
18
+
19
+ beforeEach(() => {
20
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
21
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
22
+ });
23
+
24
+ afterEach(() => {
25
+ consoleLogSpy.mockRestore();
26
+ consoleErrorSpy.mockRestore();
27
+ });
28
+
29
+ describe("formatDuration", () => {
30
+ it("formats milliseconds for durations under 1 second", () => {
31
+ expect(formatDuration(500)).toBe("500ms");
32
+ expect(formatDuration(0)).toBe("0ms");
33
+ expect(formatDuration(999)).toBe("999ms");
34
+ });
35
+
36
+ it("formats seconds for durations 1 second or more", () => {
37
+ expect(formatDuration(1000)).toBe("1.0s");
38
+ expect(formatDuration(1500)).toBe("1.5s");
39
+ expect(formatDuration(2345)).toBe("2.3s");
40
+ expect(formatDuration(10000)).toBe("10.0s");
41
+ });
42
+ });
43
+
44
+ describe("showResourceChange", () => {
45
+ it("shows created resource", () => {
46
+ showResourceChange("events.datasource", "created");
47
+ expect(consoleLogSpy).toHaveBeenCalledWith("✓ events.datasource created");
48
+ });
49
+
50
+ it("shows changed resource", () => {
51
+ showResourceChange("top_pages.pipe", "changed");
52
+ expect(consoleLogSpy).toHaveBeenCalledWith("✓ top_pages.pipe changed");
53
+ });
54
+
55
+ it("shows deleted resource", () => {
56
+ showResourceChange("old_data.datasource", "deleted");
57
+ expect(consoleLogSpy).toHaveBeenCalledWith("✓ old_data.datasource deleted");
58
+ });
59
+ });
60
+
61
+ describe("showBuildErrors", () => {
62
+ it("shows errors with filename", () => {
63
+ showBuildErrors([
64
+ { filename: "events.datasource", error: "Invalid column type" },
65
+ ]);
66
+ expect(consoleErrorSpy).toHaveBeenCalledWith("events.datasource");
67
+ expect(consoleErrorSpy).toHaveBeenCalledWith(" Invalid column type");
68
+ });
69
+
70
+ it("shows errors without filename", () => {
71
+ showBuildErrors([{ error: "General build error" }]);
72
+ expect(consoleErrorSpy).toHaveBeenCalledWith("General build error");
73
+ });
74
+
75
+ it("shows multiple errors", () => {
76
+ showBuildErrors([
77
+ { filename: "a.datasource", error: "Error A" },
78
+ { filename: "b.pipe", error: "Error B" },
79
+ ]);
80
+ expect(consoleErrorSpy).toHaveBeenCalledWith("a.datasource");
81
+ expect(consoleErrorSpy).toHaveBeenCalledWith(" Error A");
82
+ expect(consoleErrorSpy).toHaveBeenCalledWith("b.pipe");
83
+ expect(consoleErrorSpy).toHaveBeenCalledWith(" Error B");
84
+ });
85
+
86
+ it("handles multi-line errors", () => {
87
+ showBuildErrors([
88
+ { filename: "test.pipe", error: "Line 1\nLine 2\nLine 3" },
89
+ ]);
90
+ expect(consoleErrorSpy).toHaveBeenCalledWith("test.pipe");
91
+ expect(consoleErrorSpy).toHaveBeenCalledWith(" Line 1");
92
+ expect(consoleErrorSpy).toHaveBeenCalledWith(" Line 2");
93
+ expect(consoleErrorSpy).toHaveBeenCalledWith(" Line 3");
94
+ });
95
+ });
96
+
97
+ describe("showBuildSuccess", () => {
98
+ it("shows build success with duration in ms", () => {
99
+ showBuildSuccess(500);
100
+ expect(consoleLogSpy).toHaveBeenCalled();
101
+ const call = consoleLogSpy.mock.calls[0][0];
102
+ expect(call).toContain("✓");
103
+ expect(call).toContain("Build completed in 500ms");
104
+ });
105
+
106
+ it("shows build success with duration in seconds", () => {
107
+ showBuildSuccess(2500);
108
+ expect(consoleLogSpy).toHaveBeenCalled();
109
+ const call = consoleLogSpy.mock.calls[0][0];
110
+ expect(call).toContain("Build completed in 2.5s");
111
+ });
112
+
113
+ it("shows rebuild success when isRebuild is true", () => {
114
+ showBuildSuccess(1000, true);
115
+ expect(consoleLogSpy).toHaveBeenCalled();
116
+ const call = consoleLogSpy.mock.calls[0][0];
117
+ expect(call).toContain("Rebuild completed in 1.0s");
118
+ });
119
+ });
120
+
121
+ describe("showBuildFailure", () => {
122
+ it("shows build failure", () => {
123
+ showBuildFailure();
124
+ expect(consoleErrorSpy).toHaveBeenCalled();
125
+ const call = consoleErrorSpy.mock.calls[0][0];
126
+ expect(call).toContain("✗");
127
+ expect(call).toContain("Build failed");
128
+ });
129
+
130
+ it("shows rebuild failure when isRebuild is true", () => {
131
+ showBuildFailure(true);
132
+ expect(consoleErrorSpy).toHaveBeenCalled();
133
+ const call = consoleErrorSpy.mock.calls[0][0];
134
+ expect(call).toContain("Rebuild failed");
135
+ });
136
+ });
137
+
138
+ describe("showNoChanges", () => {
139
+ it("shows no changes message", () => {
140
+ showNoChanges();
141
+ expect(consoleLogSpy).toHaveBeenCalledWith("No changes. Build skipped.");
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Console output utilities with color support
3
+ * Provides consistent formatting similar to the Python CLI
4
+ */
5
+
6
+ // ANSI color codes
7
+ const colors = {
8
+ reset: "\x1b[0m",
9
+ bold: "\x1b[1m",
10
+ red: "\x1b[91m",
11
+ green: "\x1b[92m",
12
+ yellow: "\x1b[38;5;208m",
13
+ blue: "\x1b[94m",
14
+ gray: "\x1b[90m",
15
+ } as const;
16
+
17
+ // Check if colors should be disabled
18
+ const noColor = process.env.NO_COLOR !== undefined || !process.stdout.isTTY;
19
+
20
+ function colorize(text: string, color: keyof typeof colors): string {
21
+ if (noColor) return text;
22
+ return `${colors[color]}${text}${colors.reset}`;
23
+ }
24
+
25
+ /**
26
+ * Output a success message (green)
27
+ */
28
+ export function success(message: string): void {
29
+ console.log(colorize(message, "green"));
30
+ }
31
+
32
+ /**
33
+ * Output an error message (red)
34
+ */
35
+ export function error(message: string): void {
36
+ console.error(colorize(message, "red"));
37
+ }
38
+
39
+ /**
40
+ * Output a warning message (yellow/orange)
41
+ */
42
+ export function warning(message: string): void {
43
+ console.log(colorize(message, "yellow"));
44
+ }
45
+
46
+ /**
47
+ * Output an info message (default color)
48
+ */
49
+ export function info(message: string): void {
50
+ console.log(message);
51
+ }
52
+
53
+ /**
54
+ * Output a highlighted message (blue)
55
+ */
56
+ export function highlight(message: string): void {
57
+ console.log(colorize(message, "blue"));
58
+ }
59
+
60
+ /**
61
+ * Output a gray message (dimmed)
62
+ */
63
+ export function gray(message: string): void {
64
+ console.log(colorize(message, "gray"));
65
+ }
66
+
67
+ /**
68
+ * Output a bold message
69
+ */
70
+ export function bold(message: string): void {
71
+ console.log(colorize(message, "bold"));
72
+ }
73
+
74
+ /**
75
+ * Format a timestamp for console output
76
+ */
77
+ export function formatTime(): string {
78
+ return new Date().toLocaleTimeString("en-US", { hour12: false });
79
+ }
80
+
81
+ /**
82
+ * Format duration in human-readable format
83
+ */
84
+ export function formatDuration(ms: number): string {
85
+ if (ms < 1000) {
86
+ return `${ms}ms`;
87
+ }
88
+ return `${(ms / 1000).toFixed(1)}s`;
89
+ }
90
+
91
+ /**
92
+ * Show a resource change (checkmark + path + status)
93
+ */
94
+ export function showResourceChange(
95
+ path: string,
96
+ status: "created" | "changed" | "deleted"
97
+ ): void {
98
+ console.log(`✓ ${path} ${status}`);
99
+ }
100
+
101
+ /**
102
+ * Show a warning for a resource
103
+ */
104
+ export function showResourceWarning(
105
+ level: string,
106
+ resource: string,
107
+ message: string
108
+ ): void {
109
+ warning(`△ ${level}: ${resource}: ${message}`);
110
+ }
111
+
112
+ /**
113
+ * Show build errors in formatted style
114
+ */
115
+ export function showBuildErrors(errors: Array<{ filename?: string; error: string }>): void {
116
+ for (const err of errors) {
117
+ if (err.filename) {
118
+ error(`${err.filename}`);
119
+ // Indent the error message
120
+ const lines = err.error.split("\n");
121
+ for (const line of lines) {
122
+ error(` ${line}`);
123
+ }
124
+ } else {
125
+ error(err.error);
126
+ }
127
+ console.log(); // Empty line between errors
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Show final build success message
133
+ */
134
+ export function showBuildSuccess(durationMs: number, isRebuild = false): void {
135
+ const prefix = isRebuild ? "Rebuild" : "Build";
136
+ success(`\n✓ ${prefix} completed in ${formatDuration(durationMs)}`);
137
+ }
138
+
139
+ /**
140
+ * Show final build failure message
141
+ */
142
+ export function showBuildFailure(isRebuild = false): void {
143
+ const prefix = isRebuild ? "Rebuild" : "Build";
144
+ error(`\n✗ ${prefix} failed`);
145
+ }
146
+
147
+ /**
148
+ * Show no changes message
149
+ */
150
+ export function showNoChanges(): void {
151
+ info("No changes. Build skipped.");
152
+ }
153
+
154
+ /**
155
+ * Output object containing all output functions
156
+ */
157
+ export const output = {
158
+ success,
159
+ error,
160
+ warning,
161
+ info,
162
+ highlight,
163
+ gray,
164
+ bold,
165
+ formatTime,
166
+ formatDuration,
167
+ showResourceChange,
168
+ showResourceWarning,
169
+ showBuildErrors,
170
+ showBuildSuccess,
171
+ showBuildFailure,
172
+ showNoChanges,
173
+ };