@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.
Files changed (73) hide show
  1. package/README.md +64 -29
  2. package/dist/cli.js +401 -92
  3. package/dist/commands/auth.js +12 -11
  4. package/dist/commands/blueprint/create.js +108 -0
  5. package/dist/commands/blueprint/get.js +37 -0
  6. package/dist/commands/blueprint/list.js +293 -225
  7. package/dist/commands/blueprint/logs.js +40 -0
  8. package/dist/commands/blueprint/preview.js +45 -0
  9. package/dist/commands/devbox/create.js +10 -9
  10. package/dist/commands/devbox/delete.js +8 -8
  11. package/dist/commands/devbox/download.js +49 -0
  12. package/dist/commands/devbox/exec.js +23 -13
  13. package/dist/commands/devbox/execAsync.js +43 -0
  14. package/dist/commands/devbox/get.js +37 -0
  15. package/dist/commands/devbox/getAsync.js +37 -0
  16. package/dist/commands/devbox/list.js +328 -190
  17. package/dist/commands/devbox/logs.js +40 -0
  18. package/dist/commands/devbox/read.js +49 -0
  19. package/dist/commands/devbox/resume.js +37 -0
  20. package/dist/commands/devbox/rsync.js +118 -0
  21. package/dist/commands/devbox/scp.js +122 -0
  22. package/dist/commands/devbox/shutdown.js +37 -0
  23. package/dist/commands/devbox/ssh.js +104 -0
  24. package/dist/commands/devbox/suspend.js +37 -0
  25. package/dist/commands/devbox/tunnel.js +120 -0
  26. package/dist/commands/devbox/upload.js +10 -10
  27. package/dist/commands/devbox/write.js +51 -0
  28. package/dist/commands/mcp-http.js +37 -0
  29. package/dist/commands/mcp-install.js +120 -0
  30. package/dist/commands/mcp.js +30 -0
  31. package/dist/commands/menu.js +20 -20
  32. package/dist/commands/object/delete.js +37 -0
  33. package/dist/commands/object/download.js +88 -0
  34. package/dist/commands/object/get.js +37 -0
  35. package/dist/commands/object/list.js +112 -0
  36. package/dist/commands/object/upload.js +130 -0
  37. package/dist/commands/snapshot/create.js +12 -11
  38. package/dist/commands/snapshot/delete.js +8 -8
  39. package/dist/commands/snapshot/list.js +56 -97
  40. package/dist/commands/snapshot/status.js +37 -0
  41. package/dist/components/ActionsPopup.js +16 -13
  42. package/dist/components/Banner.js +4 -4
  43. package/dist/components/Breadcrumb.js +55 -5
  44. package/dist/components/DetailView.js +7 -4
  45. package/dist/components/DevboxActionsMenu.js +315 -178
  46. package/dist/components/DevboxCard.js +15 -14
  47. package/dist/components/DevboxCreatePage.js +147 -113
  48. package/dist/components/DevboxDetailPage.js +180 -102
  49. package/dist/components/ErrorMessage.js +5 -4
  50. package/dist/components/Header.js +4 -3
  51. package/dist/components/MainMenu.js +34 -33
  52. package/dist/components/MetadataDisplay.js +17 -9
  53. package/dist/components/OperationsMenu.js +6 -5
  54. package/dist/components/ResourceActionsMenu.js +117 -0
  55. package/dist/components/ResourceListView.js +213 -0
  56. package/dist/components/Spinner.js +5 -4
  57. package/dist/components/StatusBadge.js +81 -31
  58. package/dist/components/SuccessMessage.js +4 -3
  59. package/dist/components/Table.example.js +53 -23
  60. package/dist/components/Table.js +19 -11
  61. package/dist/hooks/useCursorPagination.js +125 -0
  62. package/dist/mcp/server-http.js +416 -0
  63. package/dist/mcp/server.js +397 -0
  64. package/dist/utils/CommandExecutor.js +16 -12
  65. package/dist/utils/client.js +7 -7
  66. package/dist/utils/config.js +130 -4
  67. package/dist/utils/interactiveCommand.js +2 -2
  68. package/dist/utils/output.js +17 -17
  69. package/dist/utils/ssh.js +160 -0
  70. package/dist/utils/sshSession.js +16 -12
  71. package/dist/utils/theme.js +22 -0
  72. package/dist/utils/url.js +4 -4
  73. 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 'ink';
3
- import { Badge } from '@inkjs/ui';
4
- import figures from 'figures';
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 colors = ['cyan', 'magenta', 'yellow', 'blue', 'green', 'red'];
8
- return colors[index % colors.length];
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 = 'Metadata', showBorder = false, selectedKey }) => {
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: "#0a4d3a", bold: true, children: [figures.info, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
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: "cyan", bold: true, children: [figures.pointer, " "] })), _jsx(Badge, { color: isSelected ? 'cyan' : color, children: `${key}: ${value}` })] }, key));
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: "#0a4d3a", paddingX: 2, paddingY: 1, flexDirection: "column", children: content }));
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 'ink';
3
- import figures from 'figures';
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: "cyan", bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
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 ? 'cyan' : 'gray', children: [isSelected ? figures.pointer : ' ', ' '] }), _jsxs(Text, { color: isSelected ? op.color : 'gray', bold: isSelected, children: [op.icon, " ", op.label] })] }, op.key));
12
- }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022", additionalActions.map((action) => ` [${action.key}] ${action.label} •`), ' ', "[q] Back"] }) })] }));
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 'ink';
3
- import Spinner from 'ink-spinner';
4
- export const SpinnerComponent = ({ message }) => {
5
- return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] }));
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 'ink';
3
- import figures from 'figures';
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 { icon: figures.questionMarkPrefix, color: 'gray', text: 'UNKNOWN' };
7
+ return {
8
+ icon: figures.questionMarkPrefix,
9
+ color: colors.textDim,
10
+ text: "UNKNOWN ",
11
+ };
7
12
  }
8
13
  switch (status) {
9
- case 'running':
10
- return { icon: figures.circleFilled, color: 'green', text: 'RUNNING ' };
11
- case 'provisioning':
12
- return { icon: figures.ellipsis, color: 'yellow', text: 'PROVISION ' };
13
- case 'initializing':
14
- return { icon: figures.ellipsis, color: 'cyan', text: 'INITIALIZE' };
15
- case 'suspended':
16
- return { icon: figures.circleDotted, color: 'yellow', text: 'SUSPENDED ' };
17
- case 'failure':
18
- return { icon: figures.cross, color: 'red', text: 'FAILED ' };
19
- case 'shutdown':
20
- return { icon: figures.circle, color: 'gray', text: 'SHUTDOWN ' };
21
- case 'resuming':
22
- return { icon: figures.ellipsis, color: 'cyan', text: 'RESUMING ' };
23
- case 'suspending':
24
- return { icon: figures.ellipsis, color: 'yellow', text: 'SUSPENDING' };
25
- case 'ready':
26
- return { icon: figures.tick, color: 'green', text: 'READY ' };
27
- case 'build_complete':
28
- case 'building_complete':
29
- return { icon: figures.tick, color: 'green', text: 'COMPLETE ' };
30
- case 'building':
31
- return { icon: figures.ellipsis, color: 'yellow', text: 'BUILDING ' };
32
- case 'build_failed':
33
- return { icon: figures.cross, color: 'red', text: 'FAILED ' };
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 { icon: figures.questionMarkPrefix, color: 'gray', text: padded };
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 'ink';
3
- import figures from 'figures';
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: "green", bold: true, children: [figures.tick, " ", message] }) }), details && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: details.split('\n').map((line, i) => (_jsx(Text, { color: "gray", dimColor: true, children: line }, i))) }))] }));
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
  };