@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
@@ -8,6 +8,7 @@ import { SpinnerComponent } from "../../components/Spinner.js";
8
8
  import { ErrorMessage } from "../../components/ErrorMessage.js";
9
9
  import { getStatusDisplay } from "../../components/StatusBadge.js";
10
10
  import { Breadcrumb } from "../../components/Breadcrumb.js";
11
+ import { NavigationTips } from "../../components/NavigationTips.js";
11
12
  import { Table, createTextColumn } from "../../components/Table.js";
12
13
  import { formatTimeAgo } from "../../components/ResourceListView.js";
13
14
  import { output, outputError } from "../../utils/output.js";
@@ -104,46 +105,24 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
104
105
  const sourceWidth = 26;
105
106
  // ID is always full width (25 chars for dbx_31CYd5LLFbBxst8mqnUjO format)
106
107
  const idWidth = 26;
107
- // Responsive layout based on terminal width (simplified like blueprint list)
108
- const showCapabilities = terminalWidth >= 140;
109
- const showSource = terminalWidth >= 120;
108
+ // Responsive layout - hide less important columns on smaller screens
109
+ // Priority (most to least important): ID, Name, Status, Created, Source, Capabilities
110
+ const showCapabilities = terminalWidth >= 160;
111
+ const showSource = terminalWidth >= 135;
112
+ const showCreated = terminalWidth >= 100;
110
113
  // CRITICAL: Absolute maximum column widths to prevent Yoga crashes
111
114
  const ABSOLUTE_MAX_NAME_WIDTH = 80;
112
115
  // Name width is flexible and uses remaining space
113
- let nameWidth = 15;
114
- if (terminalWidth >= 120) {
115
- const remainingWidth = terminalWidth -
116
- fixedWidth -
117
- statusIconWidth -
118
- idWidth -
119
- statusTextWidth -
120
- timeWidth -
121
- capabilitiesWidth -
122
- sourceWidth -
123
- 12;
124
- nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(15, remainingWidth));
125
- }
126
- else if (terminalWidth >= 110) {
127
- const remainingWidth = terminalWidth -
128
- fixedWidth -
129
- statusIconWidth -
130
- idWidth -
131
- statusTextWidth -
132
- timeWidth -
133
- sourceWidth -
134
- 10;
135
- nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(12, remainingWidth));
136
- }
137
- else {
138
- const remainingWidth = terminalWidth -
139
- fixedWidth -
140
- statusIconWidth -
141
- idWidth -
142
- statusTextWidth -
143
- timeWidth -
144
- 10;
145
- nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(8, remainingWidth));
146
- }
116
+ // Only subtract widths of columns that are actually shown
117
+ const baseWidth = fixedWidth +
118
+ statusIconWidth +
119
+ idWidth +
120
+ statusTextWidth +
121
+ (showCreated ? timeWidth : 0) +
122
+ 6; // border + padding
123
+ const optionalWidth = (showSource ? sourceWidth : 0) + (showCapabilities ? capabilitiesWidth : 0);
124
+ const remainingWidth = terminalWidth - baseWidth - optionalWidth;
125
+ const nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(15, remainingWidth));
147
126
  // Build responsive column list (memoized to prevent recreating on every render)
148
127
  const tableColumns = React.useMemo(() => {
149
128
  const ABSOLUTE_MAX_NAME = 80;
@@ -156,22 +135,9 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
156
135
  width: statusIconWidth,
157
136
  render: (devbox, _index, isSelected) => {
158
137
  const statusDisplay = getStatusDisplay(devbox?.status);
159
- const statusColor = statusDisplay.color === colors.textDim
160
- ? colors.info
161
- : statusDisplay.color;
162
- return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
138
+ return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
163
139
  },
164
140
  },
165
- createTextColumn("name", "Name", (devbox) => {
166
- const name = String(devbox?.name || "");
167
- const safeMax = Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME);
168
- return name.length > safeMax
169
- ? name.substring(0, Math.max(1, safeMax - 3)) + "..."
170
- : name;
171
- }, {
172
- width: Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME),
173
- dimColor: false,
174
- }),
175
141
  createTextColumn("id", "ID", (devbox) => {
176
142
  const id = String(devbox?.id || "");
177
143
  const safeMax = Math.min(idWidth || 26, ABSOLUTE_MAX_ID);
@@ -184,6 +150,16 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
184
150
  dimColor: false,
185
151
  bold: false,
186
152
  }),
153
+ createTextColumn("name", "Name", (devbox) => {
154
+ const name = String(devbox?.name || "");
155
+ const safeMax = Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME);
156
+ return name.length > safeMax
157
+ ? name.substring(0, Math.max(1, safeMax - 3)) + "..."
158
+ : name;
159
+ }, {
160
+ width: Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME),
161
+ dimColor: false,
162
+ }),
187
163
  // Status text column with color matching the icon
188
164
  {
189
165
  key: "status",
@@ -191,13 +167,10 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
191
167
  width: statusTextWidth,
192
168
  render: (devbox, _index, isSelected) => {
193
169
  const statusDisplay = getStatusDisplay(devbox?.status);
194
- const statusColor = statusDisplay.color === colors.textDim
195
- ? colors.info
196
- : statusDisplay.color;
197
170
  const safeWidth = Math.max(1, statusTextWidth);
198
171
  const truncated = statusDisplay.text.slice(0, safeWidth);
199
172
  const padded = truncated.padEnd(safeWidth, " ");
200
- return (_jsx(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
173
+ return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
201
174
  },
202
175
  },
203
176
  createTextColumn("created", "Created", (devbox) => {
@@ -208,18 +181,11 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
208
181
  width: timeWidth,
209
182
  color: colors.textDim,
210
183
  dimColor: false,
184
+ visible: showCreated,
211
185
  }),
212
186
  ];
213
187
  if (showSource) {
214
- columns.push(createTextColumn("source", "Source", (devbox) => {
215
- if (devbox?.blueprint_id) {
216
- const bpId = String(devbox.blueprint_id);
217
- const truncated = bpId.slice(0, 16);
218
- const text = `${truncated}`;
219
- return text.length > 30 ? text.substring(0, 27) + "..." : text;
220
- }
221
- return "-";
222
- }, {
188
+ columns.push(createTextColumn("source", "Source", (devbox) => devbox?.blueprint_id || "-", {
223
189
  width: sourceWidth,
224
190
  color: colors.textDim,
225
191
  dimColor: false,
@@ -243,6 +209,7 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
243
209
  idWidth,
244
210
  statusTextWidth,
245
211
  timeWidth,
212
+ showCreated,
246
213
  showSource,
247
214
  sourceWidth,
248
215
  showCapabilities,
@@ -511,7 +478,19 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
511
478
  setSelectedIndex(0);
512
479
  } }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to search, Esc to cancel]"] })] })), !searchMode && submittedSearchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: submittedSearchQuery.length > 50
513
480
  ? submittedSearchQuery.substring(0, 50) + "..."
514
- : submittedSearchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_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 ", totalCount] }), submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), (hasMore || hasPrev) && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Enter] Details"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [c] Create"] }), selectedDevbox && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [o] Open in Browser"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [/] Search"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
481
+ : submittedSearchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No devboxes found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_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 ", totalCount] }), submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
482
+ {
483
+ icon: `${figures.arrowLeft}${figures.arrowRight}`,
484
+ label: "Page",
485
+ condition: hasMore || hasPrev,
486
+ },
487
+ { key: "Enter", label: "Details" },
488
+ { key: "a", label: "Actions" },
489
+ { key: "c", label: "Create" },
490
+ { key: "o", label: "Open in Browser", condition: !!selectedDevbox },
491
+ { key: "/", label: "Search" },
492
+ { key: "Esc", label: "Back" },
493
+ ] })] }));
515
494
  };
516
495
  // Export the UI component for use in the main menu
517
496
  export { ListDevboxesUI };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from "ink";
3
- import { enterAlternateScreenBuffer, exitAlternateScreenBuffer, } from "../utils/screen.js";
3
+ import { enterAlternateScreenBuffer, exitAlternateScreenBuffer, clearScreen, } from "../utils/screen.js";
4
4
  import { processUtils } from "../utils/processUtils.js";
5
5
  import { Router } from "../router/Router.js";
6
6
  import { NavigationProvider } from "../store/navigationStore.js";
@@ -14,6 +14,7 @@ function App({ initialScreen = "menu", focusDevboxId, }) {
14
14
  }
15
15
  export async function runMainMenu(initialScreen = "menu", focusDevboxId) {
16
16
  enterAlternateScreenBuffer();
17
+ clearScreen(); // Ensure cursor is at top-left before Ink renders
17
18
  try {
18
19
  const { waitUntilExit } = render(_jsx(App, { initialScreen: initialScreen, focusDevboxId: focusDevboxId }, `app-${initialScreen}-${focusDevboxId}`), {
19
20
  patchConsole: false,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Create network policy command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function createNetworkPolicy(options) {
7
+ try {
8
+ const client = getClient();
9
+ const policy = await client.networkPolicies.create({
10
+ name: options.name,
11
+ description: options.description,
12
+ allow_all: options.allowAll ?? false,
13
+ allow_devbox_to_devbox: options.allowDevboxToDevbox ?? false,
14
+ allowed_hostnames: options.allowedHostnames ?? [],
15
+ });
16
+ // Default: just output the ID for easy scripting
17
+ if (!options.output || options.output === "text") {
18
+ console.log(policy.id);
19
+ }
20
+ else {
21
+ output(policy, { format: options.output, defaultFormat: "json" });
22
+ }
23
+ }
24
+ catch (error) {
25
+ outputError("Failed to create network policy", error);
26
+ }
27
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Delete network policy command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function deleteNetworkPolicy(id, options = {}) {
7
+ try {
8
+ const client = getClient();
9
+ await client.networkPolicies.delete(id);
10
+ // Default: just output the ID for easy scripting
11
+ if (!options.output || options.output === "text") {
12
+ console.log(id);
13
+ }
14
+ else {
15
+ output({ id, status: "deleted" }, { format: options.output, defaultFormat: "json" });
16
+ }
17
+ }
18
+ catch (error) {
19
+ outputError("Failed to delete network policy", error);
20
+ }
21
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Get network policy details command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function getNetworkPolicy(options) {
7
+ try {
8
+ const client = getClient();
9
+ const policy = await client.networkPolicies.retrieve(options.id);
10
+ output(policy, { format: options.output, defaultFormat: "json" });
11
+ }
12
+ catch (error) {
13
+ outputError("Failed to get network policy", error);
14
+ }
15
+ }