@runloop/rl-cli 1.7.1 → 1.9.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 (73) hide show
  1. package/README.md +19 -5
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/blueprint/delete.js +21 -0
  4. package/dist/commands/blueprint/list.js +226 -174
  5. package/dist/commands/blueprint/prune.js +13 -28
  6. package/dist/commands/devbox/create.js +41 -0
  7. package/dist/commands/devbox/list.js +125 -109
  8. package/dist/commands/devbox/tunnel.js +4 -19
  9. package/dist/commands/gateway-config/create.js +44 -0
  10. package/dist/commands/gateway-config/delete.js +21 -0
  11. package/dist/commands/gateway-config/get.js +15 -0
  12. package/dist/commands/gateway-config/list.js +493 -0
  13. package/dist/commands/gateway-config/update.js +60 -0
  14. package/dist/commands/menu.js +2 -1
  15. package/dist/commands/secret/list.js +379 -4
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +108 -0
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +9 -61
  21. package/dist/components/DevboxCreatePage.js +531 -14
  22. package/dist/components/DevboxDetailPage.js +27 -22
  23. package/dist/components/GatewayConfigCreatePage.js +265 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/MainMenu.js +63 -22
  26. package/dist/components/ResourceDetailPage.js +143 -160
  27. package/dist/components/ResourceListView.js +3 -33
  28. package/dist/components/ResourcePicker.js +220 -0
  29. package/dist/components/SecretCreatePage.js +183 -0
  30. package/dist/components/SettingsMenu.js +95 -0
  31. package/dist/components/StateHistory.js +1 -20
  32. package/dist/components/StatusBadge.js +80 -0
  33. package/dist/components/StreamingLogsViewer.js +8 -42
  34. package/dist/components/form/FormTextInput.js +4 -2
  35. package/dist/components/resourceDetailTypes.js +18 -0
  36. package/dist/hooks/useInputHandler.js +103 -0
  37. package/dist/router/Router.js +99 -2
  38. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  39. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  40. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  41. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  42. package/dist/screens/BenchmarkListScreen.js +266 -0
  43. package/dist/screens/BenchmarkMenuScreen.js +29 -0
  44. package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
  45. package/dist/screens/BenchmarkRunListScreen.js +275 -0
  46. package/dist/screens/BlueprintDetailScreen.js +5 -1
  47. package/dist/screens/DevboxCreateScreen.js +2 -2
  48. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  49. package/dist/screens/GatewayConfigListScreen.js +7 -0
  50. package/dist/screens/MenuScreen.js +5 -2
  51. package/dist/screens/ScenarioRunDetailScreen.js +226 -0
  52. package/dist/screens/ScenarioRunListScreen.js +245 -0
  53. package/dist/screens/SecretCreateScreen.js +7 -0
  54. package/dist/screens/SecretDetailScreen.js +198 -0
  55. package/dist/screens/SecretListScreen.js +7 -0
  56. package/dist/screens/SettingsMenuScreen.js +26 -0
  57. package/dist/screens/SnapshotDetailScreen.js +6 -0
  58. package/dist/services/agentService.js +42 -0
  59. package/dist/services/benchmarkJobService.js +122 -0
  60. package/dist/services/benchmarkService.js +120 -0
  61. package/dist/services/gatewayConfigService.js +114 -0
  62. package/dist/services/scenarioService.js +34 -0
  63. package/dist/store/benchmarkJobStore.js +66 -0
  64. package/dist/store/benchmarkStore.js +183 -0
  65. package/dist/store/betaFeatureStore.js +47 -0
  66. package/dist/store/gatewayConfigStore.js +83 -0
  67. package/dist/store/index.js +1 -0
  68. package/dist/utils/browser.js +22 -0
  69. package/dist/utils/clipboard.js +41 -0
  70. package/dist/utils/commands.js +80 -0
  71. package/dist/utils/config.js +8 -0
  72. package/dist/utils/time.js +121 -0
  73. package/package.json +42 -43
@@ -10,9 +10,17 @@ import { SuccessMessage } from "./SuccessMessage.js";
10
10
  import { Breadcrumb } from "./Breadcrumb.js";
11
11
  import { NavigationTips } from "./NavigationTips.js";
12
12
  import { MetadataDisplay } from "./MetadataDisplay.js";
13
+ import { ResourcePicker, createTextColumn } from "./ResourcePicker.js";
14
+ import { formatTimeAgo } from "./ResourceListView.js";
15
+ import { getStatusDisplay } from "./StatusBadge.js";
13
16
  import { FormTextInput, FormSelect, FormActionButton, useFormSelectNavigation, } from "./form/index.js";
14
17
  import { colors } from "../utils/theme.js";
15
18
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
19
+ import { listBlueprints } from "../services/blueprintService.js";
20
+ import { listSnapshots } from "../services/snapshotService.js";
21
+ import { listNetworkPolicies } from "../services/networkPolicyService.js";
22
+ import { listGatewayConfigs } from "../services/gatewayConfigService.js";
23
+ const sourceTypes = ["blueprint", "snapshot"];
16
24
  const architectures = ["arm64", "x86_64"];
17
25
  const resourceSizes = [
18
26
  "X_SMALL",
@@ -37,6 +45,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
37
45
  blueprint_id: initialBlueprintId || "",
38
46
  snapshot_id: initialSnapshotId || "",
39
47
  network_policy_id: "",
48
+ gateways: [],
40
49
  });
41
50
  const [metadataKey, setMetadataKey] = React.useState("");
42
51
  const [metadataValue, setMetadataValue] = React.useState("");
@@ -46,6 +55,22 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
46
55
  const [creating, setCreating] = React.useState(false);
47
56
  const [result, setResult] = React.useState(null);
48
57
  const [error, setError] = React.useState(null);
58
+ // Source picker states (toggle between blueprint/snapshot)
59
+ const [sourceTypeToggle, setSourceTypeToggle] = React.useState(initialSnapshotId ? "snapshot" : "blueprint");
60
+ const [showBlueprintPicker, setShowBlueprintPicker] = React.useState(false);
61
+ const [showSnapshotPicker, setShowSnapshotPicker] = React.useState(false);
62
+ const [showNetworkPolicyPicker, setShowNetworkPolicyPicker] = React.useState(false);
63
+ const [selectedBlueprintName, setSelectedBlueprintName] = React.useState("");
64
+ const [selectedSnapshotName, setSelectedSnapshotName] = React.useState("");
65
+ const [selectedNetworkPolicyName, setSelectedNetworkPolicyName] = React.useState("");
66
+ // Gateway picker states
67
+ const [showGatewayPicker, setShowGatewayPicker] = React.useState(false);
68
+ const [showSecretPicker, setShowSecretPicker] = React.useState(false);
69
+ const [inGatewaySection, setInGatewaySection] = React.useState(false);
70
+ const [gatewayEnvPrefix, setGatewayEnvPrefix] = React.useState("");
71
+ const [gatewayInputMode, setGatewayInputMode] = React.useState(null);
72
+ const [selectedGatewayIndex, setSelectedGatewayIndex] = React.useState(0);
73
+ const [pendingGateway, setPendingGateway] = React.useState(null);
49
74
  const baseFields = [
50
75
  { key: "create", label: "Devbox Create", type: "action" },
51
76
  { key: "name", label: "Name", type: "text", placeholder: "my-devbox" },
@@ -83,22 +108,22 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
83
108
  placeholder: "3600",
84
109
  },
85
110
  {
86
- key: "blueprint_id",
87
- label: "Blueprint ID (optional)",
88
- type: "text",
89
- placeholder: "bpt_xxx",
111
+ key: "source",
112
+ label: "Source (optional)",
113
+ type: "source",
114
+ placeholder: "Select Blueprint or Snapshot...",
90
115
  },
91
116
  {
92
- key: "snapshot_id",
93
- label: "Snapshot ID (optional)",
94
- type: "text",
95
- placeholder: "snp_xxx",
117
+ key: "network_policy_id",
118
+ label: "Network Policy (optional)",
119
+ type: "picker",
120
+ placeholder: "Select a network policy...",
96
121
  },
97
122
  {
98
- key: "network_policy_id",
99
- label: "Network Policy ID (optional)",
100
- type: "text",
101
- placeholder: "np_xxx",
123
+ key: "gateways",
124
+ label: "Gateways (optional)",
125
+ type: "gateways",
126
+ placeholder: "Configure API credential proxying...",
102
127
  },
103
128
  { key: "metadata", label: "Metadata (optional)", type: "metadata" },
104
129
  ];
@@ -109,6 +134,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
109
134
  // Select navigation handlers using shared hook
110
135
  const handleArchitectureNav = useFormSelectNavigation(formData.architecture, architectures, (value) => setFormData({ ...formData, architecture: value }), currentField === "architecture");
111
136
  const handleResourceSizeNav = useFormSelectNavigation(formData.resource_size || "SMALL", resourceSizes, (value) => setFormData({ ...formData, resource_size: value }), currentField === "resource_size");
137
+ const handleSourceTypeNav = useFormSelectNavigation(sourceTypeToggle, sourceTypes, (value) => setSourceTypeToggle(value), currentField === "source");
112
138
  // Main form input handler - active when not in metadata section
113
139
  useInput((input, key) => {
114
140
  // Handle result screen
@@ -155,6 +181,43 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
155
181
  setSelectedMetadataIndex(0);
156
182
  return;
157
183
  }
184
+ // Enter key on gateways field to enter gateway section
185
+ if (currentField === "gateways" && key.return) {
186
+ setInGatewaySection(true);
187
+ setSelectedGatewayIndex(0);
188
+ return;
189
+ }
190
+ // Enter key on source field to open the appropriate picker
191
+ if (currentField === "source" && key.return) {
192
+ // If something is already selected, open that type's picker to change it
193
+ const hasBlueprint = !!(selectedBlueprintName || formData.blueprint_id);
194
+ const hasSnapshot = !!(selectedSnapshotName || formData.snapshot_id);
195
+ if (hasBlueprint) {
196
+ setShowBlueprintPicker(true);
197
+ }
198
+ else if (hasSnapshot) {
199
+ setShowSnapshotPicker(true);
200
+ }
201
+ else {
202
+ // Nothing selected, use the toggle value
203
+ if (sourceTypeToggle === "blueprint") {
204
+ setShowBlueprintPicker(true);
205
+ }
206
+ else {
207
+ setShowSnapshotPicker(true);
208
+ }
209
+ }
210
+ return;
211
+ }
212
+ // Delete key on source field to clear selection
213
+ if (currentField === "source" && (input === "d" || key.delete)) {
214
+ handleClearSource();
215
+ return;
216
+ }
217
+ if (currentField === "network_policy_id" && key.return) {
218
+ setShowNetworkPolicyPicker(true);
219
+ return;
220
+ }
158
221
  // Handle Enter on any field to submit
159
222
  if (key.return) {
160
223
  handleCreate();
@@ -165,6 +228,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
165
228
  return;
166
229
  if (handleResourceSizeNav(input, key))
167
230
  return;
231
+ if (handleSourceTypeNav(input, key))
232
+ return;
168
233
  // Navigation (up/down arrows and tab/shift+tab)
169
234
  if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
170
235
  setCurrentField(fields[currentFieldIndex - 1].key);
@@ -175,7 +240,93 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
175
240
  setCurrentField(fields[currentFieldIndex + 1].key);
176
241
  return;
177
242
  }
178
- }, { isActive: !inMetadataSection });
243
+ }, {
244
+ isActive: !inMetadataSection &&
245
+ !inGatewaySection &&
246
+ !showBlueprintPicker &&
247
+ !showSnapshotPicker &&
248
+ !showNetworkPolicyPicker &&
249
+ !showGatewayPicker &&
250
+ !showSecretPicker,
251
+ });
252
+ // Handle blueprint selection
253
+ const handleBlueprintSelect = React.useCallback((blueprints) => {
254
+ if (blueprints.length > 0) {
255
+ const blueprint = blueprints[0];
256
+ setFormData((prev) => ({
257
+ ...prev,
258
+ blueprint_id: blueprint.id,
259
+ snapshot_id: "",
260
+ }));
261
+ setSelectedBlueprintName(blueprint.name || blueprint.id);
262
+ setSelectedSnapshotName("");
263
+ }
264
+ setShowBlueprintPicker(false);
265
+ }, []);
266
+ // Handle snapshot selection
267
+ const handleSnapshotSelect = React.useCallback((snapshots) => {
268
+ if (snapshots.length > 0) {
269
+ const snapshot = snapshots[0];
270
+ setFormData((prev) => ({
271
+ ...prev,
272
+ snapshot_id: snapshot.id,
273
+ blueprint_id: "",
274
+ }));
275
+ setSelectedSnapshotName(snapshot.name || snapshot.id);
276
+ setSelectedBlueprintName("");
277
+ }
278
+ setShowSnapshotPicker(false);
279
+ }, []);
280
+ // Handle network policy selection
281
+ const handleNetworkPolicySelect = React.useCallback((policies) => {
282
+ if (policies.length > 0) {
283
+ const policy = policies[0];
284
+ setFormData((prev) => ({ ...prev, network_policy_id: policy.id }));
285
+ setSelectedNetworkPolicyName(policy.name || policy.id);
286
+ }
287
+ setShowNetworkPolicyPicker(false);
288
+ }, []);
289
+ // Handle gateway config selection
290
+ const handleGatewaySelect = React.useCallback((configs) => {
291
+ if (configs.length > 0) {
292
+ const config = configs[0];
293
+ setPendingGateway({ id: config.id, name: config.name || config.id });
294
+ setShowGatewayPicker(false);
295
+ // Now show secret picker
296
+ setShowSecretPicker(true);
297
+ }
298
+ else {
299
+ setShowGatewayPicker(false);
300
+ }
301
+ }, []);
302
+ // Handle secret selection for gateway
303
+ const handleSecretSelect = React.useCallback((secrets) => {
304
+ if (secrets.length > 0 && pendingGateway && gatewayEnvPrefix) {
305
+ const secret = secrets[0];
306
+ const newGateway = {
307
+ envPrefix: gatewayEnvPrefix,
308
+ gateway: pendingGateway.id,
309
+ gatewayName: pendingGateway.name,
310
+ secret: secret.id,
311
+ secretName: secret.name || secret.id,
312
+ };
313
+ setFormData((prev) => ({
314
+ ...prev,
315
+ gateways: [...prev.gateways, newGateway],
316
+ }));
317
+ }
318
+ setShowSecretPicker(false);
319
+ setPendingGateway(null);
320
+ setGatewayEnvPrefix("");
321
+ setGatewayInputMode(null);
322
+ setSelectedGatewayIndex(0);
323
+ }, [pendingGateway, gatewayEnvPrefix]);
324
+ // Handle clearing source
325
+ const handleClearSource = React.useCallback(() => {
326
+ setFormData((prev) => ({ ...prev, blueprint_id: "", snapshot_id: "" }));
327
+ setSelectedBlueprintName("");
328
+ setSelectedSnapshotName("");
329
+ }, []);
179
330
  // Metadata section input handler - active when in metadata section
180
331
  useInput((input, key) => {
181
332
  const metadataKeys = Object.keys(formData.metadata);
@@ -265,6 +416,66 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
265
416
  setMetadataInputMode(null);
266
417
  }
267
418
  }, { isActive: inMetadataSection });
419
+ // Gateway section input handler - active when in gateway section
420
+ useInput((input, key) => {
421
+ const gatewayCount = formData.gateways.length;
422
+ const maxIndex = gatewayCount + 1; // Add new + existing items + Done
423
+ // Handle input mode (typing env prefix)
424
+ if (gatewayInputMode === "envPrefix") {
425
+ if (key.return && gatewayEnvPrefix.trim()) {
426
+ // Open gateway picker
427
+ setGatewayInputMode(null);
428
+ setShowGatewayPicker(true);
429
+ return;
430
+ }
431
+ else if (key.escape) {
432
+ setGatewayEnvPrefix("");
433
+ setGatewayInputMode(null);
434
+ return;
435
+ }
436
+ return;
437
+ }
438
+ // Navigation mode in gateway section
439
+ if (key.upArrow && selectedGatewayIndex > 0) {
440
+ setSelectedGatewayIndex(selectedGatewayIndex - 1);
441
+ }
442
+ else if (key.downArrow && selectedGatewayIndex < maxIndex) {
443
+ setSelectedGatewayIndex(selectedGatewayIndex + 1);
444
+ }
445
+ else if (key.return) {
446
+ if (selectedGatewayIndex === 0) {
447
+ // Add new gateway - start with env prefix input
448
+ setGatewayEnvPrefix("");
449
+ setGatewayInputMode("envPrefix");
450
+ }
451
+ else if (selectedGatewayIndex === maxIndex) {
452
+ // Done
453
+ setInGatewaySection(false);
454
+ setSelectedGatewayIndex(0);
455
+ setGatewayEnvPrefix("");
456
+ setGatewayInputMode(null);
457
+ }
458
+ }
459
+ else if ((input === "d" || key.delete) &&
460
+ selectedGatewayIndex >= 1 &&
461
+ selectedGatewayIndex <= gatewayCount) {
462
+ // Delete gateway at index
463
+ const indexToDelete = selectedGatewayIndex - 1;
464
+ const newGateways = [...formData.gateways];
465
+ newGateways.splice(indexToDelete, 1);
466
+ setFormData({ ...formData, gateways: newGateways });
467
+ const newLength = newGateways.length;
468
+ if (selectedGatewayIndex > newLength) {
469
+ setSelectedGatewayIndex(Math.max(0, newLength));
470
+ }
471
+ }
472
+ else if (key.escape || input === "q") {
473
+ setInGatewaySection(false);
474
+ setSelectedGatewayIndex(0);
475
+ setGatewayEnvPrefix("");
476
+ setGatewayInputMode(null);
477
+ }
478
+ }, { isActive: inGatewaySection && !showGatewayPicker && !showSecretPicker });
268
479
  // Validate custom resource configuration
269
480
  const validateCustomResources = () => {
270
481
  if (formData.resource_size !== "CUSTOM_SIZE") {
@@ -342,6 +553,17 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
342
553
  if (Object.keys(launchParameters).length > 0) {
343
554
  createParams.launch_parameters = launchParameters;
344
555
  }
556
+ // Add gateway specifications
557
+ if (formData.gateways.length > 0) {
558
+ const gateways = {};
559
+ for (const gw of formData.gateways) {
560
+ gateways[gw.envPrefix] = {
561
+ gateway: gw.gateway,
562
+ secret: gw.secret,
563
+ };
564
+ }
565
+ createParams.gateways = gateways;
566
+ }
345
567
  const devbox = await client.devboxes.create(createParams);
346
568
  setResult(devbox);
347
569
  }
@@ -367,6 +589,240 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
367
589
  if (creating) {
368
590
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
369
591
  }
592
+ // Blueprint picker screen
593
+ if (showBlueprintPicker) {
594
+ const blueprintColumns = [
595
+ {
596
+ key: "statusIcon",
597
+ label: "",
598
+ width: 2,
599
+ render: (blueprint, _index, isSelected) => {
600
+ const statusDisplay = getStatusDisplay(blueprint.status || "");
601
+ return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
602
+ },
603
+ },
604
+ createTextColumn("id", "ID", (blueprint) => blueprint.id, {
605
+ width: 25,
606
+ color: colors.idColor,
607
+ }),
608
+ createTextColumn("name", "Name", (blueprint) => blueprint.name || "", { width: 30 }),
609
+ {
610
+ key: "status",
611
+ label: "Status",
612
+ width: 12,
613
+ render: (blueprint, _index, isSelected) => {
614
+ const statusDisplay = getStatusDisplay(blueprint.status || "");
615
+ const padded = statusDisplay.text.slice(0, 12).padEnd(12, " ");
616
+ return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: padded }));
617
+ },
618
+ },
619
+ createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
620
+ ? formatTimeAgo(blueprint.create_time_ms)
621
+ : "", { width: 18, color: colors.textDim }),
622
+ ];
623
+ // Filter out failed blueprints
624
+ const failedStatuses = ["failure", "build_failed", "failed"];
625
+ return (_jsx(ResourcePicker, { config: {
626
+ title: "Select Blueprint",
627
+ fetchPage: async (params) => {
628
+ const result = await listBlueprints({
629
+ limit: params.limit,
630
+ startingAfter: params.startingAt,
631
+ search: params.search,
632
+ });
633
+ // Filter out failed blueprints
634
+ const validBlueprints = result.blueprints.filter((bp) => !failedStatuses.includes(bp.status || ""));
635
+ return {
636
+ items: validBlueprints,
637
+ hasMore: result.hasMore,
638
+ totalCount: validBlueprints.length,
639
+ };
640
+ },
641
+ getItemId: (blueprint) => blueprint.id,
642
+ getItemLabel: (blueprint) => blueprint.name || blueprint.id,
643
+ columns: blueprintColumns,
644
+ mode: "single",
645
+ emptyMessage: "No blueprints found (failed blueprints are hidden)",
646
+ searchPlaceholder: "Search blueprints...",
647
+ breadcrumbItems: [
648
+ { label: "Devboxes" },
649
+ { label: "Create" },
650
+ { label: "Select Blueprint", active: true },
651
+ ],
652
+ }, onSelect: handleBlueprintSelect, onCancel: () => setShowBlueprintPicker(false), initialSelected: formData.blueprint_id ? [formData.blueprint_id] : [] }));
653
+ }
654
+ // Snapshot picker screen
655
+ if (showSnapshotPicker) {
656
+ const snapshotColumns = [
657
+ createTextColumn("id", "ID", (snapshot) => snapshot.id, {
658
+ width: 25,
659
+ color: colors.idColor,
660
+ }),
661
+ createTextColumn("name", "Name", (snapshot) => snapshot.name || "", { width: 30 }),
662
+ createTextColumn("status", "Status", (snapshot) => snapshot.status || "", { width: 12 }),
663
+ createTextColumn("created", "Created", (snapshot) => snapshot.create_time_ms ? formatTimeAgo(snapshot.create_time_ms) : "", { width: 18, color: colors.textDim }),
664
+ ];
665
+ return (_jsx(ResourcePicker, { config: {
666
+ title: "Select Snapshot",
667
+ fetchPage: async (params) => {
668
+ const result = await listSnapshots({
669
+ limit: params.limit,
670
+ startingAfter: params.startingAt,
671
+ });
672
+ return {
673
+ items: result.snapshots,
674
+ hasMore: result.hasMore,
675
+ totalCount: result.totalCount,
676
+ };
677
+ },
678
+ getItemId: (snapshot) => snapshot.id,
679
+ getItemLabel: (snapshot) => snapshot.name || snapshot.id,
680
+ columns: snapshotColumns,
681
+ mode: "single",
682
+ emptyMessage: "No snapshots found",
683
+ searchPlaceholder: "Search snapshots...",
684
+ breadcrumbItems: [
685
+ { label: "Devboxes" },
686
+ { label: "Create" },
687
+ { label: "Select Snapshot", active: true },
688
+ ],
689
+ }, onSelect: handleSnapshotSelect, onCancel: () => setShowSnapshotPicker(false), initialSelected: formData.snapshot_id ? [formData.snapshot_id] : [] }));
690
+ }
691
+ // Network policy picker screen
692
+ if (showNetworkPolicyPicker) {
693
+ // Helper to get egress type label
694
+ const getEgressLabel = (egress) => {
695
+ if (egress.allow_all)
696
+ return "Allow All";
697
+ if (egress.allowed_hostnames?.length === 0)
698
+ return "Deny All";
699
+ return `Custom (${egress.allowed_hostnames?.length || 0})`;
700
+ };
701
+ const networkPolicyColumns = [
702
+ createTextColumn("id", "ID", (policy) => policy.id, {
703
+ width: 25,
704
+ color: colors.idColor,
705
+ }),
706
+ createTextColumn("name", "Name", (policy) => policy.name || "", { width: 25 }),
707
+ createTextColumn("egress", "Egress", (policy) => getEgressLabel(policy.egress), { width: 15 }),
708
+ createTextColumn("created", "Created", (policy) => policy.create_time_ms ? formatTimeAgo(policy.create_time_ms) : "", { width: 18, color: colors.textDim }),
709
+ ];
710
+ return (_jsx(ResourcePicker, { config: {
711
+ title: "Select Network Policy",
712
+ fetchPage: async (params) => {
713
+ const result = await listNetworkPolicies({
714
+ limit: params.limit,
715
+ startingAfter: params.startingAt,
716
+ search: params.search,
717
+ });
718
+ return {
719
+ items: result.networkPolicies,
720
+ hasMore: result.hasMore,
721
+ totalCount: result.totalCount,
722
+ };
723
+ },
724
+ getItemId: (policy) => policy.id,
725
+ getItemLabel: (policy) => policy.name || policy.id,
726
+ columns: networkPolicyColumns,
727
+ mode: "single",
728
+ emptyMessage: "No network policies found",
729
+ searchPlaceholder: "Search network policies...",
730
+ breadcrumbItems: [
731
+ { label: "Devboxes" },
732
+ { label: "Create" },
733
+ { label: "Select Network Policy", active: true },
734
+ ],
735
+ }, onSelect: handleNetworkPolicySelect, onCancel: () => setShowNetworkPolicyPicker(false), initialSelected: formData.network_policy_id ? [formData.network_policy_id] : [] }));
736
+ }
737
+ // Gateway config picker screen
738
+ if (showGatewayPicker) {
739
+ const gatewayColumns = [
740
+ createTextColumn("id", "ID", (config) => config.id, {
741
+ width: 25,
742
+ color: colors.idColor,
743
+ }),
744
+ createTextColumn("name", "Name", (config) => config.name || "", { width: 25 }),
745
+ createTextColumn("endpoint", "Endpoint", (config) => config.endpoint || "", { width: 30, color: colors.textDim }),
746
+ createTextColumn("created", "Created", (config) => config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "", { width: 18, color: colors.textDim }),
747
+ ];
748
+ return (_jsx(ResourcePicker, { config: {
749
+ title: "Select Gateway Config",
750
+ fetchPage: async (params) => {
751
+ const result = await listGatewayConfigs({
752
+ limit: params.limit,
753
+ startingAfter: params.startingAt,
754
+ search: params.search,
755
+ });
756
+ return {
757
+ items: result.gatewayConfigs,
758
+ hasMore: result.hasMore,
759
+ totalCount: result.totalCount,
760
+ };
761
+ },
762
+ getItemId: (config) => config.id,
763
+ getItemLabel: (config) => config.name || config.id,
764
+ columns: gatewayColumns,
765
+ mode: "single",
766
+ emptyMessage: "No gateway configs found",
767
+ searchPlaceholder: "Search gateway configs...",
768
+ breadcrumbItems: [
769
+ { label: "Devboxes" },
770
+ { label: "Create" },
771
+ { label: `Gateway: ${gatewayEnvPrefix}`, active: true },
772
+ ],
773
+ }, onSelect: handleGatewaySelect, onCancel: () => {
774
+ setShowGatewayPicker(false);
775
+ setGatewayEnvPrefix("");
776
+ setGatewayInputMode(null);
777
+ }, initialSelected: [] }, "gateway-config-picker"));
778
+ }
779
+ // Secret picker screen (for gateway)
780
+ if (showSecretPicker) {
781
+ const secretColumns = [
782
+ createTextColumn("id", "ID", (secret) => secret.id, {
783
+ width: 25,
784
+ color: colors.idColor,
785
+ }),
786
+ createTextColumn("name", "Name", (secret) => secret.name || "", { width: 30 }),
787
+ createTextColumn("created", "Created", (secret) => secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "", { width: 18, color: colors.textDim }),
788
+ ];
789
+ return (_jsx(ResourcePicker, { config: {
790
+ title: "Select Secret for Gateway",
791
+ fetchPage: async (params) => {
792
+ const client = getClient();
793
+ // Secrets API doesn't support cursor pagination, just limit
794
+ const page = await client.secrets.list({
795
+ limit: params.limit,
796
+ });
797
+ return {
798
+ items: (page.secrets || []).map((s) => ({
799
+ id: s.id,
800
+ name: s.name,
801
+ create_time_ms: s.create_time_ms,
802
+ })),
803
+ hasMore: false, // Secrets API doesn't support pagination
804
+ totalCount: page.total_count || 0,
805
+ };
806
+ },
807
+ getItemId: (secret) => secret.id,
808
+ getItemLabel: (secret) => secret.name || secret.id,
809
+ columns: secretColumns,
810
+ mode: "single",
811
+ emptyMessage: "No secrets found",
812
+ searchPlaceholder: "Search secrets...",
813
+ breadcrumbItems: [
814
+ { label: "Devboxes" },
815
+ { label: "Create" },
816
+ { label: `Gateway: ${gatewayEnvPrefix}` },
817
+ { label: "Select Secret", active: true },
818
+ ],
819
+ }, onSelect: handleSecretSelect, onCancel: () => {
820
+ setShowSecretPicker(false);
821
+ setPendingGateway(null);
822
+ setGatewayEnvPrefix("");
823
+ setGatewayInputMode(null);
824
+ }, initialSelected: [] }, "secret-picker"));
825
+ }
370
826
  // Form screen
371
827
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
372
828
  const isActive = currentField === field.key;
@@ -381,6 +837,35 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
381
837
  const value = fieldData;
382
838
  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));
383
839
  }
840
+ if (field.type === "source") {
841
+ // Check if either blueprint or snapshot is selected
842
+ const selectedBlueprintValue = selectedBlueprintName || formData.blueprint_id;
843
+ const selectedSnapshotValue = selectedSnapshotName || formData.snapshot_id;
844
+ const hasBlueprint = !!selectedBlueprintValue;
845
+ const hasSnapshot = !!selectedSnapshotValue;
846
+ const hasSelection = hasBlueprint || hasSnapshot;
847
+ // If something is selected, show it clearly with its type
848
+ if (hasSelection) {
849
+ const selectedType = hasBlueprint ? "Blueprint" : "Snapshot";
850
+ const selectedValue = hasBlueprint
851
+ ? selectedBlueprintValue
852
+ : selectedSnapshotValue;
853
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: colors.success, children: [selectedType, ": "] }), _jsx(Text, { color: colors.idColor, children: selectedValue }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to change, d to clear]"] }))] }, field.key));
854
+ }
855
+ // Nothing selected - show toggle to choose type
856
+ 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.text : colors.textDim, children: [isActive ? figures.arrowLeft : "", " "] }), _jsx(Text, { color: sourceTypeToggle === "blueprint"
857
+ ? colors.primary
858
+ : colors.textDim, bold: sourceTypeToggle === "blueprint", children: "Blueprint" }), _jsx(Text, { color: colors.textDim, children: " / " }), _jsx(Text, { color: sourceTypeToggle === "snapshot"
859
+ ? colors.primary
860
+ : colors.textDim, bold: sourceTypeToggle === "snapshot", children: "Snapshot" }), _jsxs(Text, { color: isActive ? colors.text : colors.textDim, children: [" ", isActive ? figures.arrowRight : ""] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }, field.key));
861
+ }
862
+ if (field.type === "picker") {
863
+ const value = fieldData;
864
+ const displayName = field.key === "network_policy_id"
865
+ ? selectedNetworkPolicyName || value
866
+ : value;
867
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), displayName ? (_jsx(Text, { color: colors.idColor, children: displayName })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: field.placeholder || "(none)" })), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to ", displayName ? "change" : "select", "]"] }))] }, field.key));
868
+ }
384
869
  if (field.type === "metadata") {
385
870
  if (!inMetadataSection) {
386
871
  // Collapsed view
@@ -414,9 +899,41 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
414
899
  ? `[Tab] Switch field • [Enter] ${metadataInputMode === "key" ? "Next" : "Save"} • [esc] Cancel`
415
900
  : `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedMetadataIndex === 0 ? "Add" : selectedMetadataIndex === maxIndex ? "Done" : "Edit"} • [d] Delete • [esc] Back` }) })] }, field.key));
416
901
  }
902
+ if (field.type === "gateways") {
903
+ if (!inGatewaySection) {
904
+ // Collapsed view
905
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: colors.text, children: [formData.gateways.length, " gateway(s)"] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), formData.gateways.length > 0 && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: formData.gateways.map((gw, idx) => (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointer, " ", gw.envPrefix, ": ", gw.gatewayName, " \u2192", " ", gw.secretName] }, idx))) }))] }, field.key));
906
+ }
907
+ // Expanded gateway section view
908
+ const gatewayCount = formData.gateways.length;
909
+ const maxGatewayIndex = gatewayCount + 1;
910
+ 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, " Manage Gateway Configurations"] }), gatewayInputMode === "envPrefix" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: colors.success, paddingX: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Adding New Gateway" }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.primary, children: ["Env Prefix (e.g., GWS_ANTHROPIC):", " "] }), _jsx(TextInput, { value: gatewayEnvPrefix || "", onChange: setGatewayEnvPrefix, placeholder: "GWS_ANTHROPIC" })] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Enter to select gateway config" })] })), !gatewayInputMode && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === 0
911
+ ? colors.primary
912
+ : colors.textDim, children: [selectedGatewayIndex === 0
913
+ ? figures.pointer
914
+ : " ", " "] }), _jsx(Text, { color: selectedGatewayIndex === 0
915
+ ? colors.success
916
+ : colors.textDim, bold: selectedGatewayIndex === 0, children: "+ Add new gateway" })] }), gatewayCount > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: formData.gateways.map((gw, index) => {
917
+ const itemIndex = index + 1;
918
+ const isGatewaySelected = selectedGatewayIndex === itemIndex;
919
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isGatewaySelected
920
+ ? colors.primary
921
+ : colors.textDim, children: [isGatewaySelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isGatewaySelected
922
+ ? colors.primary
923
+ : colors.textDim, bold: isGatewaySelected, children: [gw.envPrefix, ": ", gw.gatewayName, " \u2192", " ", gw.secretName] })] }, gw.envPrefix));
924
+ }) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
925
+ ? colors.primary
926
+ : colors.textDim, children: [selectedGatewayIndex === maxGatewayIndex
927
+ ? figures.pointer
928
+ : " ", " "] }), _jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
929
+ ? colors.success
930
+ : colors.textDim, bold: selectedGatewayIndex === maxGatewayIndex, children: [figures.tick, " Done"] })] })] })), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: gatewayInputMode
931
+ ? `[Enter] Select gateway • [esc] Cancel`
932
+ : `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedGatewayIndex === 0 ? "Add" : selectedGatewayIndex === maxGatewayIndex ? "Done" : "Select"} • [d] Delete • [esc] Back` }) })] }, field.key));
933
+ }
417
934
  return null;
418
935
  }) }), formData.resource_size === "CUSTOM_SIZE" &&
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: [
936
+ 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 && !inGatewaySection && (_jsx(NavigationTips, { showArrows: true, tips: [
420
937
  { key: "Enter", label: "Create" },
421
938
  { key: "q", label: "Cancel" },
422
939
  ] }))] }));