@runloop/rl-cli 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +28 -8
  2. package/dist/commands/blueprint/list.js +97 -28
  3. package/dist/commands/blueprint/prune.js +7 -19
  4. package/dist/commands/devbox/create.js +3 -0
  5. package/dist/commands/devbox/list.js +44 -65
  6. package/dist/commands/menu.js +2 -1
  7. package/dist/commands/network-policy/create.js +27 -0
  8. package/dist/commands/network-policy/delete.js +21 -0
  9. package/dist/commands/network-policy/get.js +15 -0
  10. package/dist/commands/network-policy/list.js +494 -0
  11. package/dist/commands/object/list.js +516 -24
  12. package/dist/commands/snapshot/list.js +90 -29
  13. package/dist/components/Banner.js +109 -8
  14. package/dist/components/ConfirmationPrompt.js +45 -0
  15. package/dist/components/DevboxActionsMenu.js +42 -6
  16. package/dist/components/DevboxCard.js +1 -1
  17. package/dist/components/DevboxCreatePage.js +95 -81
  18. package/dist/components/DevboxDetailPage.js +218 -272
  19. package/dist/components/LogsViewer.js +8 -1
  20. package/dist/components/MainMenu.js +35 -4
  21. package/dist/components/NavigationTips.js +24 -0
  22. package/dist/components/NetworkPolicyCreatePage.js +264 -0
  23. package/dist/components/OperationsMenu.js +9 -1
  24. package/dist/components/ResourceActionsMenu.js +5 -1
  25. package/dist/components/ResourceDetailPage.js +204 -0
  26. package/dist/components/ResourceListView.js +19 -2
  27. package/dist/components/StatusBadge.js +2 -2
  28. package/dist/components/Table.js +6 -8
  29. package/dist/components/form/FormActionButton.js +7 -0
  30. package/dist/components/form/FormField.js +7 -0
  31. package/dist/components/form/FormListManager.js +112 -0
  32. package/dist/components/form/FormSelect.js +34 -0
  33. package/dist/components/form/FormTextInput.js +8 -0
  34. package/dist/components/form/index.js +8 -0
  35. package/dist/hooks/useViewportHeight.js +38 -20
  36. package/dist/router/Router.js +23 -1
  37. package/dist/screens/BlueprintDetailScreen.js +337 -0
  38. package/dist/screens/MenuScreen.js +6 -0
  39. package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
  40. package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
  41. package/dist/screens/NetworkPolicyListScreen.js +7 -0
  42. package/dist/screens/ObjectDetailScreen.js +377 -0
  43. package/dist/screens/ObjectListScreen.js +7 -0
  44. package/dist/screens/SnapshotDetailScreen.js +208 -0
  45. package/dist/services/blueprintService.js +30 -11
  46. package/dist/services/networkPolicyService.js +108 -0
  47. package/dist/services/objectService.js +101 -0
  48. package/dist/services/snapshotService.js +39 -3
  49. package/dist/store/blueprintStore.js +4 -10
  50. package/dist/store/index.js +1 -0
  51. package/dist/store/networkPolicyStore.js +83 -0
  52. package/dist/store/objectStore.js +92 -0
  53. package/dist/store/snapshotStore.js +4 -8
  54. package/dist/utils/commands.js +47 -0
  55. package/package.json +2 -2
@@ -8,9 +8,21 @@ import { SpinnerComponent } from "./Spinner.js";
8
8
  import { ErrorMessage } from "./ErrorMessage.js";
9
9
  import { SuccessMessage } from "./SuccessMessage.js";
10
10
  import { Breadcrumb } from "./Breadcrumb.js";
11
+ import { NavigationTips } from "./NavigationTips.js";
11
12
  import { MetadataDisplay } from "./MetadataDisplay.js";
13
+ import { FormTextInput, FormSelect, FormActionButton, useFormSelectNavigation, } from "./form/index.js";
12
14
  import { colors } from "../utils/theme.js";
13
15
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
16
+ const architectures = ["arm64", "x86_64"];
17
+ const resourceSizes = [
18
+ "X_SMALL",
19
+ "SMALL",
20
+ "MEDIUM",
21
+ "LARGE",
22
+ "X_LARGE",
23
+ "XX_LARGE",
24
+ "CUSTOM_SIZE",
25
+ ];
14
26
  export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initialSnapshotId, }) => {
15
27
  const [currentField, setCurrentField] = React.useState("create");
16
28
  const [formData, setFormData] = React.useState({
@@ -24,53 +36,79 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
24
36
  metadata: {},
25
37
  blueprint_id: initialBlueprintId || "",
26
38
  snapshot_id: initialSnapshotId || "",
39
+ network_policy_id: "",
27
40
  });
28
41
  const [metadataKey, setMetadataKey] = React.useState("");
29
42
  const [metadataValue, setMetadataValue] = React.useState("");
30
43
  const [inMetadataSection, setInMetadataSection] = React.useState(false);
31
44
  const [metadataInputMode, setMetadataInputMode] = React.useState(null);
32
- const [selectedMetadataIndex, setSelectedMetadataIndex] = React.useState(-1); // -1 means "add new" row
45
+ const [selectedMetadataIndex, setSelectedMetadataIndex] = React.useState(0);
33
46
  const [creating, setCreating] = React.useState(false);
34
47
  const [result, setResult] = React.useState(null);
35
48
  const [error, setError] = React.useState(null);
36
49
  const baseFields = [
37
50
  { key: "create", label: "Devbox Create", type: "action" },
38
- { key: "name", label: "Name", type: "text" },
51
+ { key: "name", label: "Name", type: "text", placeholder: "my-devbox" },
39
52
  { key: "architecture", label: "Architecture", type: "select" },
40
53
  { key: "resource_size", label: "Resource Size", type: "select" },
41
54
  ];
42
55
  // Add custom resource fields if CUSTOM_SIZE is selected
43
56
  const customFields = formData.resource_size === "CUSTOM_SIZE"
44
57
  ? [
45
- { key: "custom_cpu", label: "CPU Cores (2-16, even)", type: "text" },
58
+ {
59
+ key: "custom_cpu",
60
+ label: "CPU Cores (2-16, even)",
61
+ type: "text",
62
+ placeholder: "4",
63
+ },
46
64
  {
47
65
  key: "custom_memory",
48
66
  label: "Memory GB (2-64, even)",
49
67
  type: "text",
68
+ placeholder: "8",
69
+ },
70
+ {
71
+ key: "custom_disk",
72
+ label: "Disk GB (2-64, even)",
73
+ type: "text",
74
+ placeholder: "16",
50
75
  },
51
- { key: "custom_disk", label: "Disk GB (2-64, even)", type: "text" },
52
76
  ]
53
77
  : [];
54
78
  const remainingFields = [
55
- { key: "keep_alive", label: "Keep Alive (seconds)", type: "text" },
56
- { key: "blueprint_id", label: "Blueprint ID (optional)", type: "text" },
57
- { key: "snapshot_id", label: "Snapshot ID (optional)", type: "text" },
79
+ {
80
+ key: "keep_alive",
81
+ label: "Keep Alive (seconds)",
82
+ type: "text",
83
+ placeholder: "3600",
84
+ },
85
+ {
86
+ key: "blueprint_id",
87
+ label: "Blueprint ID (optional)",
88
+ type: "text",
89
+ placeholder: "bpt_xxx",
90
+ },
91
+ {
92
+ key: "snapshot_id",
93
+ label: "Snapshot ID (optional)",
94
+ type: "text",
95
+ placeholder: "snp_xxx",
96
+ },
97
+ {
98
+ key: "network_policy_id",
99
+ label: "Network Policy ID (optional)",
100
+ type: "text",
101
+ placeholder: "np_xxx",
102
+ },
58
103
  { key: "metadata", label: "Metadata (optional)", type: "metadata" },
59
104
  ];
60
105
  const fields = [...baseFields, ...customFields, ...remainingFields];
61
- const architectures = ["arm64", "x86_64"];
62
- const resourceSizes = [
63
- "X_SMALL",
64
- "SMALL",
65
- "MEDIUM",
66
- "LARGE",
67
- "X_LARGE",
68
- "XX_LARGE",
69
- "CUSTOM_SIZE",
70
- ];
71
106
  const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
72
107
  // Handle Ctrl+C to exit
73
108
  useExitOnCtrlC();
109
+ // Select navigation handlers using shared hook
110
+ const handleArchitectureNav = useFormSelectNavigation(formData.architecture, architectures, (value) => setFormData({ ...formData, architecture: value }), currentField === "architecture");
111
+ const handleResourceSizeNav = useFormSelectNavigation(formData.resource_size || "SMALL", resourceSizes, (value) => setFormData({ ...formData, resource_size: value }), currentField === "resource_size");
74
112
  useInput((input, key) => {
75
113
  // Handle result screen
76
114
  if (result) {
@@ -98,22 +136,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
98
136
  if (creating) {
99
137
  return;
100
138
  }
101
- // Back to list
102
- if (input === "q" || key.escape) {
103
- onBack();
104
- return;
105
- }
106
- // Submit form
107
- if (input === "s" && key.ctrl) {
108
- handleCreate();
109
- return;
110
- }
111
- // Handle Enter on create field
112
- if (currentField === "create" && key.return) {
113
- handleCreate();
114
- return;
115
- }
116
- // Handle metadata section
139
+ // Handle metadata section FIRST (before general escape handler)
117
140
  if (inMetadataSection) {
118
141
  const metadataKeys = Object.keys(formData.metadata);
119
142
  // Selection model: 0 = "Add new", 1..n = Existing items, n+1 = "Done"
@@ -213,14 +236,33 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
213
236
  }
214
237
  return;
215
238
  }
216
- // Now safe to get field from list
217
- const field = fields[currentFieldIndex];
218
- // Navigation
219
- if (key.upArrow && currentFieldIndex > 0) {
239
+ // Back to list (only when not in metadata section)
240
+ if (input === "q" || key.escape) {
241
+ onBack();
242
+ return;
243
+ }
244
+ // Submit form
245
+ if (input === "s" && key.ctrl) {
246
+ handleCreate();
247
+ return;
248
+ }
249
+ // Handle Enter on create field
250
+ if (currentField === "create" && key.return) {
251
+ handleCreate();
252
+ return;
253
+ }
254
+ // Handle select field navigation using shared hooks
255
+ if (handleArchitectureNav(input, key))
256
+ return;
257
+ if (handleResourceSizeNav(input, key))
258
+ return;
259
+ // Navigation (up/down arrows and tab/shift+tab)
260
+ if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
220
261
  setCurrentField(fields[currentFieldIndex - 1].key);
221
262
  return;
222
263
  }
223
- if (key.downArrow && currentFieldIndex < fields.length - 1) {
264
+ if ((key.downArrow || (key.tab && !key.shift)) &&
265
+ currentFieldIndex < fields.length - 1) {
224
266
  setCurrentField(fields[currentFieldIndex + 1].key);
225
267
  return;
226
268
  }
@@ -230,33 +272,6 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
230
272
  setSelectedMetadataIndex(0); // Start at "add new" row
231
273
  return;
232
274
  }
233
- // Handle select fields
234
- if (field && field.type === "select" && (key.leftArrow || key.rightArrow)) {
235
- if (currentField === "architecture") {
236
- const currentIndex = architectures.indexOf(formData.architecture);
237
- const newIndex = key.leftArrow
238
- ? Math.max(0, currentIndex - 1)
239
- : Math.min(architectures.length - 1, currentIndex + 1);
240
- setFormData({
241
- ...formData,
242
- architecture: architectures[newIndex],
243
- });
244
- }
245
- else if (currentField === "resource_size") {
246
- // Find current index, defaulting to 0 if not found (e.g., empty string)
247
- const currentSize = formData.resource_size || "SMALL";
248
- const currentIndex = resourceSizes.indexOf(currentSize);
249
- const safeIndex = currentIndex === -1 ? 0 : currentIndex;
250
- const newIndex = key.leftArrow
251
- ? Math.max(0, safeIndex - 1)
252
- : Math.min(resourceSizes.length - 1, safeIndex + 1);
253
- setFormData({
254
- ...formData,
255
- resource_size: resourceSizes[newIndex],
256
- });
257
- }
258
- return;
259
- }
260
275
  });
261
276
  // Validate custom resource configuration
262
277
  const validateCustomResources = () => {
@@ -329,6 +344,9 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
329
344
  if (formData.snapshot_id) {
330
345
  createParams.snapshot_id = formData.snapshot_id;
331
346
  }
347
+ if (formData.network_policy_id) {
348
+ launchParameters.network_policy_id = formData.network_policy_id;
349
+ }
332
350
  if (Object.keys(launchParameters).length > 0) {
333
351
  createParams.launch_parameters = launchParameters;
334
352
  }
@@ -344,11 +362,14 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
344
362
  };
345
363
  // Result screen
346
364
  if (result) {
347
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SuccessMessage, { message: "Devbox created successfully!" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: result.id })] }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name: ", result.name || "(none)"] }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Status: ", result.status] }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
365
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SuccessMessage, { message: "Devbox created successfully!" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: result.id })] }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name: ", result.name || "(none)"] }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Status: ", result.status] }) })] }), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Return to list" }] })] }));
348
366
  }
349
367
  // Error screen
350
368
  if (error) {
351
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(ErrorMessage, { message: "Failed to create devbox", error: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] or [r] to retry \u2022 [q] or [esc] to cancel" }) })] }));
369
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(ErrorMessage, { message: "Failed to create devbox", error: error }), _jsx(NavigationTips, { tips: [
370
+ { key: "Enter/r", label: "Retry" },
371
+ { key: "q/esc", label: "Cancel" },
372
+ ] })] }));
352
373
  }
353
374
  // Creating screen
354
375
  if (creating) {
@@ -359,24 +380,14 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
359
380
  const isActive = currentField === field.key;
360
381
  const fieldData = formData[field.key];
361
382
  if (field.type === "action") {
362
- return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.success : colors.textDim, bold: isActive, children: [isActive ? figures.pointer : " ", " ", field.label] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to create]"] }))] }, field.key));
383
+ return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: "[Enter to create]" }, field.key));
363
384
  }
364
385
  if (field.type === "text") {
365
- return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), isActive ? (_jsx(TextInput, { value: String(fieldData || ""), onChange: (value) => {
366
- setFormData({ ...formData, [field.key]: value });
367
- }, placeholder: field.key === "name"
368
- ? "my-devbox"
369
- : field.key === "keep_alive"
370
- ? "3600"
371
- : field.key === "blueprint_id"
372
- ? "bp_xxx"
373
- : field.key === "snapshot_id"
374
- ? "snap_xxx"
375
- : "" })) : (_jsx(Text, { color: colors.text, children: String(fieldData || "(empty)") }))] }, field.key));
386
+ return (_jsx(FormTextInput, { label: field.label, value: String(fieldData || ""), onChange: (value) => setFormData({ ...formData, [field.key]: value }), isActive: isActive, placeholder: field.placeholder }, field.key));
376
387
  }
377
388
  if (field.type === "select") {
378
389
  const value = fieldData;
379
- return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":"] }), _jsxs(Text, { color: isActive ? colors.primary : colors.text, bold: isActive, children: [" ", value || "(none)"] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", figures.arrowLeft, figures.arrowRight, " to change]"] }))] }, field.key));
390
+ return (_jsx(FormSelect, { label: field.label, value: value || "", options: field.key === "architecture" ? architectures : resourceSizes, onChange: (newValue) => setFormData({ ...formData, [field.key]: newValue }), isActive: isActive }, field.key));
380
391
  }
381
392
  if (field.type === "metadata") {
382
393
  if (!inMetadataSection) {
@@ -413,5 +424,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
413
424
  }
414
425
  return null;
415
426
  }) }), formData.resource_size === "CUSTOM_SIZE" &&
416
- validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: colors.error, paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: colors.error, dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Create \u2022 [q] Cancel"] }) }))] }));
427
+ validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: colors.error, paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: colors.error, dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && (_jsx(NavigationTips, { showArrows: true, tips: [
428
+ { key: "Enter", label: "Create" },
429
+ { key: "q", label: "Cancel" },
430
+ ] }))] }));
417
431
  };