@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.
- package/dist/api/branches.d.ts +12 -1
- package/dist/api/branches.d.ts.map +1 -1
- package/dist/api/branches.js +21 -2
- package/dist/api/branches.js.map +1 -1
- package/dist/api/branches.test.js +95 -5
- package/dist/api/branches.test.js.map +1 -1
- package/dist/api/build.d.ts +3 -1
- package/dist/api/build.d.ts.map +1 -1
- package/dist/api/build.js +2 -0
- package/dist/api/build.js.map +1 -1
- package/dist/api/local.d.ts +15 -0
- package/dist/api/local.d.ts.map +1 -1
- package/dist/api/local.js +52 -0
- package/dist/api/local.js.map +1 -1
- package/dist/api/local.test.js +80 -1
- package/dist/api/local.test.js.map +1 -1
- package/dist/cli/commands/clear.d.ts +37 -0
- package/dist/cli/commands/clear.d.ts.map +1 -0
- package/dist/cli/commands/clear.js +141 -0
- package/dist/cli/commands/clear.js.map +1 -0
- package/dist/cli/index.js +144 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts +88 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +150 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/output.test.d.ts +5 -0
- package/dist/cli/output.test.d.ts.map +1 -0
- package/dist/cli/output.test.js +119 -0
- package/dist/cli/output.test.js.map +1 -0
- package/package.json +1 -1
- package/src/api/branches.test.ts +116 -4
- package/src/api/branches.ts +28 -2
- package/src/api/build.ts +5 -1
- package/src/api/local.test.ts +106 -0
- package/src/api/local.ts +77 -0
- package/src/cli/commands/clear.ts +194 -0
- package/src/cli/index.ts +159 -58
- package/src/cli/output.test.ts +144 -0
- 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
|
-
|
|
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 (
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
230
|
+
output.showNoChanges();
|
|
235
231
|
} else {
|
|
236
|
-
|
|
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
|
-
|
|
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 (
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
318
|
+
output.showNoChanges();
|
|
297
319
|
} else {
|
|
298
|
-
|
|
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
|
-
|
|
412
|
+
output.highlight("Building...");
|
|
370
413
|
},
|
|
371
414
|
onBuildComplete: (result) => {
|
|
372
415
|
if (!result.success) {
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
+
output.showResourceChange(`${name}.datasource`, "created");
|
|
391
437
|
}
|
|
392
438
|
for (const name of deploy.datasources.changed) {
|
|
393
|
-
|
|
439
|
+
output.showResourceChange(`${name}.datasource`, "changed");
|
|
394
440
|
}
|
|
395
441
|
for (const name of deploy.datasources.deleted) {
|
|
396
|
-
|
|
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
|
-
|
|
449
|
+
output.showResourceChange(`${name}.pipe`, "created");
|
|
404
450
|
}
|
|
405
451
|
for (const name of deploy.pipes.changed) {
|
|
406
|
-
|
|
452
|
+
output.showResourceChange(`${name}.pipe`, "changed");
|
|
407
453
|
}
|
|
408
454
|
for (const name of deploy.pipes.deleted) {
|
|
409
|
-
|
|
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
|
-
|
|
465
|
+
output.info("Schema validation:");
|
|
418
466
|
for (const issue of validation.issues) {
|
|
419
467
|
if (issue.type === "error") {
|
|
420
|
-
|
|
421
|
-
` ERROR [${issue.pipeName}]: ${issue.message}`
|
|
422
|
-
);
|
|
468
|
+
output.error(` ERROR [${issue.pipeName}]: ${issue.message}`);
|
|
423
469
|
} else {
|
|
424
|
-
|
|
470
|
+
output.warning(` WARN [${issue.pipeName}]: ${issue.message}`);
|
|
425
471
|
}
|
|
426
472
|
}
|
|
427
473
|
}
|
|
428
474
|
},
|
|
429
|
-
onError: (
|
|
430
|
-
|
|
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
|
+
};
|