@runloop/rl-cli 1.2.0 → 1.4.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.
Files changed (57) hide show
  1. package/README.md +29 -8
  2. package/dist/commands/blueprint/from-dockerfile.js +182 -0
  3. package/dist/commands/blueprint/list.js +97 -28
  4. package/dist/commands/blueprint/prune.js +7 -19
  5. package/dist/commands/devbox/create.js +3 -0
  6. package/dist/commands/devbox/list.js +44 -65
  7. package/dist/commands/menu.js +2 -1
  8. package/dist/commands/network-policy/create.js +27 -0
  9. package/dist/commands/network-policy/delete.js +21 -0
  10. package/dist/commands/network-policy/get.js +15 -0
  11. package/dist/commands/network-policy/list.js +494 -0
  12. package/dist/commands/object/list.js +516 -24
  13. package/dist/commands/snapshot/list.js +90 -29
  14. package/dist/components/Banner.js +109 -8
  15. package/dist/components/ConfirmationPrompt.js +45 -0
  16. package/dist/components/DevboxActionsMenu.js +42 -6
  17. package/dist/components/DevboxCard.js +1 -1
  18. package/dist/components/DevboxCreatePage.js +174 -168
  19. package/dist/components/DevboxDetailPage.js +218 -272
  20. package/dist/components/LogsViewer.js +8 -1
  21. package/dist/components/MainMenu.js +35 -4
  22. package/dist/components/NavigationTips.js +24 -0
  23. package/dist/components/NetworkPolicyCreatePage.js +263 -0
  24. package/dist/components/OperationsMenu.js +9 -1
  25. package/dist/components/ResourceActionsMenu.js +5 -1
  26. package/dist/components/ResourceDetailPage.js +204 -0
  27. package/dist/components/ResourceListView.js +19 -2
  28. package/dist/components/StatusBadge.js +2 -2
  29. package/dist/components/Table.js +6 -8
  30. package/dist/components/form/FormActionButton.js +7 -0
  31. package/dist/components/form/FormField.js +7 -0
  32. package/dist/components/form/FormListManager.js +112 -0
  33. package/dist/components/form/FormSelect.js +34 -0
  34. package/dist/components/form/FormTextInput.js +8 -0
  35. package/dist/components/form/index.js +8 -0
  36. package/dist/hooks/useViewportHeight.js +38 -20
  37. package/dist/router/Router.js +23 -1
  38. package/dist/screens/BlueprintDetailScreen.js +355 -0
  39. package/dist/screens/DevboxDetailScreen.js +4 -4
  40. package/dist/screens/MenuScreen.js +6 -0
  41. package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
  42. package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
  43. package/dist/screens/NetworkPolicyListScreen.js +7 -0
  44. package/dist/screens/ObjectDetailScreen.js +377 -0
  45. package/dist/screens/ObjectListScreen.js +7 -0
  46. package/dist/screens/SnapshotDetailScreen.js +208 -0
  47. package/dist/services/blueprintService.js +30 -11
  48. package/dist/services/networkPolicyService.js +108 -0
  49. package/dist/services/objectService.js +101 -0
  50. package/dist/services/snapshotService.js +39 -3
  51. package/dist/store/blueprintStore.js +4 -10
  52. package/dist/store/index.js +1 -0
  53. package/dist/store/networkPolicyStore.js +83 -0
  54. package/dist/store/objectStore.js +92 -0
  55. package/dist/store/snapshotStore.js +4 -8
  56. package/dist/utils/commands.js +65 -0
  57. package/package.json +2 -2
@@ -4,6 +4,7 @@ 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";
7
+ import { NavigationTips } from "./NavigationTips.js";
7
8
  import { SpinnerComponent } from "./Spinner.js";
8
9
  import { ErrorMessage } from "./ErrorMessage.js";
9
10
  import { Table } from "./Table.js";
@@ -233,6 +234,22 @@ export function ResourceListView({ config }) {
233
234
  setSearchMode(false);
234
235
  setCurrentPage(0);
235
236
  setSelectedIndex(0);
236
- } }), _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 &&
237
- 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"] })] })] }));
237
+ } }), _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 })] }), _jsx(NavigationTips, { showArrows: true, tips: [
238
+ {
239
+ icon: `${figures.arrowLeft}${figures.arrowRight}`,
240
+ label: "Page",
241
+ condition: totalPages > 1,
242
+ },
243
+ { key: "Enter", label: "Details", condition: !!config.onSelect },
244
+ {
245
+ key: "/",
246
+ label: "Search",
247
+ condition: config.searchConfig?.enabled,
248
+ },
249
+ ...(config.additionalShortcuts || []).map((shortcut) => ({
250
+ key: shortcut.key,
251
+ label: shortcut.label,
252
+ })),
253
+ { key: "Esc", label: "Back" },
254
+ ] })] }));
238
255
  }
@@ -68,7 +68,7 @@ export const getStatusDisplay = (status) => {
68
68
  // === ERROR STATES ===
69
69
  case "failure":
70
70
  return {
71
- icon: figures.cross,
71
+ icon: figures.warning,
72
72
  color: colors.error,
73
73
  text: "FAILED ",
74
74
  label: "Failed",
@@ -76,7 +76,7 @@ export const getStatusDisplay = (status) => {
76
76
  case "build_failed":
77
77
  case "failed":
78
78
  return {
79
- icon: figures.cross,
79
+ icon: figures.warning,
80
80
  color: colors.error,
81
81
  text: "FAILED ",
82
82
  label: "Failed",
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Box, Text } from "ink";
4
4
  import figures from "figures";
@@ -10,21 +10,19 @@ import { colors, sanitizeWidth } from "../utils/theme.js";
10
10
  export function Table({ data, columns, selectedIndex = -1, showSelection = true, emptyState, keyExtractor, title, }) {
11
11
  // Safety: Handle null/undefined data
12
12
  if (!data || !Array.isArray(data)) {
13
- return emptyState ? _jsx(_Fragment, { children: emptyState }) : null;
14
- }
15
- if (data.length === 0 && emptyState) {
16
- return _jsx(_Fragment, { children: emptyState });
13
+ data = [];
17
14
  }
15
+ const isEmpty = data.length === 0;
18
16
  // Filter visible columns
19
17
  const visibleColumns = columns.filter((col) => col.visible !== false);
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) => {
18
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", 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", width: "100%", borderStyle: title ? "single" : "round", borderColor: colors.border, paddingX: 1, children: [_jsxs(Box, { width: "100%", children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column) => {
21
19
  // Cap column width to prevent Yoga crashes from padEnd creating massive strings
22
20
  const safeWidth = sanitizeWidth(column.width, 1, 100);
23
21
  return (_jsx(Text, { bold: true, dimColor: true, children: column.label.slice(0, safeWidth).padEnd(safeWidth, " ") }, `header-${column.key}`));
24
- })] }), data.map((row, index) => {
22
+ }), _jsx(Box, { flexGrow: 1 })] }), isEmpty && (_jsxs(Box, { paddingY: 1, width: "100%", children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), emptyState || (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.info, " No items found"] })), _jsx(Box, { flexGrow: 1 })] })), data.map((row, index) => {
25
23
  const isSelected = index === selectedIndex;
26
24
  const rowKey = keyExtractor(row);
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));
25
+ return (_jsxs(Box, { width: "100%", 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}`))), _jsx(Box, { flexGrow: 1 })] }, rowKey));
28
26
  })] })] }));
29
27
  }
30
28
  /**
@@ -0,0 +1,7 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import figures from "figures";
4
+ import { colors } from "../../utils/theme.js";
5
+ export const FormActionButton = ({ label, isActive, hint = "[Enter to execute]", }) => {
6
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.success : colors.textDim, bold: isActive, children: [isActive ? figures.pointer : " ", " ", label] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", hint] }))] }));
7
+ };
@@ -0,0 +1,7 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import figures from "figures";
4
+ import { colors } from "../../utils/theme.js";
5
+ export const FormField = ({ label, isActive, children, hint, error, }) => {
6
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: error ? colors.error : isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", label, ":", " "] }), children, error && (_jsxs(Text, { color: colors.error, children: [" ", figures.cross, " ", error] })), hint && isActive && !error && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", hint] }))] }));
7
+ };
@@ -0,0 +1,112 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * FormListManager - List management component for forms (add/edit/delete items)
4
+ */
5
+ import React from "react";
6
+ import { Box, Text, useInput } from "ink";
7
+ import TextInput from "ink-text-input";
8
+ import figures from "figures";
9
+ import { colors } from "../../utils/theme.js";
10
+ export const FormListManager = ({ title, items, onItemsChange, isActive, isExpanded, onExpandedChange, itemPlaceholder = "item", addLabel = "+ Add new item", collapsedLabel = "items", }) => {
11
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
12
+ const [inputMode, setInputMode] = React.useState(null);
13
+ const [inputValue, setInputValue] = React.useState("");
14
+ const [editingIndex, setEditingIndex] = React.useState(-1);
15
+ // Selection model: 0 = "Add new", 1..n = Existing items, n+1 = "Done"
16
+ const maxIndex = items.length + 1;
17
+ useInput((input, key) => {
18
+ if (!isExpanded)
19
+ return;
20
+ // Handle input mode (typing)
21
+ if (inputMode) {
22
+ if (key.return && inputValue.trim()) {
23
+ if (inputMode === "add") {
24
+ onItemsChange([...items, inputValue.trim()]);
25
+ }
26
+ else if (inputMode === "edit" && editingIndex >= 0) {
27
+ const newItems = [...items];
28
+ newItems[editingIndex] = inputValue.trim();
29
+ onItemsChange(newItems);
30
+ }
31
+ setInputValue("");
32
+ setInputMode(null);
33
+ setEditingIndex(-1);
34
+ setSelectedIndex(0);
35
+ return;
36
+ }
37
+ else if (key.escape) {
38
+ // Cancel input - restore item if editing
39
+ if (inputMode === "edit" && editingIndex >= 0) {
40
+ // Item was already removed, add it back
41
+ const newItems = [...items];
42
+ newItems.splice(editingIndex, 0, inputValue);
43
+ onItemsChange(newItems);
44
+ }
45
+ setInputValue("");
46
+ setInputMode(null);
47
+ setEditingIndex(-1);
48
+ return;
49
+ }
50
+ return;
51
+ }
52
+ // Navigation mode
53
+ if (key.upArrow && selectedIndex > 0) {
54
+ setSelectedIndex(selectedIndex - 1);
55
+ }
56
+ else if (key.downArrow && selectedIndex < maxIndex) {
57
+ setSelectedIndex(selectedIndex + 1);
58
+ }
59
+ else if (key.return) {
60
+ if (selectedIndex === 0) {
61
+ // Add new
62
+ setInputValue("");
63
+ setInputMode("add");
64
+ }
65
+ else if (selectedIndex === maxIndex) {
66
+ // Done - exit expanded mode
67
+ onExpandedChange(false);
68
+ setSelectedIndex(0);
69
+ }
70
+ else if (selectedIndex >= 1 && selectedIndex <= items.length) {
71
+ // Edit existing (selectedIndex - 1 gives array index)
72
+ const itemIndex = selectedIndex - 1;
73
+ const itemToEdit = items[itemIndex];
74
+ setInputValue(itemToEdit);
75
+ setEditingIndex(itemIndex);
76
+ // Remove the item while editing
77
+ const newItems = items.filter((_, i) => i !== itemIndex);
78
+ onItemsChange(newItems);
79
+ setInputMode("edit");
80
+ }
81
+ }
82
+ else if ((input === "d" || key.delete) &&
83
+ selectedIndex >= 1 &&
84
+ selectedIndex <= items.length) {
85
+ // Delete selected item
86
+ const itemIndex = selectedIndex - 1;
87
+ const newItems = items.filter((_, i) => i !== itemIndex);
88
+ onItemsChange(newItems);
89
+ // Adjust selection
90
+ if (selectedIndex > newItems.length) {
91
+ setSelectedIndex(Math.max(0, newItems.length));
92
+ }
93
+ }
94
+ else if (key.escape || input === "q") {
95
+ // Exit expanded mode
96
+ onExpandedChange(false);
97
+ setSelectedIndex(0);
98
+ }
99
+ }, { isActive: isExpanded });
100
+ // Collapsed view
101
+ if (!isExpanded) {
102
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", title, ":", " "] }), _jsxs(Text, { color: colors.text, children: [items.length, " ", collapsedLabel] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), items.length > 0 && (_jsxs(Box, { marginLeft: 3, flexDirection: "column", children: [items.slice(0, 3).map((item, idx) => (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointer, " ", item] }, idx))), items.length > 3 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["... and ", items.length - 3, " more"] }))] }))] }));
103
+ }
104
+ // Expanded view
105
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", title] }), inputMode && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: inputMode === "add" ? colors.success : colors.warning, paddingX: 1, children: [_jsx(Text, { color: inputMode === "add" ? colors.success : colors.warning, bold: true, children: inputMode === "add" ? "Adding New" : "Editing" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.primary, children: "Value: " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, placeholder: itemPlaceholder })] })] })), !inputMode && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedIndex === 0 ? colors.primary : colors.textDim, children: [selectedIndex === 0 ? figures.pointer : " ", " "] }), _jsx(Text, { color: selectedIndex === 0 ? colors.success : colors.textDim, bold: selectedIndex === 0, children: addLabel })] }), items.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: items.map((item, index) => {
106
+ const itemIndex = index + 1;
107
+ const isSelected = selectedIndex === itemIndex;
108
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsx(Text, { color: isSelected ? colors.primary : colors.textDim, bold: isSelected, children: item })] }, index));
109
+ }) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedIndex === maxIndex ? colors.primary : colors.textDim, children: [selectedIndex === maxIndex ? figures.pointer : " ", " "] }), _jsxs(Text, { color: selectedIndex === maxIndex ? colors.success : colors.textDim, bold: selectedIndex === maxIndex, children: [figures.tick, " Done"] })] })] })), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: inputMode
110
+ ? "[Enter] Save • [esc] Cancel"
111
+ : `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedIndex === 0 ? "Add" : selectedIndex === maxIndex ? "Done" : "Edit"} • [d] Delete • [esc] Back` }) })] }));
112
+ };
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * FormSelect - Left/right arrow selection field for forms
4
+ */
5
+ import React from "react";
6
+ import { Text } from "ink";
7
+ import figures from "figures";
8
+ import { FormField } from "./FormField.js";
9
+ import { colors } from "../../utils/theme.js";
10
+ export function FormSelect({ label, value, options, onChange, isActive, getDisplayLabel, }) {
11
+ const displayValue = getDisplayLabel ? getDisplayLabel(value) : value;
12
+ return (_jsx(FormField, { label: label, isActive: isActive, hint: `[${figures.arrowLeft}${figures.arrowRight} to change]`, children: _jsx(Text, { color: isActive ? colors.primary : colors.text, bold: isActive, children: displayValue || "(none)" }) }));
13
+ }
14
+ /**
15
+ * Helper hook to handle left/right arrow navigation for select fields
16
+ */
17
+ export function useFormSelectNavigation(value, options, onChange, isActive) {
18
+ const handleInput = React.useCallback((input, key) => {
19
+ if (!isActive)
20
+ return false;
21
+ if (key.leftArrow || key.rightArrow) {
22
+ const currentIndex = options.indexOf(value);
23
+ const safeIndex = currentIndex === -1 ? 0 : currentIndex;
24
+ // Wrap around: left from first goes to last, right from last goes to first
25
+ const newIndex = key.leftArrow
26
+ ? (safeIndex - 1 + options.length) % options.length
27
+ : (safeIndex + 1) % options.length;
28
+ onChange(options[newIndex]);
29
+ return true;
30
+ }
31
+ return false;
32
+ }, [value, options, onChange, isActive]);
33
+ return handleInput;
34
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import { FormField } from "./FormField.js";
5
+ import { colors } from "../../utils/theme.js";
6
+ export const FormTextInput = ({ label, value, onChange, isActive, placeholder, error, onSubmit, }) => {
7
+ return (_jsx(FormField, { label: label, isActive: isActive, error: error, children: isActive ? (_jsx(TextInput, { value: value, onChange: onChange, placeholder: placeholder, onSubmit: onSubmit })) : (_jsx(Text, { color: error ? colors.error : colors.text, children: value || "(empty)" })) }));
8
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Form Components - Reusable form primitives for create/edit pages
3
+ */
4
+ export { FormField } from "./FormField.js";
5
+ export { FormTextInput } from "./FormTextInput.js";
6
+ export { FormSelect, useFormSelectNavigation, } from "./FormSelect.js";
7
+ export { FormActionButton, } from "./FormActionButton.js";
8
+ export { FormListManager, } from "./FormListManager.js";
@@ -1,8 +1,20 @@
1
1
  import React from "react";
2
2
  import { useStdout } from "ink";
3
+ /**
4
+ * Get safe terminal dimensions with bounds checking
5
+ */
6
+ function getSafeDimensions(stdout) {
7
+ const sampledWidth = stdout?.columns && stdout.columns > 0 ? stdout.columns : 120;
8
+ const sampledHeight = stdout?.rows && stdout.rows > 0 ? stdout.rows : 30;
9
+ return {
10
+ width: Math.max(80, Math.min(300, sampledWidth)),
11
+ height: Math.max(20, Math.min(100, sampledHeight)),
12
+ };
13
+ }
3
14
  /**
4
15
  * Custom hook to calculate available viewport height for content rendering.
5
16
  * Ensures consistent layout calculations across all CLI screens and prevents overflow.
17
+ * Responds to terminal resize events to update layout dynamically.
6
18
  *
7
19
  * @param options Configuration for viewport calculation
8
20
  * @returns Viewport dimensions including available height for content
@@ -16,29 +28,35 @@ import { useStdout } from "ink";
16
28
  export function useViewportHeight(options = {}) {
17
29
  const { overhead = 0, minHeight = 5, maxHeight = 100 } = options;
18
30
  const { stdout } = useStdout();
19
- // Sample terminal dimensions ONCE and use fixed values - no reactive dependencies
20
- // This prevents re-renders and Yoga WASM crashes from dynamic resizing
21
- // CRITICAL: Initialize with safe fallback values to prevent null/undefined
22
- const dimensions = React.useRef({
23
- width: 120,
24
- height: 30,
25
- });
26
- // Only sample on first call when still at default values
27
- if (dimensions.current.width === 120 && dimensions.current.height === 30) {
28
- // Only sample if stdout has valid dimensions
29
- const sampledWidth = stdout?.columns && stdout.columns > 0 ? stdout.columns : 120;
30
- const sampledHeight = stdout?.rows && stdout.rows > 0 ? stdout.rows : 30;
31
- // Always enforce safe bounds to prevent Yoga crashes
32
- dimensions.current = {
33
- width: Math.max(80, Math.min(200, sampledWidth)),
34
- height: Math.max(20, Math.min(100, sampledHeight)),
31
+ // Use state to track dimensions so we can respond to resize events
32
+ const [dimensions, setDimensions] = React.useState(() => getSafeDimensions(stdout));
33
+ // Listen for terminal resize events
34
+ React.useEffect(() => {
35
+ if (!stdout)
36
+ return;
37
+ const handleResize = () => {
38
+ const newDimensions = getSafeDimensions(stdout);
39
+ setDimensions((prev) => {
40
+ // Only update if dimensions actually changed
41
+ if (prev.width !== newDimensions.width ||
42
+ prev.height !== newDimensions.height) {
43
+ return newDimensions;
44
+ }
45
+ return prev;
46
+ });
47
+ };
48
+ // Listen for resize events
49
+ stdout.on("resize", handleResize);
50
+ // Also check dimensions on mount in case they differ from initial
51
+ handleResize();
52
+ return () => {
53
+ stdout.off("resize", handleResize);
35
54
  };
36
- }
37
- const terminalHeight = dimensions.current.height;
38
- const terminalWidth = dimensions.current.width;
55
+ }, [stdout]);
56
+ const terminalHeight = dimensions.height;
57
+ const terminalWidth = dimensions.width;
39
58
  // Calculate viewport height with bounds
40
59
  const viewportHeight = Math.max(minHeight, Math.min(maxHeight, terminalHeight - overhead));
41
- // Removed console.logs to prevent rendering interference
42
60
  return {
43
61
  viewportHeight,
44
62
  terminalHeight,
@@ -8,6 +8,8 @@ import { useNavigation } from "../store/navigationStore.js";
8
8
  import { useDevboxStore } from "../store/devboxStore.js";
9
9
  import { useBlueprintStore } from "../store/blueprintStore.js";
10
10
  import { useSnapshotStore } from "../store/snapshotStore.js";
11
+ import { useNetworkPolicyStore } from "../store/networkPolicyStore.js";
12
+ import { useObjectStore } from "../store/objectStore.js";
11
13
  import { ErrorBoundary } from "../components/ErrorBoundary.js";
12
14
  // Import screen components
13
15
  import { MenuScreen } from "../screens/MenuScreen.js";
@@ -16,8 +18,15 @@ import { DevboxDetailScreen } from "../screens/DevboxDetailScreen.js";
16
18
  import { DevboxActionsScreen } from "../screens/DevboxActionsScreen.js";
17
19
  import { DevboxCreateScreen } from "../screens/DevboxCreateScreen.js";
18
20
  import { BlueprintListScreen } from "../screens/BlueprintListScreen.js";
21
+ import { BlueprintDetailScreen } from "../screens/BlueprintDetailScreen.js";
19
22
  import { BlueprintLogsScreen } from "../screens/BlueprintLogsScreen.js";
20
23
  import { SnapshotListScreen } from "../screens/SnapshotListScreen.js";
24
+ import { SnapshotDetailScreen } from "../screens/SnapshotDetailScreen.js";
25
+ import { NetworkPolicyListScreen } from "../screens/NetworkPolicyListScreen.js";
26
+ import { NetworkPolicyDetailScreen } from "../screens/NetworkPolicyDetailScreen.js";
27
+ import { NetworkPolicyCreateScreen } from "../screens/NetworkPolicyCreateScreen.js";
28
+ import { ObjectListScreen } from "../screens/ObjectListScreen.js";
29
+ import { ObjectDetailScreen } from "../screens/ObjectDetailScreen.js";
21
30
  import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
22
31
  /**
23
32
  * Router component that renders the current screen
@@ -58,6 +67,19 @@ export function Router() {
58
67
  useSnapshotStore.getState().clearAll();
59
68
  }
60
69
  break;
70
+ case "network-policy-list":
71
+ case "network-policy-detail":
72
+ case "network-policy-create":
73
+ if (!currentScreen.startsWith("network-policy")) {
74
+ useNetworkPolicyStore.getState().clearAll();
75
+ }
76
+ break;
77
+ case "object-list":
78
+ case "object-detail":
79
+ if (!currentScreen.startsWith("object")) {
80
+ useObjectStore.getState().clearAll();
81
+ }
82
+ break;
61
83
  }
62
84
  }
63
85
  prevScreenRef.current = currentScreen;
@@ -66,5 +88,5 @@ export function Router() {
66
88
  // and mount new component, preventing race conditions during screen transitions.
67
89
  // The key ensures React treats this as a completely new component tree.
68
90
  // Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
69
- return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
91
+ return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
70
92
  }