@runloop/rl-cli 0.0.3 → 0.1.1
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 +64 -29
- package/dist/cli.js +401 -92
- package/dist/commands/auth.js +12 -11
- package/dist/commands/blueprint/create.js +108 -0
- package/dist/commands/blueprint/get.js +37 -0
- package/dist/commands/blueprint/list.js +293 -225
- package/dist/commands/blueprint/logs.js +40 -0
- package/dist/commands/blueprint/preview.js +45 -0
- package/dist/commands/devbox/create.js +10 -9
- package/dist/commands/devbox/delete.js +8 -8
- package/dist/commands/devbox/download.js +49 -0
- package/dist/commands/devbox/exec.js +23 -13
- package/dist/commands/devbox/execAsync.js +43 -0
- package/dist/commands/devbox/get.js +37 -0
- package/dist/commands/devbox/getAsync.js +37 -0
- package/dist/commands/devbox/list.js +328 -190
- package/dist/commands/devbox/logs.js +40 -0
- package/dist/commands/devbox/read.js +49 -0
- package/dist/commands/devbox/resume.js +37 -0
- package/dist/commands/devbox/rsync.js +118 -0
- package/dist/commands/devbox/scp.js +122 -0
- package/dist/commands/devbox/shutdown.js +37 -0
- package/dist/commands/devbox/ssh.js +104 -0
- package/dist/commands/devbox/suspend.js +37 -0
- package/dist/commands/devbox/tunnel.js +120 -0
- package/dist/commands/devbox/upload.js +10 -10
- package/dist/commands/devbox/write.js +51 -0
- package/dist/commands/mcp-http.js +37 -0
- package/dist/commands/mcp-install.js +120 -0
- package/dist/commands/mcp.js +30 -0
- package/dist/commands/menu.js +20 -20
- package/dist/commands/object/delete.js +37 -0
- package/dist/commands/object/download.js +88 -0
- package/dist/commands/object/get.js +37 -0
- package/dist/commands/object/list.js +112 -0
- package/dist/commands/object/upload.js +130 -0
- package/dist/commands/snapshot/create.js +12 -11
- package/dist/commands/snapshot/delete.js +8 -8
- package/dist/commands/snapshot/list.js +56 -97
- package/dist/commands/snapshot/status.js +37 -0
- package/dist/components/ActionsPopup.js +16 -13
- package/dist/components/Banner.js +4 -4
- package/dist/components/Breadcrumb.js +55 -5
- package/dist/components/DetailView.js +7 -4
- package/dist/components/DevboxActionsMenu.js +315 -178
- package/dist/components/DevboxCard.js +15 -14
- package/dist/components/DevboxCreatePage.js +147 -113
- package/dist/components/DevboxDetailPage.js +180 -102
- package/dist/components/ErrorMessage.js +5 -4
- package/dist/components/Header.js +4 -3
- package/dist/components/MainMenu.js +34 -33
- package/dist/components/MetadataDisplay.js +17 -9
- package/dist/components/OperationsMenu.js +6 -5
- package/dist/components/ResourceActionsMenu.js +117 -0
- package/dist/components/ResourceListView.js +213 -0
- package/dist/components/Spinner.js +5 -4
- package/dist/components/StatusBadge.js +81 -31
- package/dist/components/SuccessMessage.js +4 -3
- package/dist/components/Table.example.js +53 -23
- package/dist/components/Table.js +19 -11
- package/dist/hooks/useCursorPagination.js +125 -0
- package/dist/mcp/server-http.js +416 -0
- package/dist/mcp/server.js +397 -0
- package/dist/utils/CommandExecutor.js +16 -12
- package/dist/utils/client.js +7 -7
- package/dist/utils/config.js +130 -4
- package/dist/utils/interactiveCommand.js +2 -2
- package/dist/utils/output.js +17 -17
- package/dist/utils/ssh.js +160 -0
- package/dist/utils/sshSession.js +16 -12
- package/dist/utils/theme.js +22 -0
- package/dist/utils/url.js +4 -4
- package/package.json +29 -4
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import { Badge } from
|
|
4
|
-
import figures from
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { Badge } from "@inkjs/ui";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { colors } from "../utils/theme.js";
|
|
5
6
|
// Generate color for each key based on hash
|
|
6
7
|
const getColorForKey = (key, index) => {
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const colorList = [
|
|
9
|
+
colors.primary,
|
|
10
|
+
colors.secondary,
|
|
11
|
+
colors.warning,
|
|
12
|
+
colors.info,
|
|
13
|
+
colors.success,
|
|
14
|
+
colors.error,
|
|
15
|
+
];
|
|
16
|
+
return colorList[index % colorList.length];
|
|
9
17
|
};
|
|
10
|
-
export const MetadataDisplay = ({ metadata, title =
|
|
18
|
+
export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = false, selectedKey, }) => {
|
|
11
19
|
const entries = Object.entries(metadata);
|
|
12
20
|
if (entries.length === 0) {
|
|
13
21
|
return null;
|
|
14
22
|
}
|
|
15
|
-
const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color:
|
|
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.info, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
|
|
16
24
|
const color = getColorForKey(key, index);
|
|
17
25
|
const isSelected = selectedKey === key;
|
|
18
|
-
return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color:
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " "] })), _jsx(Badge, { color: isSelected ? colors.primary : color, children: `${key}: ${value}` })] }, key));
|
|
19
27
|
})] }));
|
|
20
28
|
if (showBorder) {
|
|
21
|
-
return (_jsx(Box, { borderStyle: "round", borderColor:
|
|
29
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: colors.accent3, paddingX: 2, paddingY: 1, flexDirection: "column", children: content }));
|
|
22
30
|
}
|
|
23
31
|
return content;
|
|
24
32
|
};
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import figures from
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
4
5
|
/**
|
|
5
6
|
* Reusable operations menu component for detail pages
|
|
6
7
|
* Displays a list of available operations with keyboard navigation
|
|
7
8
|
*/
|
|
8
9
|
export const OperationsMenu = ({ operations, selectedIndex, onNavigate, onSelect, onBack, additionalActions = [], }) => {
|
|
9
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color:
|
|
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) => {
|
|
10
11
|
const isSelected = index === selectedIndex;
|
|
11
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ?
|
|
12
|
-
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color:
|
|
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));
|
|
13
|
+
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022", additionalActions.map((action) => ` [${action.key}] ${action.label} •`), " ", "[q] Back"] }) })] }));
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
15
16
|
* Helper to filter operations based on conditions
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { colors } from "../utils/theme.js";
|
|
6
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
|
+
import { ActionsPopup } from "./ActionsPopup.js";
|
|
8
|
+
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
9
|
+
export const ResourceActionsMenu = (props) => {
|
|
10
|
+
if (props.resourceType === "devbox") {
|
|
11
|
+
const { resource, onBack, breadcrumbItems, initialOperation, initialOperationIndex, skipOperationsMenu, onSSHRequest, } = props;
|
|
12
|
+
return (_jsx(DevboxActionsMenu, { devbox: resource, onBack: onBack, breadcrumbItems: breadcrumbItems, initialOperation: initialOperation, initialOperationIndex: initialOperationIndex, skipOperationsMenu: skipOperationsMenu, onSSHRequest: onSSHRequest }));
|
|
13
|
+
}
|
|
14
|
+
// Blueprint generic actions menu
|
|
15
|
+
const { resource, onBack, breadcrumbItems = [
|
|
16
|
+
{ label: "Blueprints" },
|
|
17
|
+
{ label: resource.name || resource.id, active: true },
|
|
18
|
+
], operations, initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, onExecute, } = props;
|
|
19
|
+
const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
|
|
20
|
+
const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
|
|
21
|
+
const [operationInput, setOperationInput] = React.useState("");
|
|
22
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
23
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (skipOperationsMenu && initialOperation) {
|
|
26
|
+
setExecutingOperation(initialOperation);
|
|
27
|
+
}
|
|
28
|
+
}, [skipOperationsMenu, initialOperation]);
|
|
29
|
+
const selectedOp = operations[selectedOperation] ||
|
|
30
|
+
operations.find((o) => o.key === executingOperation);
|
|
31
|
+
const execute = async () => {
|
|
32
|
+
try {
|
|
33
|
+
const result = await onExecute(executingOperation, {
|
|
34
|
+
input: operationInput.trim() || undefined,
|
|
35
|
+
});
|
|
36
|
+
if (typeof result === "string") {
|
|
37
|
+
setOperationResult(result);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// No result to show; go back
|
|
41
|
+
onBack();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
setOperationError(err);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
useInput((input, key) => {
|
|
49
|
+
// Result screen
|
|
50
|
+
if (operationResult || operationError) {
|
|
51
|
+
if (key.return || key.escape || input === "q") {
|
|
52
|
+
onBack();
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// If executing and needs input
|
|
57
|
+
if (executingOperation) {
|
|
58
|
+
const op = operations.find((o) => o.key === executingOperation);
|
|
59
|
+
if (op?.needsInput) {
|
|
60
|
+
if (key.return) {
|
|
61
|
+
execute();
|
|
62
|
+
}
|
|
63
|
+
else if (input === "q" || key.escape) {
|
|
64
|
+
setExecutingOperation(null);
|
|
65
|
+
setOperationInput("");
|
|
66
|
+
}
|
|
67
|
+
else if (input.length === 1) {
|
|
68
|
+
setOperationInput((prev) => prev + input);
|
|
69
|
+
}
|
|
70
|
+
else if (key.backspace) {
|
|
71
|
+
setOperationInput((prev) => prev.slice(0, -1));
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// No input needed: execute immediately
|
|
76
|
+
execute();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Operations menu navigation
|
|
80
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
81
|
+
setSelectedOperation(selectedOperation - 1);
|
|
82
|
+
}
|
|
83
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
84
|
+
setSelectedOperation(selectedOperation + 1);
|
|
85
|
+
}
|
|
86
|
+
else if (key.return) {
|
|
87
|
+
setExecutingOperation(operations[selectedOperation].key);
|
|
88
|
+
}
|
|
89
|
+
else if (key.escape || input === "q") {
|
|
90
|
+
onBack();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Shortcut keys
|
|
94
|
+
const idx = operations.findIndex((op) => op.shortcut === input);
|
|
95
|
+
if (idx >= 0) {
|
|
96
|
+
setSelectedOperation(idx);
|
|
97
|
+
setExecutingOperation(operations[idx].key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// Screens
|
|
102
|
+
if (operationResult || operationError) {
|
|
103
|
+
const label = operations.find((o) => o.key === executingOperation)?.label;
|
|
104
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _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
|
+
}
|
|
106
|
+
if (executingOperation && selectedOp?.needsInput) {
|
|
107
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _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
|
+
}
|
|
109
|
+
// Operations menu
|
|
110
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: resource, operations: operations.map((op) => ({
|
|
111
|
+
key: op.key,
|
|
112
|
+
label: op.label,
|
|
113
|
+
color: op.color,
|
|
114
|
+
icon: op.icon,
|
|
115
|
+
shortcut: op.shortcut || "",
|
|
116
|
+
})), selectedOperation: selectedOperation, onClose: onBack }) })] }));
|
|
117
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdout, useApp } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import figures from "figures";
|
|
6
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
|
+
import { SpinnerComponent } from "./Spinner.js";
|
|
8
|
+
import { ErrorMessage } from "./ErrorMessage.js";
|
|
9
|
+
import { Table } from "./Table.js";
|
|
10
|
+
import { colors } from "../utils/theme.js";
|
|
11
|
+
// Format time ago in a succinct way
|
|
12
|
+
export const formatTimeAgo = (timestamp) => {
|
|
13
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
14
|
+
if (seconds < 60)
|
|
15
|
+
return `${seconds}s ago`;
|
|
16
|
+
const minutes = Math.floor(seconds / 60);
|
|
17
|
+
if (minutes < 60)
|
|
18
|
+
return `${minutes}m ago`;
|
|
19
|
+
const hours = Math.floor(minutes / 60);
|
|
20
|
+
if (hours < 24)
|
|
21
|
+
return `${hours}h ago`;
|
|
22
|
+
const days = Math.floor(hours / 24);
|
|
23
|
+
if (days < 30)
|
|
24
|
+
return `${days}d ago`;
|
|
25
|
+
const months = Math.floor(days / 30);
|
|
26
|
+
if (months < 12)
|
|
27
|
+
return `${months}mo ago`;
|
|
28
|
+
const years = Math.floor(months / 12);
|
|
29
|
+
return `${years}y ago`;
|
|
30
|
+
};
|
|
31
|
+
export function ResourceListView({ config }) {
|
|
32
|
+
const { stdout } = useStdout();
|
|
33
|
+
const { exit: inkExit } = useApp();
|
|
34
|
+
const [loading, setLoading] = React.useState(true);
|
|
35
|
+
const [resources, setResources] = React.useState([]);
|
|
36
|
+
const [error, setError] = React.useState(null);
|
|
37
|
+
const [currentPage, setCurrentPage] = React.useState(0);
|
|
38
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
39
|
+
const [searchMode, setSearchMode] = React.useState(false);
|
|
40
|
+
const [searchQuery, setSearchQuery] = React.useState("");
|
|
41
|
+
const [refreshing, setRefreshing] = React.useState(false);
|
|
42
|
+
const [refreshIcon, setRefreshIcon] = React.useState(0);
|
|
43
|
+
const pageSize = config.pageSize || 10;
|
|
44
|
+
const maxFetch = config.maxFetch || 100;
|
|
45
|
+
// Calculate responsive dimensions
|
|
46
|
+
const terminalWidth = stdout?.columns || 120;
|
|
47
|
+
const terminalHeight = stdout?.rows || 30;
|
|
48
|
+
// Fetch resources
|
|
49
|
+
const fetchData = React.useCallback(async (isInitialLoad = false) => {
|
|
50
|
+
try {
|
|
51
|
+
if (isInitialLoad) {
|
|
52
|
+
setRefreshing(true);
|
|
53
|
+
}
|
|
54
|
+
const data = await config.fetchResources();
|
|
55
|
+
setResources(data);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
setError(err);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
if (isInitialLoad) {
|
|
63
|
+
setTimeout(() => setRefreshing(false), 300);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, [config.fetchResources]);
|
|
67
|
+
// Initial load
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
fetchData(true);
|
|
70
|
+
}, [fetchData]);
|
|
71
|
+
// Auto-refresh
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (config.autoRefresh?.enabled) {
|
|
74
|
+
const interval = setInterval(() => {
|
|
75
|
+
fetchData(false);
|
|
76
|
+
}, config.autoRefresh.interval || 3000);
|
|
77
|
+
return () => clearInterval(interval);
|
|
78
|
+
}
|
|
79
|
+
}, [config.autoRefresh, fetchData]);
|
|
80
|
+
// Animate refresh icon
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
const interval = setInterval(() => {
|
|
83
|
+
setRefreshIcon((prev) => (prev + 1) % 10);
|
|
84
|
+
}, 80);
|
|
85
|
+
return () => clearInterval(interval);
|
|
86
|
+
}, []);
|
|
87
|
+
// Filter resources based on search query
|
|
88
|
+
const filteredResources = React.useMemo(() => {
|
|
89
|
+
if (!config.searchConfig?.enabled || !searchQuery.trim()) {
|
|
90
|
+
return resources;
|
|
91
|
+
}
|
|
92
|
+
const query = searchQuery.toLowerCase();
|
|
93
|
+
return resources.filter((resource) => {
|
|
94
|
+
const fields = config.searchConfig.fields(resource);
|
|
95
|
+
return fields.some((field) => field.toLowerCase().includes(query));
|
|
96
|
+
});
|
|
97
|
+
}, [resources, searchQuery, config.searchConfig]);
|
|
98
|
+
// Pagination
|
|
99
|
+
const totalPages = Math.ceil(filteredResources.length / pageSize);
|
|
100
|
+
const startIndex = currentPage * pageSize;
|
|
101
|
+
const endIndex = Math.min(startIndex + pageSize, filteredResources.length);
|
|
102
|
+
const currentResources = filteredResources.slice(startIndex, endIndex);
|
|
103
|
+
// Ensure selected index is within bounds
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
if (currentResources.length > 0 &&
|
|
106
|
+
selectedIndex >= currentResources.length) {
|
|
107
|
+
setSelectedIndex(Math.max(0, currentResources.length - 1));
|
|
108
|
+
}
|
|
109
|
+
}, [currentResources.length, selectedIndex]);
|
|
110
|
+
const selectedResource = currentResources[selectedIndex];
|
|
111
|
+
// Input handling
|
|
112
|
+
useInput((input, key) => {
|
|
113
|
+
// Handle Ctrl+C to force exit
|
|
114
|
+
if (key.ctrl && input === "c") {
|
|
115
|
+
process.stdout.write("\x1b[?1049l"); // Exit alternate screen
|
|
116
|
+
process.exit(130);
|
|
117
|
+
}
|
|
118
|
+
const pageResourcesCount = currentResources.length;
|
|
119
|
+
// Skip input handling when in search mode
|
|
120
|
+
if (searchMode) {
|
|
121
|
+
if (key.escape) {
|
|
122
|
+
setSearchMode(false);
|
|
123
|
+
setSearchQuery("");
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Handle list view navigation
|
|
128
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
129
|
+
setSelectedIndex(selectedIndex - 1);
|
|
130
|
+
}
|
|
131
|
+
else if (key.downArrow && selectedIndex < pageResourcesCount - 1) {
|
|
132
|
+
setSelectedIndex(selectedIndex + 1);
|
|
133
|
+
}
|
|
134
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
135
|
+
currentPage < totalPages - 1) {
|
|
136
|
+
setCurrentPage(currentPage + 1);
|
|
137
|
+
setSelectedIndex(0);
|
|
138
|
+
}
|
|
139
|
+
else if ((input === "p" || key.leftArrow) && currentPage > 0) {
|
|
140
|
+
setCurrentPage(currentPage - 1);
|
|
141
|
+
setSelectedIndex(0);
|
|
142
|
+
}
|
|
143
|
+
else if (key.return && selectedResource && config.onSelect) {
|
|
144
|
+
console.clear();
|
|
145
|
+
config.onSelect(selectedResource);
|
|
146
|
+
}
|
|
147
|
+
else if (input === "/" && config.searchConfig?.enabled) {
|
|
148
|
+
setSearchMode(true);
|
|
149
|
+
}
|
|
150
|
+
else if (key.escape) {
|
|
151
|
+
if (searchQuery) {
|
|
152
|
+
setSearchQuery("");
|
|
153
|
+
setCurrentPage(0);
|
|
154
|
+
setSelectedIndex(0);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
if (config.onBack) {
|
|
158
|
+
config.onBack();
|
|
159
|
+
}
|
|
160
|
+
else if (config.onExit) {
|
|
161
|
+
config.onExit();
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
inkExit();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (config.additionalShortcuts) {
|
|
169
|
+
// Handle additional shortcuts
|
|
170
|
+
const shortcut = config.additionalShortcuts.find((s) => s.key === input);
|
|
171
|
+
if (shortcut && selectedResource) {
|
|
172
|
+
shortcut.handler(selectedResource);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// Calculate stats
|
|
177
|
+
const stats = React.useMemo(() => {
|
|
178
|
+
if (!config.statusConfig || !config.getStatus) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const successCount = resources.filter((r) => config.statusConfig.success.includes(config.getStatus(r))).length;
|
|
182
|
+
const warningCount = resources.filter((r) => config.statusConfig.warning.includes(config.getStatus(r))).length;
|
|
183
|
+
const errorCount = resources.filter((r) => config.statusConfig.error.includes(config.getStatus(r))).length;
|
|
184
|
+
return { successCount, warningCount, errorCount };
|
|
185
|
+
}, [resources, config.statusConfig, config.getStatus]);
|
|
186
|
+
// Loading state
|
|
187
|
+
if (loading) {
|
|
188
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: config.breadcrumbItems || [
|
|
189
|
+
{ label: config.resourceNamePlural, active: true },
|
|
190
|
+
] }), _jsx(SpinnerComponent, { message: `Loading ${config.resourceNamePlural.toLowerCase()}...` })] }));
|
|
191
|
+
}
|
|
192
|
+
// Error state
|
|
193
|
+
if (error) {
|
|
194
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: config.breadcrumbItems || [
|
|
195
|
+
{ label: config.resourceNamePlural, active: true },
|
|
196
|
+
] }), _jsx(ErrorMessage, { message: `Failed to list ${config.resourceNamePlural.toLowerCase()}`, error: error })] }));
|
|
197
|
+
}
|
|
198
|
+
// Empty state
|
|
199
|
+
if (!loading && !error && resources.length === 0) {
|
|
200
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: config.breadcrumbItems || [
|
|
201
|
+
{ label: config.resourceNamePlural, active: true },
|
|
202
|
+
] }), config.emptyState && (_jsxs(Box, { children: [_jsx(Text, { color: colors.warning, children: figures.info }), _jsxs(Text, { children: [" ", config.emptyState.message] }), config.emptyState.command && (_jsx(Text, { color: colors.primary, bold: true, children: config.emptyState.command }))] }))] }));
|
|
203
|
+
}
|
|
204
|
+
// List view with data
|
|
205
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: config.breadcrumbItems || [
|
|
206
|
+
{ label: config.resourceNamePlural, active: true },
|
|
207
|
+
] }), config.searchConfig?.enabled && (_jsxs(_Fragment, { children: [searchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointerSmall, " Search:", " "] }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: config.searchConfig.placeholder || "Type to search...", onSubmit: () => {
|
|
208
|
+
setSearchMode(false);
|
|
209
|
+
setCurrentPage(0);
|
|
210
|
+
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: " " }), refreshing ? (_jsx(Text, { color: colors.primary, children: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][refreshIcon % 10] })) : (_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
|
+
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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import Spinner from
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
5
|
+
export const SpinnerComponent = ({ message, }) => {
|
|
6
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: colors.primary, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] }));
|
|
6
7
|
};
|
|
@@ -1,44 +1,94 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Text } from
|
|
3
|
-
import figures from
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
4
5
|
export const getStatusDisplay = (status) => {
|
|
5
6
|
if (!status) {
|
|
6
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
icon: figures.questionMarkPrefix,
|
|
9
|
+
color: colors.textDim,
|
|
10
|
+
text: "UNKNOWN ",
|
|
11
|
+
};
|
|
7
12
|
}
|
|
8
13
|
switch (status) {
|
|
9
|
-
case
|
|
10
|
-
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
case
|
|
16
|
-
return {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
case
|
|
22
|
-
return {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
case
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
case "running":
|
|
15
|
+
return {
|
|
16
|
+
icon: figures.circleFilled,
|
|
17
|
+
color: colors.success,
|
|
18
|
+
text: "RUNNING ",
|
|
19
|
+
};
|
|
20
|
+
case "provisioning":
|
|
21
|
+
return {
|
|
22
|
+
icon: figures.ellipsis,
|
|
23
|
+
color: colors.warning,
|
|
24
|
+
text: "PROVISION ",
|
|
25
|
+
};
|
|
26
|
+
case "initializing":
|
|
27
|
+
return {
|
|
28
|
+
icon: figures.ellipsis,
|
|
29
|
+
color: colors.primary,
|
|
30
|
+
text: "INITIALIZE",
|
|
31
|
+
};
|
|
32
|
+
case "suspended":
|
|
33
|
+
return {
|
|
34
|
+
icon: figures.circleDotted,
|
|
35
|
+
color: colors.warning,
|
|
36
|
+
text: "SUSPENDED ",
|
|
37
|
+
};
|
|
38
|
+
case "failure":
|
|
39
|
+
return { icon: figures.cross, color: colors.error, text: "FAILED " };
|
|
40
|
+
case "shutdown":
|
|
41
|
+
return {
|
|
42
|
+
icon: figures.circle,
|
|
43
|
+
color: colors.textDim,
|
|
44
|
+
text: "SHUTDOWN ",
|
|
45
|
+
};
|
|
46
|
+
case "resuming":
|
|
47
|
+
return {
|
|
48
|
+
icon: figures.ellipsis,
|
|
49
|
+
color: colors.primary,
|
|
50
|
+
text: "RESUMING ",
|
|
51
|
+
};
|
|
52
|
+
case "suspending":
|
|
53
|
+
return {
|
|
54
|
+
icon: figures.ellipsis,
|
|
55
|
+
color: colors.warning,
|
|
56
|
+
text: "SUSPENDING",
|
|
57
|
+
};
|
|
58
|
+
case "ready":
|
|
59
|
+
return {
|
|
60
|
+
icon: figures.bullet,
|
|
61
|
+
color: colors.success,
|
|
62
|
+
text: "READY ",
|
|
63
|
+
};
|
|
64
|
+
case "build_complete":
|
|
65
|
+
case "building_complete":
|
|
66
|
+
return {
|
|
67
|
+
icon: figures.bullet,
|
|
68
|
+
color: colors.success,
|
|
69
|
+
text: "COMPLETE ",
|
|
70
|
+
};
|
|
71
|
+
case "building":
|
|
72
|
+
return {
|
|
73
|
+
icon: figures.ellipsis,
|
|
74
|
+
color: colors.warning,
|
|
75
|
+
text: "BUILDING ",
|
|
76
|
+
};
|
|
77
|
+
case "build_failed":
|
|
78
|
+
case "failed":
|
|
79
|
+
return { icon: figures.cross, color: colors.error, text: "FAILED " };
|
|
34
80
|
default:
|
|
35
81
|
// Truncate and pad any unknown status to 10 chars to match column width
|
|
36
82
|
const truncated = status.toUpperCase().slice(0, 10);
|
|
37
|
-
const padded = truncated.padEnd(10,
|
|
38
|
-
return {
|
|
83
|
+
const padded = truncated.padEnd(10, " ");
|
|
84
|
+
return {
|
|
85
|
+
icon: figures.questionMarkPrefix,
|
|
86
|
+
color: colors.textDim,
|
|
87
|
+
text: padded,
|
|
88
|
+
};
|
|
39
89
|
}
|
|
40
90
|
};
|
|
41
|
-
export const StatusBadge = ({ status, showText = true }) => {
|
|
91
|
+
export const StatusBadge = ({ status, showText = true, }) => {
|
|
42
92
|
const statusDisplay = getStatusDisplay(status);
|
|
43
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 })] }))] }));
|
|
44
94
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import figures from
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
4
5
|
export const SuccessMessage = ({ message, details, }) => {
|
|
5
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color:
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: colors.success, bold: true, children: [figures.tick, " ", message] }) }), details && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: details.split("\n").map((line, i) => (_jsx(Text, { color: colors.textDim, dimColor: true, children: line }, i))) }))] }));
|
|
6
7
|
};
|