@runloop/rl-cli 0.1.2 → 0.3.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 +54 -10
- package/dist/cli.js +79 -72
- package/dist/commands/auth.js +2 -2
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +278 -230
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/config.js +118 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +46 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +37 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +12 -10
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +26 -67
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +64 -39
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +11 -48
- package/dist/components/DevboxActionsMenu.js +117 -207
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +104 -0
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +37 -33
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +15 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +71 -7
- package/dist/router/Router.js +70 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +101 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/client.js +4 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +208 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +153 -61
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +61 -0
- package/dist/utils/ssh.js +6 -3
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +185 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +162 -13
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +19 -17
|
@@ -1,39 +1,43 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
import { Banner } from "./Banner.js";
|
|
6
6
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
|
-
import { VERSION } from "../
|
|
7
|
+
import { VERSION } from "../version.js";
|
|
8
8
|
import { colors } from "../utils/theme.js";
|
|
9
|
-
|
|
9
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
10
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
11
|
+
const menuItems = [
|
|
12
|
+
{
|
|
13
|
+
key: "devboxes",
|
|
14
|
+
label: "Devboxes",
|
|
15
|
+
description: "Manage cloud development environments",
|
|
16
|
+
icon: "◉",
|
|
17
|
+
color: colors.accent1,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: "blueprints",
|
|
21
|
+
label: "Blueprints",
|
|
22
|
+
description: "Create and manage devbox templates",
|
|
23
|
+
icon: "▣",
|
|
24
|
+
color: colors.accent2,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "snapshots",
|
|
28
|
+
label: "Snapshots",
|
|
29
|
+
description: "Save and restore devbox states",
|
|
30
|
+
icon: "◈",
|
|
31
|
+
color: colors.accent3,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
export const MainMenu = ({ onSelect }) => {
|
|
10
35
|
const { exit } = useApp();
|
|
11
36
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
12
|
-
//
|
|
13
|
-
const terminalHeight =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
key: "devboxes",
|
|
17
|
-
label: "Devboxes",
|
|
18
|
-
description: "Manage cloud development environments",
|
|
19
|
-
icon: "◉",
|
|
20
|
-
color: colors.accent1,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
key: "blueprints",
|
|
24
|
-
label: "Blueprints",
|
|
25
|
-
description: "Create and manage devbox templates",
|
|
26
|
-
icon: "▣",
|
|
27
|
-
color: colors.accent2,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
key: "snapshots",
|
|
31
|
-
label: "Snapshots",
|
|
32
|
-
description: "Save and restore devbox states",
|
|
33
|
-
icon: "◈",
|
|
34
|
-
color: colors.accent3,
|
|
35
|
-
},
|
|
36
|
-
], []);
|
|
37
|
+
// Use centralized viewport hook for consistent layout
|
|
38
|
+
const { terminalHeight } = useViewportHeight({ overhead: 0 });
|
|
39
|
+
// Handle Ctrl+C to exit
|
|
40
|
+
useExitOnCtrlC();
|
|
37
41
|
useInput((input, key) => {
|
|
38
42
|
if (key.upArrow && selectedIndex > 0) {
|
|
39
43
|
setSelectedIndex(selectedIndex - 1);
|
|
@@ -58,15 +62,15 @@ export const MainMenu = React.memo(({ onSelect }) => {
|
|
|
58
62
|
}
|
|
59
63
|
});
|
|
60
64
|
// Use compact layout if terminal height is less than 20 lines (memoized)
|
|
61
|
-
const useCompactLayout =
|
|
65
|
+
const useCompactLayout = terminalHeight < 20;
|
|
62
66
|
if (useCompactLayout) {
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
67
|
+
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) => {
|
|
64
68
|
const isSelected = index === selectedIndex;
|
|
65
69
|
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));
|
|
66
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"] }) })] }));
|
|
67
71
|
}
|
|
68
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
72
|
+
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) => {
|
|
69
73
|
const isSelected = index === selectedIndex;
|
|
70
|
-
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle:
|
|
74
|
+
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));
|
|
71
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"] }) }) })] }));
|
|
72
|
-
}
|
|
76
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { Badge } from "@inkjs/ui";
|
|
4
3
|
import figures from "figures";
|
|
5
4
|
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: ": " }), _jsx(Text, { color: color, children: value })] }));
|
|
6
6
|
// Generate color for each key based on hash
|
|
7
7
|
const getColorForKey = (key, index) => {
|
|
8
8
|
const colorList = [
|
|
@@ -20,10 +20,10 @@ export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = fal
|
|
|
20
20
|
if (entries.length === 0) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
-
const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.
|
|
23
|
+
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
24
|
const color = getColorForKey(key, index);
|
|
25
25
|
const isSelected = selectedKey === key;
|
|
26
|
-
return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " "] })),
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " "] })), renderKeyValueBadge(key, value, isSelected ? colors.primary : color)] }, key));
|
|
27
27
|
})] }));
|
|
28
28
|
if (showBorder) {
|
|
29
29
|
return (_jsx(Box, { borderStyle: "round", borderColor: colors.accent3, paddingX: 2, paddingY: 1, flexDirection: "column", children: content }));
|
|
@@ -6,7 +6,7 @@ import { colors } from "../utils/theme.js";
|
|
|
6
6
|
* Reusable operations menu component for detail pages
|
|
7
7
|
* Displays a list of available operations with keyboard navigation
|
|
8
8
|
*/
|
|
9
|
-
export const OperationsMenu = ({ operations, selectedIndex, onNavigate, onSelect, onBack, additionalActions = [], }) => {
|
|
9
|
+
export const OperationsMenu = ({ operations, selectedIndex, onNavigate: _onNavigate, onSelect: _onSelect, onBack: _onBack, additionalActions = [], }) => {
|
|
10
10
|
return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
11
11
|
const isSelected = index === selectedIndex;
|
|
12
12
|
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] })] }, op.key));
|
|
@@ -8,8 +8,8 @@ import { ActionsPopup } from "./ActionsPopup.js";
|
|
|
8
8
|
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
9
9
|
export const ResourceActionsMenu = (props) => {
|
|
10
10
|
if (props.resourceType === "devbox") {
|
|
11
|
-
const { resource, onBack, breadcrumbItems, initialOperation, initialOperationIndex, skipOperationsMenu,
|
|
12
|
-
return (_jsx(DevboxActionsMenu, { devbox: resource, onBack: onBack, breadcrumbItems: breadcrumbItems, initialOperation: initialOperation, initialOperationIndex: initialOperationIndex, skipOperationsMenu: skipOperationsMenu
|
|
11
|
+
const { resource, onBack, breadcrumbItems, initialOperation, initialOperationIndex, skipOperationsMenu, } = props;
|
|
12
|
+
return (_jsx(DevboxActionsMenu, { devbox: resource, onBack: onBack, breadcrumbItems: breadcrumbItems, initialOperation: initialOperation, initialOperationIndex: initialOperationIndex, skipOperationsMenu: skipOperationsMenu }));
|
|
13
13
|
}
|
|
14
14
|
// Blueprint generic actions menu
|
|
15
15
|
const { resource, onBack, breadcrumbItems = [
|
|
@@ -101,10 +101,10 @@ export const ResourceActionsMenu = (props) => {
|
|
|
101
101
|
// Screens
|
|
102
102
|
if (operationResult || operationError) {
|
|
103
103
|
const label = operations.find((o) => o.key === executingOperation)?.label;
|
|
104
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems
|
|
104
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: operationError ? colors.error : colors.success, children: operationError ? `${label} failed` : `${label} completed` }), !!operationResult && (_jsx(Text, { color: colors.textDim, dimColor: true, children: operationResult })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointerSmall, " Press [Enter] to go back"] })] })] }));
|
|
105
105
|
}
|
|
106
106
|
if (executingOperation && selectedOp?.needsInput) {
|
|
107
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems
|
|
107
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, children: [selectedOp.inputPrompt || "Input:", " "] }), _jsxs(Text, { children: [" ", operationInput] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" })] })] }));
|
|
108
108
|
}
|
|
109
109
|
// Operations menu
|
|
110
110
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: resource, operations: operations.map((op) => ({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Box, Text, useInput,
|
|
3
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import figures from "figures";
|
|
6
6
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
@@ -8,6 +8,8 @@ import { SpinnerComponent } from "./Spinner.js";
|
|
|
8
8
|
import { ErrorMessage } from "./ErrorMessage.js";
|
|
9
9
|
import { Table } from "./Table.js";
|
|
10
10
|
import { colors } from "../utils/theme.js";
|
|
11
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
12
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
11
13
|
// Format time ago in a succinct way
|
|
12
14
|
export const formatTimeAgo = (timestamp) => {
|
|
13
15
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
@@ -29,8 +31,15 @@ export const formatTimeAgo = (timestamp) => {
|
|
|
29
31
|
return `${years}y ago`;
|
|
30
32
|
};
|
|
31
33
|
export function ResourceListView({ config }) {
|
|
32
|
-
const { stdout } = useStdout();
|
|
33
34
|
const { exit: inkExit } = useApp();
|
|
35
|
+
const isMounted = React.useRef(true);
|
|
36
|
+
// Track mounted state
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
isMounted.current = true;
|
|
39
|
+
return () => {
|
|
40
|
+
isMounted.current = false;
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
34
43
|
const [loading, setLoading] = React.useState(true);
|
|
35
44
|
const [resources, setResources] = React.useState([]);
|
|
36
45
|
const [error, setError] = React.useState(null);
|
|
@@ -38,29 +47,39 @@ export function ResourceListView({ config }) {
|
|
|
38
47
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
39
48
|
const [searchMode, setSearchMode] = React.useState(false);
|
|
40
49
|
const [searchQuery, setSearchQuery] = React.useState("");
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
// Calculate overhead for viewport height:
|
|
51
|
+
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
52
|
+
// - Search bar (if visible, 1 line + marginBottom): 2 lines
|
|
53
|
+
// - Table (title + top border + header + bottom border): 4 lines
|
|
54
|
+
// - Stats bar (marginTop + content): 2 lines
|
|
55
|
+
// - Help bar (marginTop + content): 2 lines
|
|
56
|
+
// - Safety buffer for edge cases: 1 line
|
|
57
|
+
// Total: 13 lines base + 2 if searching
|
|
58
|
+
const overhead = 13 + (searchMode || searchQuery ? 2 : 0);
|
|
59
|
+
const { viewportHeight } = useViewportHeight({
|
|
60
|
+
overhead,
|
|
61
|
+
minHeight: 5,
|
|
62
|
+
});
|
|
63
|
+
// Use viewport height for dynamic page size, or fall back to config
|
|
64
|
+
const pageSize = config.pageSize || viewportHeight;
|
|
48
65
|
// Fetch resources
|
|
49
|
-
const fetchData = React.useCallback(async (
|
|
66
|
+
const fetchData = React.useCallback(async (_isInitialLoad = false) => {
|
|
67
|
+
if (!isMounted.current)
|
|
68
|
+
return;
|
|
50
69
|
try {
|
|
51
|
-
if (isInitialLoad) {
|
|
52
|
-
setRefreshing(true);
|
|
53
|
-
}
|
|
54
70
|
const data = await config.fetchResources();
|
|
55
|
-
|
|
71
|
+
if (isMounted.current) {
|
|
72
|
+
setResources(data);
|
|
73
|
+
}
|
|
56
74
|
}
|
|
57
75
|
catch (err) {
|
|
58
|
-
|
|
76
|
+
if (isMounted.current) {
|
|
77
|
+
setError(err);
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
finally {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
setTimeout(() => setRefreshing(false), 300);
|
|
81
|
+
if (isMounted.current) {
|
|
82
|
+
setLoading(false);
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
85
|
}, [config.fetchResources]);
|
|
@@ -77,13 +96,7 @@ export function ResourceListView({ config }) {
|
|
|
77
96
|
return () => clearInterval(interval);
|
|
78
97
|
}
|
|
79
98
|
}, [config.autoRefresh, fetchData]);
|
|
80
|
-
//
|
|
81
|
-
React.useEffect(() => {
|
|
82
|
-
const interval = setInterval(() => {
|
|
83
|
-
setRefreshIcon((prev) => (prev + 1) % 10);
|
|
84
|
-
}, 80);
|
|
85
|
-
return () => clearInterval(interval);
|
|
86
|
-
}, []);
|
|
99
|
+
// Removed refresh icon animation to prevent constant re-renders and flashing
|
|
87
100
|
// Filter resources based on search query
|
|
88
101
|
const filteredResources = React.useMemo(() => {
|
|
89
102
|
if (!config.searchConfig?.enabled || !searchQuery.trim()) {
|
|
@@ -108,13 +121,13 @@ export function ResourceListView({ config }) {
|
|
|
108
121
|
}
|
|
109
122
|
}, [currentResources.length, selectedIndex]);
|
|
110
123
|
const selectedResource = currentResources[selectedIndex];
|
|
124
|
+
// Handle Ctrl+C to exit
|
|
125
|
+
useExitOnCtrlC();
|
|
111
126
|
// Input handling
|
|
112
127
|
useInput((input, key) => {
|
|
113
|
-
//
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
process.exit(130);
|
|
117
|
-
}
|
|
128
|
+
// Don't process input if unmounting
|
|
129
|
+
if (!isMounted.current)
|
|
130
|
+
return;
|
|
118
131
|
const pageResourcesCount = currentResources.length;
|
|
119
132
|
// Skip input handling when in search mode
|
|
120
133
|
if (searchMode) {
|
|
@@ -141,7 +154,6 @@ export function ResourceListView({ config }) {
|
|
|
141
154
|
setSelectedIndex(0);
|
|
142
155
|
}
|
|
143
156
|
else if (key.return && selectedResource && config.onSelect) {
|
|
144
|
-
console.clear();
|
|
145
157
|
config.onSelect(selectedResource);
|
|
146
158
|
}
|
|
147
159
|
else if (input === "/" && config.searchConfig?.enabled) {
|
|
@@ -173,8 +185,8 @@ export function ResourceListView({ config }) {
|
|
|
173
185
|
}
|
|
174
186
|
}
|
|
175
187
|
});
|
|
176
|
-
// Calculate stats
|
|
177
|
-
const
|
|
188
|
+
// Calculate stats (computed for potential future use)
|
|
189
|
+
const _stats = React.useMemo(() => {
|
|
178
190
|
if (!config.statusConfig || !config.getStatus) {
|
|
179
191
|
return null;
|
|
180
192
|
}
|
|
@@ -208,6 +220,6 @@ export function ResourceListView({ config }) {
|
|
|
208
220
|
setSearchMode(false);
|
|
209
221
|
setCurrentPage(0);
|
|
210
222
|
setSelectedIndex(0);
|
|
211
|
-
} }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Esc to cancel]"] })] })), !searchMode && searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: searchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", currentResources.length, " results) [/ to edit, Esc to clear]"] })] }))] })), _jsx(Table, { data: currentResources, keyExtractor: config.keyExtractor, selectedIndex: selectedIndex, title: `${config.resourceNamePlural.toLowerCase()}[${searchQuery ? currentResources.length : resources.length}]`, columns: config.columns }, `table-${searchQuery}-${currentPage}`), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", resources.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] })] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", filteredResources.length] }), _jsx(Text, { children: " " }),
|
|
223
|
+
} }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Esc to cancel]"] })] })), !searchMode && searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: searchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", currentResources.length, " results) [/ to edit, Esc to clear]"] })] }))] })), _jsx(Table, { data: currentResources, keyExtractor: config.keyExtractor, selectedIndex: selectedIndex, title: `${config.resourceNamePlural.toLowerCase()}[${searchQuery ? currentResources.length : resources.length}]`, columns: config.columns }, `table-${searchQuery}-${currentPage}`), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", resources.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] })] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", filteredResources.length] }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.success, children: figures.circleFilled })] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), totalPages > 1 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), config.onSelect && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Enter] Details"] })), config.searchConfig?.enabled && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [/] Search"] })), config.additionalShortcuts &&
|
|
212
224
|
config.additionalShortcuts.map((shortcut) => (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [", shortcut.key, "] ", shortcut.label] }, shortcut.key))), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
213
225
|
}
|
|
@@ -2,6 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import Spinner from "ink-spinner";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
export const SpinnerComponent = ({ message
|
|
6
|
-
|
|
5
|
+
export const SpinnerComponent = ({ message }) => {
|
|
6
|
+
// Limit message length to prevent Yoga layout engine errors
|
|
7
|
+
const MAX_LENGTH = 200;
|
|
8
|
+
const truncatedMessage = message.length > MAX_LENGTH
|
|
9
|
+
? message.substring(0, MAX_LENGTH) + "..."
|
|
10
|
+
: message;
|
|
11
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: colors.primary, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", truncatedMessage] })] }));
|
|
7
12
|
};
|
|
@@ -88,7 +88,7 @@ export const getStatusDisplay = (status) => {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
|
-
export const StatusBadge = ({ status, showText = true
|
|
91
|
+
export const StatusBadge = ({ status, showText = true }) => {
|
|
92
92
|
const statusDisplay = getStatusDisplay(status);
|
|
93
93
|
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: statusDisplay.text })] }))] }));
|
|
94
94
|
};
|
|
@@ -2,6 +2,16 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import figures from "figures";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
export const SuccessMessage = ({ message, details
|
|
6
|
-
|
|
5
|
+
export const SuccessMessage = ({ message, details }) => {
|
|
6
|
+
// Limit message length to prevent Yoga layout engine errors
|
|
7
|
+
const MAX_LENGTH = 500;
|
|
8
|
+
const truncatedMessage = message.length > MAX_LENGTH
|
|
9
|
+
? message.substring(0, MAX_LENGTH) + "..."
|
|
10
|
+
: message;
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: colors.success, bold: true, children: [figures.tick, " ", truncatedMessage] }) }), details && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: details.split("\n").map((line, i) => {
|
|
12
|
+
const truncatedLine = line.length > MAX_LENGTH
|
|
13
|
+
? line.substring(0, MAX_LENGTH) + "..."
|
|
14
|
+
: line;
|
|
15
|
+
return (_jsx(Text, { color: colors.textDim, dimColor: true, children: truncatedLine }, i));
|
|
16
|
+
}) }))] }));
|
|
7
17
|
};
|
package/dist/components/Table.js
CHANGED
|
@@ -2,21 +2,29 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import figures from "figures";
|
|
5
|
-
import { colors } from "../utils/theme.js";
|
|
5
|
+
import { colors, sanitizeWidth } from "../utils/theme.js";
|
|
6
6
|
/**
|
|
7
7
|
* Reusable table component for displaying lists of data with optional selection
|
|
8
8
|
* Designed to be responsive and work across devboxes, blueprints, and snapshots
|
|
9
9
|
*/
|
|
10
10
|
export function Table({ data, columns, selectedIndex = -1, showSelection = true, emptyState, keyExtractor, title, }) {
|
|
11
|
+
// Safety: Handle null/undefined data
|
|
12
|
+
if (!data || !Array.isArray(data)) {
|
|
13
|
+
return emptyState ? _jsx(_Fragment, { children: emptyState }) : null;
|
|
14
|
+
}
|
|
11
15
|
if (data.length === 0 && emptyState) {
|
|
12
16
|
return _jsx(_Fragment, { children: emptyState });
|
|
13
17
|
}
|
|
14
18
|
// Filter visible columns
|
|
15
19
|
const visibleColumns = columns.filter((col) => col.visible !== false);
|
|
16
|
-
return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsxs(Text, { color: colors.primary, bold: true, children: ["\u256D\u2500 ", title, " ", "─".repeat(
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsxs(Text, { color: colors.primary, bold: true, children: ["\u256D\u2500 ", title.length > 50 ? title.substring(0, 50) + "..." : title, " ", "─".repeat(10), "\u256E"] }) })), _jsxs(Box, { flexDirection: "column", borderStyle: title ? "single" : "round", borderColor: colors.border, paddingX: 1, children: [_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column) => {
|
|
21
|
+
// Cap column width to prevent Yoga crashes from padEnd creating massive strings
|
|
22
|
+
const safeWidth = sanitizeWidth(column.width, 1, 100);
|
|
23
|
+
return (_jsx(Text, { bold: true, dimColor: true, children: column.label.slice(0, safeWidth).padEnd(safeWidth, " ") }, `header-${column.key}`));
|
|
24
|
+
})] }), data.map((row, index) => {
|
|
17
25
|
const isSelected = index === selectedIndex;
|
|
18
26
|
const rowKey = keyExtractor(row);
|
|
19
|
-
return (_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { color: isSelected ? colors.primary : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column, colIndex) => (_jsx(React.Fragment, { children: column.render(row, index, isSelected) }, `${rowKey}-${column.key}`)))] }, rowKey));
|
|
27
|
+
return (_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { color: isSelected ? colors.primary : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column, colIndex) => (_jsx(React.Fragment, { children: column.render(row, index, isSelected) }, `${rowKey}-${column.key}-${colIndex}`)))] }, rowKey));
|
|
20
28
|
})] })] }));
|
|
21
29
|
}
|
|
22
30
|
/**
|
|
@@ -29,8 +37,10 @@ export function createTextColumn(key, label, getValue, options) {
|
|
|
29
37
|
width: options?.width || 20,
|
|
30
38
|
visible: options?.visible,
|
|
31
39
|
render: (row, index, isSelected) => {
|
|
32
|
-
const value = getValue(row);
|
|
33
|
-
const
|
|
40
|
+
const value = String(getValue(row) || "");
|
|
41
|
+
const rawWidth = options?.width || 20;
|
|
42
|
+
// CRITICAL: Sanitize width to prevent padEnd from creating invalid strings that crash Yoga
|
|
43
|
+
const width = sanitizeWidth(rawWidth, 1, 100);
|
|
34
44
|
const color = options?.color || (isSelected ? colors.text : colors.text);
|
|
35
45
|
const bold = options?.bold !== undefined ? options.bold : isSelected;
|
|
36
46
|
const dimColor = options?.dimColor || false;
|
|
@@ -38,7 +48,7 @@ export function createTextColumn(key, label, getValue, options) {
|
|
|
38
48
|
let truncated;
|
|
39
49
|
if (value.length > width) {
|
|
40
50
|
// Reserve space for ellipsis if truncating
|
|
41
|
-
truncated = value.slice(0, width - 1) + "…";
|
|
51
|
+
truncated = value.slice(0, Math.max(1, width - 1)) + "…";
|
|
42
52
|
}
|
|
43
53
|
else {
|
|
44
54
|
truncated = value;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
5
|
+
import { VERSION } from "../version.js";
|
|
6
|
+
/**
|
|
7
|
+
* Version check component that checks npm for updates and displays a notification
|
|
8
|
+
* Restored from git history and enhanced with better visual styling
|
|
9
|
+
*/
|
|
10
|
+
export const UpdateNotification = () => {
|
|
11
|
+
const [updateAvailable, setUpdateAvailable] = React.useState(null);
|
|
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
|
+
}, []);
|
|
52
|
+
if (isChecking || !updateAvailable) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.warning, paddingX: 1, paddingY: 0, marginTop: 0, children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u2728" }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "Update available:", " "] }), _jsx(Text, { color: colors.warning, bold: true, children: VERSION }), _jsxs(Text, { color: colors.primary, bold: true, children: [" ", "\u2192", " "] }), _jsx(Text, { color: colors.success, bold: true, children: updateAvailable }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "\u2022 Run:", " "] }), _jsx(Text, { color: colors.primary, bold: true, children: "npm install -g @runloop/rl-cli@latest" })] }));
|
|
56
|
+
};
|