Skip to content

Commit 1852abe

Browse files
committed
cp dines
1 parent 1c67396 commit 1852abe

5 files changed

Lines changed: 292 additions & 216 deletions

File tree

src/commands/blueprint/logs.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,6 @@ function formatLogs(response: BlueprintBuildLogsListView): void {
126126
return;
127127
}
128128

129-
console.log(
130-
chalk.bold.underline(`Blueprint Build Logs (${response.blueprint_id})\n`),
131-
);
132-
133129
for (const log of logs) {
134130
console.log(formatLogEntry(log));
135131
}

src/commands/devbox/logs.ts

Lines changed: 3 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -2,142 +2,23 @@
22
* Get devbox logs command
33
*/
44

5-
import chalk from "chalk";
65
import type { DevboxLogsListView } from "@runloop/api-client/resources/devboxes/logs";
76
import { getClient } from "../../utils/client.js";
87
import { output, outputError } from "../../utils/output.js";
9-
10-
type DevboxLog = DevboxLogsListView["logs"][number];
8+
import { formatLogsForCLI } from "../../utils/logFormatter.js";
119

1210
interface LogsOptions {
1311
output?: string;
1412
}
1513

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-
13314
export async function getLogs(devboxId: string, options: LogsOptions = {}) {
13415
try {
13516
const client = getClient();
136-
const logs = await client.devboxes.logs.list(devboxId);
17+
const logs: DevboxLogsListView = await client.devboxes.logs.list(devboxId);
13718

13819
// Pretty print for text output, JSON for others
13920
if (!options.output || options.output === "text") {
140-
formatLogs(logs);
21+
formatLogsForCLI(logs);
14122
} else {
14223
output(logs, { format: options.output, defaultFormat: "json" });
14324
}

src/components/DevboxActionsMenu.tsx

Lines changed: 83 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
createTunnel,
2323
createSSHKey,
2424
} from "../services/devboxService.js";
25+
import { parseLogEntry, formatTimestamp } from "../utils/logFormatter.js";
2526

2627
type Operation =
2728
| "exec"
@@ -419,20 +420,15 @@ export const DevboxActionsMenu = ({
419420
typeof operationResult === "object" &&
420421
(operationResult as any).__customRender === "logs"
421422
) {
422-
// Copy logs to clipboard
423+
// Copy logs to clipboard using shared formatter
423424
const logs = (operationResult as any).__logs || [];
424425
const logsText = logs
425426
.map((log: any) => {
426-
const time = new Date(log.timestamp_ms).toLocaleString();
427-
const level = log.level || "INFO";
428-
const source = log.source || "exec";
429-
const message = log.message || "";
430-
const cmd = log.cmd ? `[${log.cmd}] ` : "";
431-
const exitCode =
432-
log.exit_code !== null && log.exit_code !== undefined
433-
? `(${log.exit_code}) `
434-
: "";
435-
return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
427+
const parts = parseLogEntry(log);
428+
const cmd = parts.cmd ? `$ ${parts.cmd} ` : "";
429+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
430+
const shell = parts.shellName ? `(${parts.shellName}) ` : "";
431+
return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
436432
})
437433
.join("\n");
438434

@@ -846,94 +842,123 @@ export const DevboxActionsMenu = ({
846842
paddingX={1}
847843
>
848844
{visibleLogs.map((log: any, index: number) => {
849-
const time = new Date(log.timestamp_ms).toLocaleTimeString();
850-
const level = log.level ? log.level[0].toUpperCase() : "I";
851-
const source = log.source ? log.source.substring(0, 8) : "exec";
852-
// Sanitize message: escape special chars to prevent layout breaks while preserving visibility
853-
const rawMessage = log.message || "";
854-
const escapedMessage = rawMessage
855-
.replace(/\r\n/g, "\\n") // Windows line endings
856-
.replace(/\n/g, "\\n") // Unix line endings
857-
.replace(/\r/g, "\\r") // Old Mac line endings
858-
.replace(/\t/g, "\\t"); // Tabs
845+
const parts = parseLogEntry(log);
846+
847+
// Sanitize message: escape special chars to prevent layout breaks
848+
const escapedMessage = parts.message
849+
.replace(/\r\n/g, "\\n")
850+
.replace(/\n/g, "\\n")
851+
.replace(/\r/g, "\\r")
852+
.replace(/\t/g, "\\t");
853+
859854
// Limit message length to prevent Yoga layout engine errors
860855
const MAX_MESSAGE_LENGTH = 1000;
861856
const fullMessage =
862857
escapedMessage.length > MAX_MESSAGE_LENGTH
863858
? escapedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
864859
: escapedMessage;
865-
const cmd = log.cmd
866-
? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? "..." : ""}] `
860+
861+
const cmd = parts.cmd
862+
? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
867863
: "";
868864
const exitCode =
869-
log.exit_code !== null && log.exit_code !== undefined
870-
? `(${log.exit_code}) `
871-
: "";
872-
873-
let levelColor: string = colors.textDim;
874-
if (level === "E") levelColor = colors.error;
875-
else if (level === "W") levelColor = colors.warning;
876-
else if (level === "I") levelColor = colors.primary;
865+
parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
866+
867+
// Map color names to theme colors
868+
const levelColorMap: Record<string, string> = {
869+
red: colors.error,
870+
yellow: colors.warning,
871+
blue: colors.primary,
872+
gray: colors.textDim,
873+
};
874+
const sourceColorMap: Record<string, string> = {
875+
magenta: "#d33682",
876+
cyan: colors.info,
877+
green: colors.success,
878+
yellow: colors.warning,
879+
gray: colors.textDim,
880+
white: colors.text,
881+
};
882+
const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
883+
const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
877884

878885
if (logsWrapMode) {
879886
return (
880887
<Box key={index}>
881888
<Text color={colors.textDim} dimColor>
882-
{time}
889+
{parts.timestamp}
883890
</Text>
884891
<Text> </Text>
885-
<Text color={levelColor} bold>
886-
{level}
887-
</Text>
888-
<Text color={colors.textDim} dimColor>
889-
/{source}
892+
<Text color={levelColor} bold={parts.levelColor === "red"}>
893+
{parts.level}
890894
</Text>
891895
<Text> </Text>
892-
{exitCode && <Text color={colors.warning}>{exitCode}</Text>}
896+
<Text color={sourceColor}>[{parts.source}]</Text>
897+
<Text> </Text>
898+
{parts.shellName && (
899+
<Text color={colors.textDim} dimColor>
900+
({parts.shellName}){" "}
901+
</Text>
902+
)}
893903
{cmd && (
894-
<Text color={colors.info} dimColor>
904+
<Text color={colors.info}>
895905
{cmd}
896906
</Text>
897907
)}
898908
<Text>{fullMessage}</Text>
909+
{exitCode && (
910+
<Text color={parts.exitCode === 0 ? colors.success : colors.error}>
911+
{" "}{exitCode}
912+
</Text>
913+
)}
899914
</Box>
900915
);
901916
} else {
902-
// CRITICAL: Validate all lengths and ensure positive values for Yoga
903-
const exitCodeLen = typeof exitCode === 'string' ? exitCode.length : 0;
904-
const cmdLen = typeof cmd === 'string' ? cmd.length : 0;
905-
const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCodeLen + cmdLen + 6;
906-
// Ensure terminalWidth is valid and availableMessageWidth is always positive
917+
// Calculate available width for message truncation
918+
const timestampLen = parts.timestamp.length;
919+
const levelLen = parts.level.length;
920+
const sourceLen = parts.source.length + 2; // brackets
921+
const shellLen = parts.shellName ? parts.shellName.length + 3 : 0;
922+
const cmdLen = cmd.length;
923+
const exitLen = exitCode.length;
924+
const spacesLen = 5; // spaces between elements
925+
const metadataWidth = timestampLen + levelLen + sourceLen + shellLen + cmdLen + exitLen + spacesLen;
926+
907927
const safeTerminalWidth = Math.max(80, terminalWidth);
908-
const availableMessageWidth = Math.max(
909-
20,
910-
Math.floor(safeTerminalWidth - metadataWidth),
911-
);
928+
const availableMessageWidth = Math.max(20, safeTerminalWidth - metadataWidth);
912929
const truncatedMessage =
913930
fullMessage.length > availableMessageWidth
914-
? fullMessage.substring(0, Math.max(1, availableMessageWidth - 3)) +
915-
"..."
931+
? fullMessage.substring(0, Math.max(1, availableMessageWidth - 3)) + "..."
916932
: fullMessage;
933+
917934
return (
918935
<Box key={index}>
919936
<Text color={colors.textDim} dimColor>
920-
{time}
937+
{parts.timestamp}
921938
</Text>
922939
<Text> </Text>
923-
<Text color={levelColor} bold>
924-
{level}
925-
</Text>
926-
<Text color={colors.textDim} dimColor>
927-
/{source}
940+
<Text color={levelColor} bold={parts.levelColor === "red"}>
941+
{parts.level}
928942
</Text>
929943
<Text> </Text>
930-
{exitCode && <Text color={colors.warning}>{exitCode}</Text>}
944+
<Text color={sourceColor}>[{parts.source}]</Text>
945+
<Text> </Text>
946+
{parts.shellName && (
947+
<Text color={colors.textDim} dimColor>
948+
({parts.shellName}){" "}
949+
</Text>
950+
)}
931951
{cmd && (
932-
<Text color={colors.info} dimColor>
952+
<Text color={colors.info}>
933953
{cmd}
934954
</Text>
935955
)}
936956
<Text>{truncatedMessage}</Text>
957+
{exitCode && (
958+
<Text color={parts.exitCode === 0 ? colors.success : colors.error}>
959+
{" "}{exitCode}
960+
</Text>
961+
)}
937962
</Box>
938963
);
939964
}

0 commit comments

Comments
 (0)