@runloop/rl-cli 0.5.0 → 0.10.0
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/README.md +73 -106
- package/dist/cli.js +4 -416
- package/dist/commands/devbox/list.js +36 -10
- package/dist/components/DevboxCreatePage.js +3 -3
- package/dist/components/DevboxDetailPage.js +62 -11
- package/dist/components/MetadataDisplay.js +12 -2
- package/dist/components/ResourceListView.js +24 -11
- package/dist/components/StateHistory.js +120 -0
- package/dist/components/StatusBadge.js +51 -20
- package/dist/screens/DevboxCreateScreen.js +4 -4
- package/dist/utils/commands.js +408 -0
- package/dist/utils/config.js +21 -0
- package/dist/utils/theme.js +1 -1
- package/package.json +21 -8
- package/dist/commands/auth.js +0 -29
- package/dist/commands/blueprint/preview.js +0 -45
- package/dist/commands/config.js +0 -118
- package/dist/commands/create.js +0 -42
- package/dist/commands/delete.js +0 -34
- package/dist/commands/exec.js +0 -35
- package/dist/commands/list.js +0 -59
- package/dist/commands/upload.js +0 -40
- package/dist/components/Table.example.js +0 -85
- package/dist/screens/LogsSessionScreen.js +0 -49
- package/dist/utils/CommandExecutor.js +0 -131
- package/dist/utils/memoryMonitor.js +0 -85
- package/dist/utils/process.js +0 -106
- package/dist/utils/versionCheck.js +0 -53
|
@@ -4,9 +4,9 @@ import { Box, Text, useInput } from "ink";
|
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
import { Header } from "./Header.js";
|
|
6
6
|
import { StatusBadge } from "./StatusBadge.js";
|
|
7
|
-
import { MetadataDisplay } from "./MetadataDisplay.js";
|
|
8
7
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
9
8
|
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
9
|
+
import { StateHistory } from "./StateHistory.js";
|
|
10
10
|
import { getDevboxUrl } from "../utils/url.js";
|
|
11
11
|
import { colors } from "../utils/theme.js";
|
|
12
12
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
@@ -32,6 +32,12 @@ const formatTimeAgo = (timestamp) => {
|
|
|
32
32
|
const years = Math.floor(months / 12);
|
|
33
33
|
return `${years}y ago`;
|
|
34
34
|
};
|
|
35
|
+
// Truncate long strings to prevent layout issues
|
|
36
|
+
const truncateString = (str, maxLength) => {
|
|
37
|
+
if (str.length <= maxLength)
|
|
38
|
+
return str;
|
|
39
|
+
return str.substring(0, maxLength - 3) + "...";
|
|
40
|
+
};
|
|
35
41
|
export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
36
42
|
const isMounted = React.useRef(true);
|
|
37
43
|
// Track mounted state
|
|
@@ -359,7 +365,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
359
365
|
lines.push(_jsxs(Text, { color: colors.error, dimColor: true, children: [" ", "Failure Reason: ", selectedDevbox.failure_reason] }, "status-fail"));
|
|
360
366
|
}
|
|
361
367
|
if (selectedDevbox.shutdown_reason) {
|
|
362
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown
|
|
368
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Initiator: ", selectedDevbox.shutdown_reason] }, "status-shut"));
|
|
363
369
|
}
|
|
364
370
|
lines.push(_jsx(Text, { children: " " }, "status-space"));
|
|
365
371
|
}
|
|
@@ -424,16 +430,61 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
424
430
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
425
431
|
{ label: "Devboxes" },
|
|
426
432
|
{ label: selectedDevbox.name || selectedDevbox.id, active: true },
|
|
427
|
-
] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: selectedDevbox.name || selectedDevbox.id
|
|
433
|
+
] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [_jsx(Text, { color: colors.primary, bold: true, children: truncateString(selectedDevbox.name || selectedDevbox.id, Math.max(20, detailViewport.terminalWidth - 35)) }), selectedDevbox.name && (_jsxs(Text, { color: colors.idColor, children: [" \u2022 ", selectedDevbox.id] }))] }), _jsxs(Box, { children: [_jsx(StatusBadge, { status: selectedDevbox.status, fullText: true }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Text, { color: colors.success, dimColor: true, children: [" ", "\u2022 Uptime:", " ", uptime < 60
|
|
428
434
|
? `${uptime}m`
|
|
429
|
-
: `${Math.floor(uptime / 60)}h ${uptime % 60}m`] }),
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
: `${Math.floor(uptime / 60)}h ${uptime % 60}m`] })), selectedDevbox.status !== "running" &&
|
|
436
|
+
selectedDevbox.create_time_ms &&
|
|
437
|
+
selectedDevbox.end_time_ms && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Ran for:", " ", (() => {
|
|
438
|
+
const runtime = Math.floor((selectedDevbox.end_time_ms -
|
|
439
|
+
selectedDevbox.create_time_ms) /
|
|
440
|
+
1000);
|
|
441
|
+
if (runtime < 60)
|
|
442
|
+
return `${runtime}s`;
|
|
443
|
+
const mins = Math.floor(runtime / 60);
|
|
444
|
+
if (mins < 60)
|
|
445
|
+
return `${mins}m ${runtime % 60}s`;
|
|
446
|
+
const hours = Math.floor(mins / 60);
|
|
447
|
+
return `${hours}h ${mins % 60}m`;
|
|
448
|
+
})()] }))] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.warning, bold: true, children: [figures.squareSmallFilled, " Details"] }), _jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [selectedDevbox.create_time_ms && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Created " }), _jsx(Text, { dimColor: true, children: formattedCreateTime }), selectedDevbox.end_time_ms ? (_jsxs(Text, { dimColor: true, children: [" ", figures.arrowRight, " ", new Date(selectedDevbox.end_time_ms).toLocaleString()] })) : (_jsxs(Text, { dimColor: true, children: [" (", createTimeAgo, ")"] }))] })), (lp?.resource_size_request ||
|
|
449
|
+
lp?.custom_cpu_cores ||
|
|
450
|
+
lp?.custom_gb_memory ||
|
|
451
|
+
lp?.custom_disk_size ||
|
|
452
|
+
lp?.architecture) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Resources " }), _jsx(Text, { dimColor: true, children: [
|
|
453
|
+
lp?.resource_size_request,
|
|
454
|
+
lp?.architecture,
|
|
455
|
+
lp?.custom_cpu_cores && `${lp.custom_cpu_cores}VCPU`,
|
|
456
|
+
lp?.custom_gb_memory && `${lp.custom_gb_memory}GB RAM`,
|
|
457
|
+
lp?.custom_disk_size && `${lp.custom_disk_size}GB DISC`,
|
|
458
|
+
]
|
|
459
|
+
.filter(Boolean)
|
|
460
|
+
.join(" • ") })] })), (lp?.keep_alive_time_seconds || lp?.user_parameters) && (_jsxs(Box, { children: [lp?.keep_alive_time_seconds && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.textDim, children: "Lifetime " }), _jsx(Text, { dimColor: true, children: lp.keep_alive_time_seconds < 3600
|
|
461
|
+
? `${Math.floor(lp.keep_alive_time_seconds / 60)}m`
|
|
462
|
+
: `${Math.floor(lp.keep_alive_time_seconds / 3600)}h ${Math.floor((lp.keep_alive_time_seconds % 3600) / 60)}m` }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Text, { children: [" ", "\u2022", " ", (() => {
|
|
463
|
+
const maxLifetimeMinutes = Math.floor(lp.keep_alive_time_seconds / 60);
|
|
464
|
+
const remainingMinutes = maxLifetimeMinutes - uptime;
|
|
465
|
+
if (remainingMinutes <= 0) {
|
|
466
|
+
return _jsx(Text, { color: colors.error, children: "Expired" });
|
|
467
|
+
}
|
|
468
|
+
else if (remainingMinutes < 5) {
|
|
469
|
+
return (_jsxs(Text, { color: colors.error, children: [remainingMinutes, "m remaining"] }));
|
|
470
|
+
}
|
|
471
|
+
else if (remainingMinutes < 15) {
|
|
472
|
+
return (_jsxs(Text, { color: colors.warning, children: [remainingMinutes, "m remaining"] }));
|
|
473
|
+
}
|
|
474
|
+
else if (remainingMinutes < 60) {
|
|
475
|
+
return (_jsxs(Text, { color: colors.success, children: [remainingMinutes, "m remaining"] }));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
const hours = Math.floor(remainingMinutes / 60);
|
|
479
|
+
const mins = remainingMinutes % 60;
|
|
480
|
+
return (_jsxs(Text, { color: colors.success, children: [hours, "h ", mins, "m remaining"] }));
|
|
481
|
+
}
|
|
482
|
+
})()] })), lp?.user_parameters && (_jsx(Text, { color: colors.textDim, children: " \u2022 " }))] })), lp?.user_parameters && (_jsxs(_Fragment, { children: [!lp?.keep_alive_time_seconds && (_jsx(Text, { color: colors.textDim, children: "User " })), _jsx(Text, { color: colors.textDim, children: "User: " }), _jsxs(Text, { dimColor: true, children: [lp.user_parameters.username || "default", lp.user_parameters.uid != null &&
|
|
483
|
+
lp.user_parameters.uid !== 0 &&
|
|
484
|
+
` (UID: ${lp.user_parameters.uid})`] })] }))] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Source " }), _jsx(Text, { color: colors.success, children: selectedDevbox.blueprint_id || selectedDevbox.snapshot_id })] })), selectedDevbox.initiator_id && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Initiator " }), _jsx(Text, { color: colors.secondary, children: selectedDevbox.initiator_id })] })), hasCapabilities && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Capabilities " }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities
|
|
485
|
+
.filter((c) => c !== "unknown")
|
|
486
|
+
.join(", ") })] }))] })] }), selectedDevbox.metadata &&
|
|
487
|
+
Object.keys(selectedDevbox.metadata).length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.identical, " Metadata"] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: Object.entries(selectedDevbox.metadata).map(([key, value]) => (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: key }), _jsx(Text, { color: colors.textDim, children: ": " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: value })] }, key))) })] })), selectedDevbox.failure_reason && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Error"] }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: colors.error, children: selectedDevbox.failure_reason }) })] })), _jsx(StateHistory, { stateTransitions: selectedDevbox.state_transitions, shutdownReason: selectedDevbox.shutdown_reason ?? undefined }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: operations.map((op, index) => {
|
|
437
488
|
const isSelected = index === selectedOperation;
|
|
438
489
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
|
|
439
490
|
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Execute \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
2
3
|
import { Box, Text } from "ink";
|
|
3
4
|
import figures from "figures";
|
|
4
5
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
const renderKeyValueBadge = (keyText, value, color) => (_jsxs(Box, { borderStyle: "round", borderColor: color, paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: color, children: "
|
|
6
|
+
const renderKeyValueBadge = (keyText, value, color) => (_jsxs(Box, { borderStyle: "round", borderColor: color, paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: color, children: "= " }), _jsx(Text, { color: color, children: value })] }));
|
|
7
|
+
const renderCompactKeyValue = (keyText, value, color, isLast) => (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: colors.textDim, children: "=" }), _jsx(Text, { color: color, children: value }), !isLast && _jsx(Text, { color: colors.textDim, children: " \u00B7 " })] }));
|
|
6
8
|
// Generate color for each key based on hash
|
|
7
9
|
const getColorForKey = (key, index) => {
|
|
8
10
|
const colorList = [
|
|
@@ -15,11 +17,19 @@ const getColorForKey = (key, index) => {
|
|
|
15
17
|
];
|
|
16
18
|
return colorList[index % colorList.length];
|
|
17
19
|
};
|
|
18
|
-
export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = false, selectedKey, }) => {
|
|
20
|
+
export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = false, selectedKey, compact = false, }) => {
|
|
19
21
|
const entries = Object.entries(metadata);
|
|
20
22
|
if (entries.length === 0) {
|
|
21
23
|
return null;
|
|
22
24
|
}
|
|
25
|
+
if (compact) {
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.identical, " ", title] })), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: entries.map(([key, value], index) => {
|
|
27
|
+
const color = getColorForKey(key, index);
|
|
28
|
+
const isSelected = selectedKey === key;
|
|
29
|
+
const isLast = index === entries.length - 1;
|
|
30
|
+
return (_jsxs(React.Fragment, { children: [isSelected && (_jsx(Text, { color: colors.primary, bold: true, children: figures.pointer })), renderCompactKeyValue(key, value, isSelected ? colors.primary : color, isLast)] }, key));
|
|
31
|
+
}) })] }));
|
|
32
|
+
}
|
|
23
33
|
const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 1, children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.identical, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
|
|
24
34
|
const color = getColorForKey(key, index);
|
|
25
35
|
const isSelected = selectedKey === key;
|
|
@@ -10,25 +10,38 @@ import { Table } from "./Table.js";
|
|
|
10
10
|
import { colors } from "../utils/theme.js";
|
|
11
11
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
12
12
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
13
|
-
// Format time ago
|
|
13
|
+
// Format time ago - concise with ISO-style date for older items
|
|
14
14
|
export const formatTimeAgo = (timestamp) => {
|
|
15
15
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
16
|
+
const date = new Date(timestamp);
|
|
17
|
+
const now = new Date();
|
|
18
|
+
// Format time as HH:MM:SS (24h)
|
|
19
|
+
const time = date.toTimeString().slice(0, 8);
|
|
20
|
+
// Less than 1 minute
|
|
16
21
|
if (seconds < 60)
|
|
17
|
-
return `${seconds}s ago`;
|
|
22
|
+
return `${time} (${seconds}s ago)`;
|
|
18
23
|
const minutes = Math.floor(seconds / 60);
|
|
24
|
+
// Less than 1 hour
|
|
19
25
|
if (minutes < 60)
|
|
20
|
-
return `${minutes}m ago`;
|
|
26
|
+
return `${time} (${minutes}m ago)`;
|
|
21
27
|
const hours = Math.floor(minutes / 60);
|
|
28
|
+
// Less than 24 hours - show time + relative
|
|
22
29
|
if (hours < 24)
|
|
23
|
-
return `${hours}
|
|
30
|
+
return `${time} (${hours}hr ago)`;
|
|
24
31
|
const days = Math.floor(hours / 24);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const sameYear = date.getFullYear() === now.getFullYear();
|
|
33
|
+
// Format date parts
|
|
34
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
35
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
36
|
+
const year = date.getFullYear();
|
|
37
|
+
// Date format: MM-DD or YYYY-MM-DD if different year
|
|
38
|
+
const dateStr = `${month}-${day}`;
|
|
39
|
+
// 1-7 days - show date + time + relative
|
|
40
|
+
if (days <= 7) {
|
|
41
|
+
return `${dateStr} ${time} (${days}d)`;
|
|
42
|
+
}
|
|
43
|
+
// More than 7 days - just date + time, no relative
|
|
44
|
+
return `${dateStr} ${time}`;
|
|
32
45
|
};
|
|
33
46
|
export function ResourceListView({ config }) {
|
|
34
47
|
const { exit: inkExit } = useApp();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
5
|
+
import { getStatusDisplay } from "./StatusBadge.js";
|
|
6
|
+
// Format shutdown reason into human-readable text
|
|
7
|
+
const formatShutdownReason = (reason) => {
|
|
8
|
+
switch (reason) {
|
|
9
|
+
case "api_shutdown":
|
|
10
|
+
return "API call";
|
|
11
|
+
case "idle_timeout":
|
|
12
|
+
return "Idle timeout";
|
|
13
|
+
case "keep_alive_timeout":
|
|
14
|
+
return "Max lifetime expired";
|
|
15
|
+
case "max_lifetime":
|
|
16
|
+
case "max_lifetime_exceeded":
|
|
17
|
+
return "Max lifetime exceeded";
|
|
18
|
+
case "user_initiated":
|
|
19
|
+
return "User initiated";
|
|
20
|
+
case "system_maintenance":
|
|
21
|
+
return "System maintenance";
|
|
22
|
+
case "resource_limit":
|
|
23
|
+
return "Resource limits";
|
|
24
|
+
case "entrypoint_exit":
|
|
25
|
+
return "Entrypoint exited";
|
|
26
|
+
case "idle":
|
|
27
|
+
return "Idle";
|
|
28
|
+
case "error":
|
|
29
|
+
case "failure":
|
|
30
|
+
return "Error";
|
|
31
|
+
default:
|
|
32
|
+
// Convert snake_case to readable text
|
|
33
|
+
return reason.replace(/_/g, " ");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
// Format time ago in a succinct way
|
|
37
|
+
const formatTimeAgo = (timestamp) => {
|
|
38
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
39
|
+
if (seconds < 60)
|
|
40
|
+
return `${seconds}s ago`;
|
|
41
|
+
const minutes = Math.floor(seconds / 60);
|
|
42
|
+
if (minutes < 60)
|
|
43
|
+
return `${minutes}m ago`;
|
|
44
|
+
const hours = Math.floor(minutes / 60);
|
|
45
|
+
if (hours < 24)
|
|
46
|
+
return `${hours}h ago`;
|
|
47
|
+
const days = Math.floor(hours / 24);
|
|
48
|
+
if (days < 30)
|
|
49
|
+
return `${days}d ago`;
|
|
50
|
+
const months = Math.floor(days / 30);
|
|
51
|
+
if (months < 12)
|
|
52
|
+
return `${months}mo ago`;
|
|
53
|
+
const years = Math.floor(months / 12);
|
|
54
|
+
return `${years}y ago`;
|
|
55
|
+
};
|
|
56
|
+
// Format duration in a succinct way
|
|
57
|
+
const formatDuration = (milliseconds) => {
|
|
58
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
59
|
+
if (seconds < 60)
|
|
60
|
+
return `${seconds}s`;
|
|
61
|
+
const minutes = Math.floor(seconds / 60);
|
|
62
|
+
if (minutes < 60)
|
|
63
|
+
return `${minutes}m`;
|
|
64
|
+
const hours = Math.floor(minutes / 60);
|
|
65
|
+
if (hours < 24)
|
|
66
|
+
return `${hours}h ${minutes % 60}m`;
|
|
67
|
+
const days = Math.floor(hours / 24);
|
|
68
|
+
return `${days}d ${hours % 24}h`;
|
|
69
|
+
};
|
|
70
|
+
// Capitalize first letter of a string
|
|
71
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
72
|
+
// Terminal states that don't need duration shown (no new state coming)
|
|
73
|
+
const TERMINAL_STATES = ["shutdown", "failure"];
|
|
74
|
+
export const StateHistory = ({ stateTransitions, shutdownReason, }) => {
|
|
75
|
+
if (!stateTransitions || stateTransitions.length === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// Check if there are more than 5 transitions
|
|
79
|
+
const totalTransitions = stateTransitions.length;
|
|
80
|
+
const hasMore = totalTransitions > 5;
|
|
81
|
+
// Get last 5 transitions (oldest first - chronological order)
|
|
82
|
+
const lastFive = stateTransitions
|
|
83
|
+
.slice(-5)
|
|
84
|
+
.map((transition, idx, arr) => {
|
|
85
|
+
const transitionTime = transition.transition_time_ms;
|
|
86
|
+
// Calculate duration: time until next transition, or until now if it's the last state
|
|
87
|
+
let duration = 0;
|
|
88
|
+
if (transitionTime) {
|
|
89
|
+
if (idx === arr.length - 1) {
|
|
90
|
+
// Most recent state - duration is from transition time to now
|
|
91
|
+
duration = Date.now() - transitionTime;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Earlier state - duration is from this transition to the next one
|
|
95
|
+
const nextTransition = arr[idx + 1];
|
|
96
|
+
const nextTransitionTime = nextTransition.transition_time_ms;
|
|
97
|
+
if (nextTransitionTime) {
|
|
98
|
+
duration = nextTransitionTime - transitionTime;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
status: transition.status,
|
|
104
|
+
transitionTime,
|
|
105
|
+
duration,
|
|
106
|
+
};
|
|
107
|
+
})
|
|
108
|
+
.filter((state) => state.transitionTime); // Only show states with valid timestamps
|
|
109
|
+
if (lastFive.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.success, bold: true, children: [figures.info, " State History", hasMore && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalTransitions - 5, " earlier)"] }))] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: lastFive.map((state, idx) => {
|
|
113
|
+
const statusDisplay = getStatusDisplay(state.status || "");
|
|
114
|
+
const isLastState = idx === lastFive.length - 1;
|
|
115
|
+
const isTerminalState = TERMINAL_STATES.includes(state.status);
|
|
116
|
+
const showDuration = state.duration > 0 && !(isLastState && isTerminalState);
|
|
117
|
+
const isShutdownState = state.status === "shutdown";
|
|
118
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: statusDisplay.color, children: [statusDisplay.icon, " "] }), _jsx(Text, { color: isLastState ? statusDisplay.color : colors.textDim, bold: isLastState, children: capitalize(state.status || "unknown") }), state.transitionTime && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: [" ", "at ", new Date(state.transitionTime).toLocaleString(), " ", _jsxs(Text, { color: colors.textDim, children: ["(", formatTimeAgo(state.transitionTime), ")"] }), showDuration && (_jsxs(_Fragment, { children: [" ", "\u2022 Duration:", " ", _jsx(Text, { color: colors.secondary, children: formatDuration(state.duration) })] }))] }), isShutdownState && shutdownReason && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.textDim, children: " due to " }), _jsx(Text, { color: colors.warning, children: formatShutdownReason(shutdownReason) })] }))] }))] }, idx));
|
|
119
|
+
}) })] }));
|
|
120
|
+
};
|
|
@@ -8,87 +8,118 @@ export const getStatusDisplay = (status) => {
|
|
|
8
8
|
icon: figures.questionMarkPrefix,
|
|
9
9
|
color: colors.textDim,
|
|
10
10
|
text: "UNKNOWN ",
|
|
11
|
+
label: "Unknown",
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
switch (status) {
|
|
15
|
+
// === ACTIVE STATE ===
|
|
14
16
|
case "running":
|
|
15
17
|
return {
|
|
16
18
|
icon: figures.circleFilled,
|
|
17
19
|
color: colors.success,
|
|
18
20
|
text: "RUNNING ",
|
|
21
|
+
label: "Running",
|
|
19
22
|
};
|
|
23
|
+
// === STARTING UP (transitioning to active) ===
|
|
20
24
|
case "provisioning":
|
|
21
25
|
return {
|
|
22
|
-
icon: figures.
|
|
26
|
+
icon: figures.arrowUp,
|
|
23
27
|
color: colors.warning,
|
|
24
28
|
text: "PROVISION ",
|
|
29
|
+
label: "Provisioning",
|
|
25
30
|
};
|
|
26
31
|
case "initializing":
|
|
27
32
|
return {
|
|
28
|
-
icon: figures.
|
|
33
|
+
icon: figures.arrowUp,
|
|
29
34
|
color: colors.primary,
|
|
30
35
|
text: "INITIALIZE",
|
|
36
|
+
label: "Initializing",
|
|
31
37
|
};
|
|
38
|
+
case "resuming":
|
|
39
|
+
return {
|
|
40
|
+
icon: figures.arrowUp,
|
|
41
|
+
color: colors.primary,
|
|
42
|
+
text: "RESUMING ",
|
|
43
|
+
label: "Resuming",
|
|
44
|
+
};
|
|
45
|
+
// === SHUTTING DOWN (transitioning to inactive) ===
|
|
46
|
+
case "suspending":
|
|
47
|
+
return {
|
|
48
|
+
icon: figures.arrowDown,
|
|
49
|
+
color: colors.warning,
|
|
50
|
+
text: "SUSPENDING",
|
|
51
|
+
label: "Suspending",
|
|
52
|
+
};
|
|
53
|
+
// === INACTIVE STATES ===
|
|
32
54
|
case "suspended":
|
|
33
55
|
return {
|
|
34
56
|
icon: figures.circleDotted,
|
|
35
57
|
color: colors.warning,
|
|
36
58
|
text: "SUSPENDED ",
|
|
59
|
+
label: "Suspended",
|
|
37
60
|
};
|
|
38
|
-
case "failure":
|
|
39
|
-
return { icon: figures.cross, color: colors.error, text: "FAILED " };
|
|
40
61
|
case "shutdown":
|
|
41
62
|
return {
|
|
42
63
|
icon: figures.circle,
|
|
43
64
|
color: colors.textDim,
|
|
44
65
|
text: "SHUTDOWN ",
|
|
66
|
+
label: "Shutdown",
|
|
45
67
|
};
|
|
46
|
-
|
|
68
|
+
// === ERROR STATES ===
|
|
69
|
+
case "failure":
|
|
47
70
|
return {
|
|
48
|
-
icon: figures.
|
|
49
|
-
color: colors.
|
|
50
|
-
text: "
|
|
71
|
+
icon: figures.cross,
|
|
72
|
+
color: colors.error,
|
|
73
|
+
text: "FAILED ",
|
|
74
|
+
label: "Failed",
|
|
51
75
|
};
|
|
52
|
-
case "
|
|
76
|
+
case "build_failed":
|
|
77
|
+
case "failed":
|
|
53
78
|
return {
|
|
54
|
-
icon: figures.
|
|
55
|
-
color: colors.
|
|
56
|
-
text: "
|
|
79
|
+
icon: figures.cross,
|
|
80
|
+
color: colors.error,
|
|
81
|
+
text: "FAILED ",
|
|
82
|
+
label: "Failed",
|
|
57
83
|
};
|
|
84
|
+
// === BUILD STATES (for blueprints) ===
|
|
58
85
|
case "ready":
|
|
59
86
|
return {
|
|
60
|
-
icon: figures.
|
|
87
|
+
icon: figures.tick,
|
|
61
88
|
color: colors.success,
|
|
62
89
|
text: "READY ",
|
|
90
|
+
label: "Ready",
|
|
63
91
|
};
|
|
64
92
|
case "build_complete":
|
|
65
93
|
case "building_complete":
|
|
66
94
|
return {
|
|
67
|
-
icon: figures.
|
|
95
|
+
icon: figures.tick,
|
|
68
96
|
color: colors.success,
|
|
69
97
|
text: "COMPLETE ",
|
|
98
|
+
label: "Build Complete",
|
|
70
99
|
};
|
|
71
100
|
case "building":
|
|
72
101
|
return {
|
|
73
|
-
icon: figures.
|
|
102
|
+
icon: figures.arrowUp,
|
|
74
103
|
color: colors.warning,
|
|
75
104
|
text: "BUILDING ",
|
|
105
|
+
label: "Building: In Progress",
|
|
76
106
|
};
|
|
77
|
-
case "build_failed":
|
|
78
|
-
case "failed":
|
|
79
|
-
return { icon: figures.cross, color: colors.error, text: "FAILED " };
|
|
80
107
|
default:
|
|
81
108
|
// Truncate and pad any unknown status to 10 chars to match column width
|
|
82
109
|
const truncated = status.toUpperCase().slice(0, 10);
|
|
83
110
|
const padded = truncated.padEnd(10, " ");
|
|
111
|
+
// Capitalize first letter for label
|
|
112
|
+
const label = status.charAt(0).toUpperCase() + status.slice(1);
|
|
84
113
|
return {
|
|
85
114
|
icon: figures.questionMarkPrefix,
|
|
86
115
|
color: colors.textDim,
|
|
87
116
|
text: padded,
|
|
117
|
+
label: label,
|
|
88
118
|
};
|
|
89
119
|
}
|
|
90
120
|
};
|
|
91
|
-
export const StatusBadge = ({ status, showText = true }) => {
|
|
121
|
+
export const StatusBadge = ({ status, showText = true, fullText = false, }) => {
|
|
92
122
|
const statusDisplay = getStatusDisplay(status);
|
|
93
|
-
|
|
123
|
+
const displayText = fullText ? statusDisplay.label : statusDisplay.text;
|
|
124
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: statusDisplay.color, children: statusDisplay.icon }), showText && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: statusDisplay.color, children: displayText })] }))] }));
|
|
94
125
|
};
|
|
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useNavigation } from "../store/navigationStore.js";
|
|
3
3
|
import { DevboxCreatePage } from "../components/DevboxCreatePage.js";
|
|
4
4
|
export function DevboxCreateScreen() {
|
|
5
|
-
const { goBack } = useNavigation();
|
|
6
|
-
const handleCreate = () => {
|
|
7
|
-
// After creation,
|
|
8
|
-
|
|
5
|
+
const { goBack, navigate } = useNavigation();
|
|
6
|
+
const handleCreate = (devbox) => {
|
|
7
|
+
// After creation, navigate to the devbox detail page
|
|
8
|
+
navigate("devbox-detail", { devboxId: devbox.id });
|
|
9
9
|
};
|
|
10
10
|
return _jsx(DevboxCreatePage, { onBack: goBack, onCreate: handleCreate });
|
|
11
11
|
}
|