Skip to content

Commit 1c67396

Browse files
committed
cp dines
1 parent cb0aa79 commit 1c67396

2 files changed

Lines changed: 260 additions & 4 deletions

File tree

src/commands/blueprint/logs.ts

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
* Get blueprint build logs command
33
*/
44

5+
import chalk from "chalk";
6+
import type {
7+
BlueprintBuildLogsListView,
8+
BlueprintBuildLog,
9+
} from "@runloop/api-client/resources/blueprints";
510
import { getClient } from "../../utils/client.js";
611
import { output, outputError } from "../../utils/output.js";
712

@@ -10,13 +15,138 @@ interface BlueprintLogsOptions {
1015
output?: string;
1116
}
1217

18+
function formatLogLevel(level: string): string {
19+
const normalized = level.toUpperCase();
20+
switch (normalized) {
21+
case "ERROR":
22+
case "ERR":
23+
return chalk.red.bold("ERROR");
24+
case "WARN":
25+
case "WARNING":
26+
return chalk.yellow.bold("WARN ");
27+
case "INFO":
28+
return chalk.blue("INFO ");
29+
case "DEBUG":
30+
return chalk.gray("DEBUG");
31+
default:
32+
return chalk.gray(normalized.padEnd(5));
33+
}
34+
}
35+
36+
function formatTimestamp(timestampMs: number): string {
37+
const date = new Date(timestampMs);
38+
const now = new Date();
39+
40+
const isToday = date.toDateString() === now.toDateString();
41+
const isThisYear = date.getFullYear() === now.getFullYear();
42+
43+
const time = date.toLocaleTimeString("en-US", {
44+
hour12: false,
45+
hour: "2-digit",
46+
minute: "2-digit",
47+
second: "2-digit",
48+
});
49+
const ms = date.getMilliseconds().toString().padStart(3, "0");
50+
51+
if (isToday) {
52+
// Today: show time with milliseconds for fine granularity
53+
return chalk.dim(`${time}.${ms}`);
54+
} else if (isThisYear) {
55+
// This year: show "Jan 5 15:44:03"
56+
const monthDay = date.toLocaleDateString("en-US", {
57+
month: "short",
58+
day: "numeric",
59+
});
60+
return chalk.dim(`${monthDay} ${time}`);
61+
} else {
62+
// Older: show "Jan 5, 2024 15:44:03"
63+
const fullDate = date.toLocaleDateString("en-US", {
64+
year: "numeric",
65+
month: "short",
66+
day: "numeric",
67+
});
68+
return chalk.dim(`${fullDate} ${time}`);
69+
}
70+
}
71+
72+
function colorizeMessage(message: string): string {
73+
// Colorize common Docker build patterns
74+
if (message.startsWith("Step ") || message.startsWith("---> ")) {
75+
return chalk.cyan.bold(message);
76+
}
77+
if (message.startsWith("Successfully")) {
78+
return chalk.green.bold(message);
79+
}
80+
if (message.startsWith("Removing intermediate container")) {
81+
return chalk.dim(message);
82+
}
83+
if (
84+
message.toLowerCase().includes("error") ||
85+
message.toLowerCase().includes("failed")
86+
) {
87+
return chalk.red(message);
88+
}
89+
if (message.toLowerCase().includes("warning")) {
90+
return chalk.yellow(message);
91+
}
92+
// Dockerfile instructions
93+
if (
94+
message.startsWith("RUN ") ||
95+
message.startsWith("COPY ") ||
96+
message.startsWith("ADD ") ||
97+
message.startsWith("FROM ") ||
98+
message.startsWith("WORKDIR ") ||
99+
message.startsWith("ENV ")
100+
) {
101+
return chalk.yellow(message);
102+
}
103+
return message;
104+
}
105+
106+
function formatLogEntry(log: BlueprintBuildLog): string {
107+
const parts: string[] = [];
108+
109+
// Timestamp
110+
parts.push(formatTimestamp(log.timestamp_ms));
111+
112+
// Level
113+
parts.push(formatLogLevel(log.level));
114+
115+
// Message with colorization
116+
parts.push(colorizeMessage(log.message));
117+
118+
return parts.join(" ");
119+
}
120+
121+
function formatLogs(response: BlueprintBuildLogsListView): void {
122+
const logs = response.logs;
123+
124+
if (!logs || logs.length === 0) {
125+
console.log(chalk.dim("No build logs available"));
126+
return;
127+
}
128+
129+
console.log(
130+
chalk.bold.underline(`Blueprint Build Logs (${response.blueprint_id})\n`),
131+
);
132+
133+
for (const log of logs) {
134+
console.log(formatLogEntry(log));
135+
}
136+
}
137+
13138
export async function getBlueprintLogs(options: BlueprintLogsOptions) {
14139
try {
15140
const client = getClient();
16141
const logs = await client.blueprints.logs(options.id);
17-
output(logs, { format: options.output, defaultFormat: "json" });
142+
143+
// Pretty print for text output, JSON for others
144+
if (!options.output || options.output === "text") {
145+
formatLogs(logs);
146+
} else {
147+
output(logs, { format: options.output, defaultFormat: "json" });
148+
}
18149
} catch (error) {
19150
outputError("Failed to get blueprint logs", error);
20151
}
21152
}
22-

src/commands/devbox/logs.ts

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,146 @@
22
* Get devbox logs command
33
*/
44

5+
import chalk from "chalk";
6+
import type { DevboxLogsListView } from "@runloop/api-client/resources/devboxes/logs";
57
import { getClient } from "../../utils/client.js";
68
import { output, outputError } from "../../utils/output.js";
79

10+
type DevboxLog = DevboxLogsListView["logs"][number];
11+
812
interface LogsOptions {
913
output?: string;
1014
}
1115

16+
function formatLogLevel(level: string): string {
17+
const normalized = level.toUpperCase();
18+
switch (normalized) {
19+
case "ERROR":
20+
case "ERR":
21+
return chalk.red.bold("ERROR");
22+
case "WARN":
23+
case "WARNING":
24+
return chalk.yellow.bold("WARN ");
25+
case "INFO":
26+
return chalk.blue("INFO ");
27+
case "DEBUG":
28+
return chalk.gray("DEBUG");
29+
default:
30+
return chalk.gray(normalized.padEnd(5));
31+
}
32+
}
33+
34+
function formatSource(source: string | null | undefined): string {
35+
if (!source) return chalk.dim("[system]");
36+
37+
const colors: Record<string, (s: string) => string> = {
38+
setup_commands: chalk.magenta,
39+
entrypoint: chalk.cyan,
40+
exec: chalk.green,
41+
files: chalk.yellow,
42+
stats: chalk.gray,
43+
};
44+
const colorFn = colors[source] || chalk.white;
45+
return colorFn(`[${source}]`);
46+
}
47+
48+
function formatTimestamp(timestampMs: number): string {
49+
const date = new Date(timestampMs);
50+
const now = new Date();
51+
52+
const isToday = date.toDateString() === now.toDateString();
53+
const isThisYear = date.getFullYear() === now.getFullYear();
54+
55+
const time = date.toLocaleTimeString("en-US", {
56+
hour12: false,
57+
hour: "2-digit",
58+
minute: "2-digit",
59+
second: "2-digit",
60+
});
61+
const ms = date.getMilliseconds().toString().padStart(3, "0");
62+
63+
if (isToday) {
64+
// Today: show time with milliseconds for fine granularity
65+
return chalk.dim(`${time}.${ms}`);
66+
} else if (isThisYear) {
67+
// This year: show "Jan 5 15:44:03"
68+
const monthDay = date.toLocaleDateString("en-US", {
69+
month: "short",
70+
day: "numeric",
71+
});
72+
return chalk.dim(`${monthDay} ${time}`);
73+
} else {
74+
// Older: show "Jan 5, 2024 15:44:03"
75+
const fullDate = date.toLocaleDateString("en-US", {
76+
year: "numeric",
77+
month: "short",
78+
day: "numeric",
79+
});
80+
return chalk.dim(`${fullDate} ${time}`);
81+
}
82+
}
83+
84+
function formatLogEntry(log: DevboxLog): string {
85+
const parts: string[] = [];
86+
87+
// Timestamp
88+
parts.push(formatTimestamp(log.timestamp_ms));
89+
90+
// Level
91+
parts.push(formatLogLevel(log.level));
92+
93+
// Source
94+
parts.push(formatSource(log.source as string));
95+
96+
// Shell name if present
97+
if (log.shell_name) {
98+
parts.push(chalk.dim(`(${log.shell_name})`));
99+
}
100+
101+
// Command if present
102+
if (log.cmd) {
103+
parts.push(chalk.cyan("$") + " " + chalk.white(log.cmd));
104+
}
105+
106+
// Message if present
107+
if (log.message) {
108+
parts.push(log.message);
109+
}
110+
111+
// Exit code if present
112+
if (log.exit_code !== undefined && log.exit_code !== null) {
113+
const exitColor = log.exit_code === 0 ? chalk.green : chalk.red;
114+
parts.push(exitColor(`exit=${log.exit_code}`));
115+
}
116+
117+
return parts.join(" ");
118+
}
119+
120+
function formatLogs(response: DevboxLogsListView): void {
121+
const logs = response.logs;
122+
123+
if (!logs || logs.length === 0) {
124+
console.log(chalk.dim("No logs available"));
125+
return;
126+
}
127+
128+
for (const log of logs) {
129+
console.log(formatLogEntry(log));
130+
}
131+
}
132+
12133
export async function getLogs(devboxId: string, options: LogsOptions = {}) {
13134
try {
14135
const client = getClient();
15136
const logs = await client.devboxes.logs.list(devboxId);
16-
output(logs, { format: options.output, defaultFormat: "json" });
137+
138+
// Pretty print for text output, JSON for others
139+
if (!options.output || options.output === "text") {
140+
formatLogs(logs);
141+
} else {
142+
output(logs, { format: options.output, defaultFormat: "json" });
143+
}
17144
} catch (error) {
18145
outputError("Failed to get devbox logs", error);
19146
}
20147
}
21-

0 commit comments

Comments
 (0)