@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,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,80 @@ 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");
112
+ // Main form input handler - active when not in metadata section
74
113
  useInput((input, key) => {
75
114
  // Handle result screen
76
115
  if (result) {
@@ -78,7 +117,9 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
78
117
  if (onCreate) {
79
118
  onCreate(result);
80
119
  }
81
- onBack();
120
+ else {
121
+ onBack();
122
+ }
82
123
  }
83
124
  return;
84
125
  }
@@ -103,161 +144,127 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
103
144
  onBack();
104
145
  return;
105
146
  }
106
- // Submit form
147
+ // Submit form with Ctrl+S
107
148
  if (input === "s" && key.ctrl) {
108
149
  handleCreate();
109
150
  return;
110
151
  }
111
- // Handle Enter on create field
112
- if (currentField === "create" && key.return) {
152
+ // Enter key on metadata field to enter metadata section
153
+ if (currentField === "metadata" && key.return) {
154
+ setInMetadataSection(true);
155
+ setSelectedMetadataIndex(0);
156
+ return;
157
+ }
158
+ // Handle Enter on any field to submit
159
+ if (key.return) {
113
160
  handleCreate();
114
161
  return;
115
162
  }
116
- // Handle metadata section
117
- if (inMetadataSection) {
118
- const metadataKeys = Object.keys(formData.metadata);
119
- // Selection model: 0 = "Add new", 1..n = Existing items, n+1 = "Done"
120
- const maxIndex = metadataKeys.length + 1;
121
- // Handle input mode (typing key or value)
122
- if (metadataInputMode) {
123
- if (metadataInputMode === "key" && key.return && metadataKey.trim()) {
124
- setMetadataInputMode("value");
125
- return;
126
- }
127
- else if (metadataInputMode === "value" && key.return) {
128
- if (metadataKey.trim() && metadataValue.trim()) {
129
- setFormData({
130
- ...formData,
131
- metadata: {
132
- ...formData.metadata,
133
- [metadataKey.trim()]: metadataValue.trim(),
134
- },
135
- });
136
- }
137
- setMetadataKey("");
138
- setMetadataValue("");
139
- setMetadataInputMode(null);
140
- setSelectedMetadataIndex(0); // Back to "add new" row
141
- return;
142
- }
143
- else if (key.escape) {
144
- // Cancel input
145
- setMetadataKey("");
146
- setMetadataValue("");
147
- setMetadataInputMode(null);
148
- return;
149
- }
150
- else if (key.tab) {
151
- // Tab between key and value
152
- setMetadataInputMode(metadataInputMode === "key" ? "value" : "key");
153
- return;
154
- }
155
- return; // Don't process other keys while in input mode
156
- }
157
- // Navigation mode
158
- if (key.upArrow && selectedMetadataIndex > 0) {
159
- setSelectedMetadataIndex(selectedMetadataIndex - 1);
160
- }
161
- else if (key.downArrow && selectedMetadataIndex < maxIndex) {
162
- setSelectedMetadataIndex(selectedMetadataIndex + 1);
163
- }
164
- else if (key.return) {
165
- if (selectedMetadataIndex === 0) {
166
- // Add new
167
- setMetadataKey("");
168
- setMetadataValue("");
169
- setMetadataInputMode("key");
170
- }
171
- else if (selectedMetadataIndex === maxIndex) {
172
- // Done - exit metadata section
173
- setInMetadataSection(false);
174
- setSelectedMetadataIndex(0);
175
- setMetadataKey("");
176
- setMetadataValue("");
177
- setMetadataInputMode(null);
178
- }
179
- else if (selectedMetadataIndex >= 1 &&
180
- selectedMetadataIndex <= metadataKeys.length) {
181
- // Edit existing (selectedMetadataIndex - 1 gives array index)
182
- const keyToEdit = metadataKeys[selectedMetadataIndex - 1];
183
- setMetadataKey(keyToEdit || "");
184
- setMetadataValue(formData.metadata[keyToEdit] || "");
185
- // Remove old entry
186
- const newMetadata = { ...formData.metadata };
187
- delete newMetadata[keyToEdit];
188
- setFormData({ ...formData, metadata: newMetadata });
189
- setMetadataInputMode("key");
190
- }
163
+ // Handle select field navigation using shared hooks
164
+ if (handleArchitectureNav(input, key))
165
+ return;
166
+ if (handleResourceSizeNav(input, key))
167
+ return;
168
+ // Navigation (up/down arrows and tab/shift+tab)
169
+ if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
170
+ setCurrentField(fields[currentFieldIndex - 1].key);
171
+ return;
172
+ }
173
+ if ((key.downArrow || (key.tab && !key.shift)) &&
174
+ currentFieldIndex < fields.length - 1) {
175
+ setCurrentField(fields[currentFieldIndex + 1].key);
176
+ return;
177
+ }
178
+ }, { isActive: !inMetadataSection });
179
+ // Metadata section input handler - active when in metadata section
180
+ useInput((input, key) => {
181
+ const metadataKeys = Object.keys(formData.metadata);
182
+ const maxIndex = metadataKeys.length + 1;
183
+ // Handle input mode (typing key or value)
184
+ if (metadataInputMode) {
185
+ if (metadataInputMode === "key" && key.return && metadataKey.trim()) {
186
+ setMetadataInputMode("value");
187
+ return;
191
188
  }
192
- else if ((input === "d" || key.delete) &&
193
- selectedMetadataIndex >= 1 &&
194
- selectedMetadataIndex <= metadataKeys.length) {
195
- // Delete selected item (selectedMetadataIndex - 1 gives array index)
196
- const keyToDelete = metadataKeys[selectedMetadataIndex - 1];
197
- const newMetadata = { ...formData.metadata };
198
- delete newMetadata[keyToDelete];
199
- setFormData({ ...formData, metadata: newMetadata });
200
- // Stay at same position or move to add new if we deleted the last item
201
- const newLength = Object.keys(newMetadata).length;
202
- if (selectedMetadataIndex > newLength) {
203
- setSelectedMetadataIndex(Math.max(0, newLength));
189
+ else if (metadataInputMode === "value" && key.return) {
190
+ if (metadataKey.trim() && metadataValue.trim()) {
191
+ setFormData({
192
+ ...formData,
193
+ metadata: {
194
+ ...formData.metadata,
195
+ [metadataKey.trim()]: metadataValue.trim(),
196
+ },
197
+ });
204
198
  }
205
- }
206
- else if (key.escape || input === "q") {
207
- // Exit metadata section
208
- setInMetadataSection(false);
199
+ setMetadataKey("");
200
+ setMetadataValue("");
201
+ setMetadataInputMode(null);
209
202
  setSelectedMetadataIndex(0);
203
+ return;
204
+ }
205
+ else if (key.escape) {
210
206
  setMetadataKey("");
211
207
  setMetadataValue("");
212
208
  setMetadataInputMode(null);
209
+ return;
210
+ }
211
+ else if (key.tab) {
212
+ setMetadataInputMode(metadataInputMode === "key" ? "value" : "key");
213
+ return;
213
214
  }
214
215
  return;
215
216
  }
216
- // Now safe to get field from list
217
- const field = fields[currentFieldIndex];
218
- // Navigation
219
- if (key.upArrow && currentFieldIndex > 0) {
220
- setCurrentField(fields[currentFieldIndex - 1].key);
221
- return;
222
- }
223
- if (key.downArrow && currentFieldIndex < fields.length - 1) {
224
- setCurrentField(fields[currentFieldIndex + 1].key);
225
- return;
217
+ // Navigation mode in metadata section
218
+ if (key.upArrow && selectedMetadataIndex > 0) {
219
+ setSelectedMetadataIndex(selectedMetadataIndex - 1);
226
220
  }
227
- // Enter key on metadata field to enter metadata section
228
- if (currentField === "metadata" && key.return) {
229
- setInMetadataSection(true);
230
- setSelectedMetadataIndex(0); // Start at "add new" row
231
- return;
221
+ else if (key.downArrow && selectedMetadataIndex < maxIndex) {
222
+ setSelectedMetadataIndex(selectedMetadataIndex + 1);
232
223
  }
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
- });
224
+ else if (key.return) {
225
+ if (selectedMetadataIndex === 0) {
226
+ setMetadataKey("");
227
+ setMetadataValue("");
228
+ setMetadataInputMode("key");
244
229
  }
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
- });
230
+ else if (selectedMetadataIndex === maxIndex) {
231
+ setInMetadataSection(false);
232
+ setSelectedMetadataIndex(0);
233
+ setMetadataKey("");
234
+ setMetadataValue("");
235
+ setMetadataInputMode(null);
236
+ }
237
+ else if (selectedMetadataIndex >= 1 &&
238
+ selectedMetadataIndex <= metadataKeys.length) {
239
+ const keyToEdit = metadataKeys[selectedMetadataIndex - 1];
240
+ setMetadataKey(keyToEdit || "");
241
+ setMetadataValue(formData.metadata[keyToEdit] || "");
242
+ const newMetadata = { ...formData.metadata };
243
+ delete newMetadata[keyToEdit];
244
+ setFormData({ ...formData, metadata: newMetadata });
245
+ setMetadataInputMode("key");
257
246
  }
258
- return;
259
247
  }
260
- });
248
+ else if ((input === "d" || key.delete) &&
249
+ selectedMetadataIndex >= 1 &&
250
+ selectedMetadataIndex <= metadataKeys.length) {
251
+ const keyToDelete = metadataKeys[selectedMetadataIndex - 1];
252
+ const newMetadata = { ...formData.metadata };
253
+ delete newMetadata[keyToDelete];
254
+ setFormData({ ...formData, metadata: newMetadata });
255
+ const newLength = Object.keys(newMetadata).length;
256
+ if (selectedMetadataIndex > newLength) {
257
+ setSelectedMetadataIndex(Math.max(0, newLength));
258
+ }
259
+ }
260
+ else if (key.escape || input === "q") {
261
+ setInMetadataSection(false);
262
+ setSelectedMetadataIndex(0);
263
+ setMetadataKey("");
264
+ setMetadataValue("");
265
+ setMetadataInputMode(null);
266
+ }
267
+ }, { isActive: inMetadataSection });
261
268
  // Validate custom resource configuration
262
269
  const validateCustomResources = () => {
263
270
  if (formData.resource_size !== "CUSTOM_SIZE") {
@@ -329,6 +336,9 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
329
336
  if (formData.snapshot_id) {
330
337
  createParams.snapshot_id = formData.snapshot_id;
331
338
  }
339
+ if (formData.network_policy_id) {
340
+ launchParameters.network_policy_id = formData.network_policy_id;
341
+ }
332
342
  if (Object.keys(launchParameters).length > 0) {
333
343
  createParams.launch_parameters = launchParameters;
334
344
  }
@@ -344,11 +354,14 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
344
354
  };
345
355
  // Result screen
346
356
  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" }) })] }));
357
+ 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
358
  }
349
359
  // Error screen
350
360
  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" }) })] }));
361
+ 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: [
362
+ { key: "Enter/r", label: "Retry" },
363
+ { key: "q/esc", label: "Cancel" },
364
+ ] })] }));
352
365
  }
353
366
  // Creating screen
354
367
  if (creating) {
@@ -359,24 +372,14 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
359
372
  const isActive = currentField === field.key;
360
373
  const fieldData = formData[field.key];
361
374
  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));
375
+ return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: "[Enter to create]" }, field.key));
363
376
  }
364
377
  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));
378
+ return (_jsx(FormTextInput, { label: field.label, value: String(fieldData || ""), onChange: (value) => setFormData({ ...formData, [field.key]: value }), onSubmit: handleCreate, isActive: isActive, placeholder: field.placeholder }, field.key));
376
379
  }
377
380
  if (field.type === "select") {
378
381
  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));
382
+ 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
383
  }
381
384
  if (field.type === "metadata") {
382
385
  if (!inMetadataSection) {
@@ -413,5 +416,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
413
416
  }
414
417
  return null;
415
418
  }) }), 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"] }) }))] }));
419
+ 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: [
420
+ { key: "Enter", label: "Create" },
421
+ { key: "q", label: "Cancel" },
422
+ ] }))] }));
417
423
  };