@runloop/rl-cli 0.4.0 → 0.9.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/blueprint/list.js +2 -1
- package/dist/commands/devbox/list.js +5 -1
- package/dist/commands/snapshot/list.js +47 -6
- package/dist/components/Breadcrumb.js +8 -8
- package/dist/components/DevboxCreatePage.js +2 -2
- package/dist/components/DevboxDetailPage.js +2 -1
- package/dist/components/MainMenu.js +13 -2
- package/dist/components/StateHistory.js +80 -0
- package/dist/components/UpdateNotification.js +3 -44
- package/dist/hooks/useUpdateCheck.js +54 -0
- package/dist/mcp/server.js +1 -1
- package/dist/screens/DevboxCreateScreen.js +4 -4
- package/dist/utils/client.js +1 -1
- package/dist/utils/commands.js +408 -0
- package/dist/utils/config.js +21 -0
- package/dist/utils/exec.js +22 -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
|
@@ -6,8 +6,10 @@ import { Banner } from "./Banner.js";
|
|
|
6
6
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
7
|
import { VERSION } from "../version.js";
|
|
8
8
|
import { colors } from "../utils/theme.js";
|
|
9
|
+
import { execCommand } from "../utils/exec.js";
|
|
9
10
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
10
11
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
12
|
+
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
|
|
11
13
|
const menuItems = [
|
|
12
14
|
{
|
|
13
15
|
key: "devboxes",
|
|
@@ -36,6 +38,8 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
36
38
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
37
39
|
// Use centralized viewport hook for consistent layout
|
|
38
40
|
const { terminalHeight } = useViewportHeight({ overhead: 0 });
|
|
41
|
+
// Check for updates
|
|
42
|
+
const { updateAvailable } = useUpdateCheck();
|
|
39
43
|
// Handle Ctrl+C to exit
|
|
40
44
|
useExitOnCtrlC();
|
|
41
45
|
useInput((input, key) => {
|
|
@@ -60,6 +64,13 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
60
64
|
else if (input === "s" || input === "3") {
|
|
61
65
|
onSelect("snapshots");
|
|
62
66
|
}
|
|
67
|
+
else if (input === "u" && updateAvailable) {
|
|
68
|
+
// Release terminal and exec into update command (never returns)
|
|
69
|
+
execCommand("sh", [
|
|
70
|
+
"-c",
|
|
71
|
+
"npm install -g @runloop/rl-cli@latest && exec rli",
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
63
74
|
});
|
|
64
75
|
// Use compact layout if terminal height is less than 20 lines (memoized)
|
|
65
76
|
const useCompactLayout = terminalHeight < 20;
|
|
@@ -67,10 +78,10 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
67
78
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingX: 2, marginBottom: 1, children: [_jsx(Text, { color: colors.primary, bold: true, children: "RUNLOOP.ai" }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Cloud development environments \u2022 v", VERSION] })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
|
|
68
79
|
const isSelected = index === selectedIndex;
|
|
69
80
|
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
70
|
-
}) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) })] }));
|
|
81
|
+
}) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit", updateAvailable && " • [u] Update"] }) })] }));
|
|
71
82
|
}
|
|
72
83
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }], showVersionCheck: true }), _jsx(Banner, {}), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
73
84
|
const isSelected = index === selectedIndex;
|
|
74
85
|
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [isSelected && (_jsxs(_Fragment, { children: [_jsx(Text, { color: item.color, bold: true, children: figures.pointer }), _jsx(Text, { children: " " })] })), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
|
|
75
|
-
})] }), _jsx(Box, { paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) }) })] }));
|
|
86
|
+
})] }), _jsx(Box, { paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit", updateAvailable && " • [u] Update"] }) }) })] }));
|
|
76
87
|
};
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
// Format time ago in a succinct way
|
|
6
|
+
const formatTimeAgo = (timestamp) => {
|
|
7
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
8
|
+
if (seconds < 60)
|
|
9
|
+
return `${seconds}s ago`;
|
|
10
|
+
const minutes = Math.floor(seconds / 60);
|
|
11
|
+
if (minutes < 60)
|
|
12
|
+
return `${minutes}m ago`;
|
|
13
|
+
const hours = Math.floor(minutes / 60);
|
|
14
|
+
if (hours < 24)
|
|
15
|
+
return `${hours}h ago`;
|
|
16
|
+
const days = Math.floor(hours / 24);
|
|
17
|
+
if (days < 30)
|
|
18
|
+
return `${days}d ago`;
|
|
19
|
+
const months = Math.floor(days / 30);
|
|
20
|
+
if (months < 12)
|
|
21
|
+
return `${months}mo ago`;
|
|
22
|
+
const years = Math.floor(months / 12);
|
|
23
|
+
return `${years}y ago`;
|
|
24
|
+
};
|
|
25
|
+
// Format duration in a succinct way
|
|
26
|
+
const formatDuration = (milliseconds) => {
|
|
27
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
28
|
+
if (seconds < 60)
|
|
29
|
+
return `${seconds}s`;
|
|
30
|
+
const minutes = Math.floor(seconds / 60);
|
|
31
|
+
if (minutes < 60)
|
|
32
|
+
return `${minutes}m`;
|
|
33
|
+
const hours = Math.floor(minutes / 60);
|
|
34
|
+
if (hours < 24)
|
|
35
|
+
return `${hours}h ${minutes % 60}m`;
|
|
36
|
+
const days = Math.floor(hours / 24);
|
|
37
|
+
return `${days}d ${hours % 24}h`;
|
|
38
|
+
};
|
|
39
|
+
// Capitalize first letter of a string
|
|
40
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
41
|
+
export const StateHistory = ({ stateTransitions }) => {
|
|
42
|
+
if (!stateTransitions ||
|
|
43
|
+
!Array.isArray(stateTransitions) ||
|
|
44
|
+
stateTransitions.length === 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
// Get last 3 transitions (most recent first)
|
|
48
|
+
const lastThree = stateTransitions
|
|
49
|
+
.slice(-3)
|
|
50
|
+
.reverse()
|
|
51
|
+
.map((transition, idx, arr) => {
|
|
52
|
+
const transitionTime = transition.transition_time_ms;
|
|
53
|
+
// Calculate duration: time until next transition, or until now if it's the current state
|
|
54
|
+
let duration = 0;
|
|
55
|
+
if (transitionTime) {
|
|
56
|
+
if (idx === 0) {
|
|
57
|
+
// Most recent state - duration is from transition time to now
|
|
58
|
+
duration = Date.now() - transitionTime;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Previous state - duration is from this transition to the next one
|
|
62
|
+
const nextTransition = arr[idx - 1];
|
|
63
|
+
const nextTransitionTime = nextTransition.transition_time_ms;
|
|
64
|
+
if (nextTransitionTime) {
|
|
65
|
+
duration = nextTransitionTime - transitionTime;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
status: transition.status,
|
|
71
|
+
transitionTime,
|
|
72
|
+
duration,
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.filter((state) => state.transitionTime); // Only show states with valid timestamps
|
|
76
|
+
if (lastThree.length === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.info, bold: true, children: [figures.circleFilled, " State History"] }), _jsx(Box, { flexDirection: "column", children: lastThree.map((state, idx) => (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: [capitalize(state.status), state.transitionTime && (_jsxs(_Fragment, { children: [" ", "at ", new Date(state.transitionTime).toLocaleString(), " ", _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", formatTimeAgo(state.transitionTime), ")"] }), state.duration > 0 && (_jsxs(_Fragment, { children: [" ", "\u2022 Duration:", " ", _jsx(Text, { color: colors.info, children: formatDuration(state.duration) })] }))] }))] }) }, idx))) })] }));
|
|
80
|
+
};
|
|
@@ -1,56 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
2
|
import { Box, Text } from "ink";
|
|
4
3
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
import {
|
|
4
|
+
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
|
|
6
5
|
/**
|
|
7
6
|
* Version check component that checks npm for updates and displays a notification
|
|
8
7
|
* Restored from git history and enhanced with better visual styling
|
|
9
8
|
*/
|
|
10
9
|
export const UpdateNotification = () => {
|
|
11
|
-
const
|
|
12
|
-
const [isChecking, setIsChecking] = React.useState(true);
|
|
13
|
-
React.useEffect(() => {
|
|
14
|
-
const checkForUpdates = async () => {
|
|
15
|
-
try {
|
|
16
|
-
const currentVersion = VERSION;
|
|
17
|
-
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
18
|
-
if (response.ok) {
|
|
19
|
-
const data = (await response.json());
|
|
20
|
-
const latestVersion = data.version;
|
|
21
|
-
if (latestVersion && latestVersion !== currentVersion) {
|
|
22
|
-
// Check if current version is older than latest
|
|
23
|
-
const compareVersions = (version1, version2) => {
|
|
24
|
-
const v1parts = version1.split(".").map(Number);
|
|
25
|
-
const v2parts = version2.split(".").map(Number);
|
|
26
|
-
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
27
|
-
const v1part = v1parts[i] || 0;
|
|
28
|
-
const v2part = v2parts[i] || 0;
|
|
29
|
-
if (v1part > v2part)
|
|
30
|
-
return 1;
|
|
31
|
-
if (v1part < v2part)
|
|
32
|
-
return -1;
|
|
33
|
-
}
|
|
34
|
-
return 0;
|
|
35
|
-
};
|
|
36
|
-
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
37
|
-
if (isUpdateAvailable) {
|
|
38
|
-
setUpdateAvailable(latestVersion);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Silently fail
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
setIsChecking(false);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
checkForUpdates();
|
|
51
|
-
}, []);
|
|
10
|
+
const { isChecking, updateAvailable, currentVersion } = useUpdateCheck();
|
|
52
11
|
if (isChecking || !updateAvailable) {
|
|
53
12
|
return null;
|
|
54
13
|
}
|
|
55
|
-
return (_jsxs(Box, { borderStyle: "
|
|
14
|
+
return (_jsx(Box, { children: _jsxs(Box, { borderStyle: "arrow", borderColor: colors.warning, paddingX: 1, paddingY: 0, children: [_jsx(Text, { color: colors.warning, children: "\u2728" }), _jsx(Text, { color: colors.text, children: " Update available: " }), _jsx(Text, { color: colors.warning, children: currentVersion }), _jsx(Text, { color: colors.primary, children: " \u2192 " }), _jsx(Text, { color: colors.success, children: updateAvailable }), _jsx(Text, { color: colors.textDim, children: " \u2022 Press " }), _jsx(Text, { color: colors.primary, bold: true, children: "[u]" }), _jsx(Text, { color: colors.textDim, children: " to run: " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "npm i -g @runloop/rl-cli@latest" })] }) }));
|
|
56
15
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { VERSION } from "../version.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hook to check for CLI updates from npm registry
|
|
5
|
+
* Returns the latest version if an update is available
|
|
6
|
+
*/
|
|
7
|
+
export function useUpdateCheck() {
|
|
8
|
+
const [updateAvailable, setUpdateAvailable] = React.useState(null);
|
|
9
|
+
const [isChecking, setIsChecking] = React.useState(true);
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
const checkForUpdates = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const currentVersion = VERSION;
|
|
14
|
+
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
15
|
+
if (response.ok) {
|
|
16
|
+
const data = (await response.json());
|
|
17
|
+
const latestVersion = data.version;
|
|
18
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
19
|
+
// Check if current version is older than latest
|
|
20
|
+
const compareVersions = (version1, version2) => {
|
|
21
|
+
const v1parts = version1.split(".").map(Number);
|
|
22
|
+
const v2parts = version2.split(".").map(Number);
|
|
23
|
+
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
24
|
+
const v1part = v1parts[i] || 0;
|
|
25
|
+
const v2part = v2parts[i] || 0;
|
|
26
|
+
if (v1part > v2part)
|
|
27
|
+
return 1;
|
|
28
|
+
if (v1part < v2part)
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
};
|
|
33
|
+
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
34
|
+
if (isUpdateAvailable) {
|
|
35
|
+
setUpdateAvailable(latestVersion);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Silently fail
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
setIsChecking(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
checkForUpdates();
|
|
48
|
+
}, []);
|
|
49
|
+
return {
|
|
50
|
+
isChecking,
|
|
51
|
+
updateAvailable,
|
|
52
|
+
currentVersion: VERSION,
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -54,7 +54,7 @@ function getClient() {
|
|
|
54
54
|
timeout: 10000, // 10 seconds instead of default 30 seconds
|
|
55
55
|
maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
|
|
56
56
|
defaultHeaders: {
|
|
57
|
-
"User-Agent": `Runloop/JS
|
|
57
|
+
"User-Agent": `Runloop/JS - CLI MCP ${VERSION}`,
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -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
|
}
|