@runloop/rl-cli 1.9.0 → 1.10.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.
- package/README.md +2 -2
- package/dist/commands/devbox/list.js +17 -1
- package/dist/commands/devbox/rsync.js +69 -41
- package/dist/commands/devbox/scp.js +180 -39
- package/dist/commands/gateway-config/create.js +22 -13
- package/dist/commands/gateway-config/get.js +7 -4
- package/dist/commands/gateway-config/list.js +11 -11
- package/dist/commands/gateway-config/update.js +37 -27
- package/dist/components/DevboxActionsMenu.js +17 -1
- package/dist/components/DevboxCreatePage.js +330 -99
- package/dist/components/DevboxDetailPage.js +46 -2
- package/dist/components/GatewayConfigCreatePage.js +35 -28
- package/dist/components/ResourcePicker.js +21 -7
- package/dist/components/SecretCreatePage.js +69 -23
- package/dist/components/SettingsMenu.js +1 -1
- package/dist/screens/GatewayConfigDetailScreen.js +14 -14
- package/dist/screens/SecretDetailScreen.js +26 -2
- package/dist/services/gatewayConfigService.js +39 -0
- package/dist/utils/commands.js +29 -13
- package/dist/utils/gatewayConfigValidation.js +58 -0
- package/package.json +2 -1
|
@@ -20,6 +20,8 @@ import { listBlueprints } from "../services/blueprintService.js";
|
|
|
20
20
|
import { listSnapshots } from "../services/snapshotService.js";
|
|
21
21
|
import { listNetworkPolicies } from "../services/networkPolicyService.js";
|
|
22
22
|
import { listGatewayConfigs } from "../services/gatewayConfigService.js";
|
|
23
|
+
import { SecretCreatePage } from "./SecretCreatePage.js";
|
|
24
|
+
import { GatewayConfigCreatePage } from "./GatewayConfigCreatePage.js";
|
|
23
25
|
const sourceTypes = ["blueprint", "snapshot"];
|
|
24
26
|
const architectures = ["arm64", "x86_64"];
|
|
25
27
|
const resourceSizes = [
|
|
@@ -31,6 +33,7 @@ const resourceSizes = [
|
|
|
31
33
|
"XX_LARGE",
|
|
32
34
|
"CUSTOM_SIZE",
|
|
33
35
|
];
|
|
36
|
+
const tunnelAuthModes = ["none", "open", "authenticated"];
|
|
34
37
|
export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initialSnapshotId, }) => {
|
|
35
38
|
const [currentField, setCurrentField] = React.useState("create");
|
|
36
39
|
const [formData, setFormData] = React.useState({
|
|
@@ -45,6 +48,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
45
48
|
blueprint_id: initialBlueprintId || "",
|
|
46
49
|
snapshot_id: initialSnapshotId || "",
|
|
47
50
|
network_policy_id: "",
|
|
51
|
+
tunnel_auth_mode: "none",
|
|
48
52
|
gateways: [],
|
|
49
53
|
});
|
|
50
54
|
const [metadataKey, setMetadataKey] = React.useState("");
|
|
@@ -68,9 +72,16 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
68
72
|
const [showSecretPicker, setShowSecretPicker] = React.useState(false);
|
|
69
73
|
const [inGatewaySection, setInGatewaySection] = React.useState(false);
|
|
70
74
|
const [gatewayEnvPrefix, setGatewayEnvPrefix] = React.useState("");
|
|
71
|
-
const [gatewayInputMode, setGatewayInputMode] = React.useState(null);
|
|
72
75
|
const [selectedGatewayIndex, setSelectedGatewayIndex] = React.useState(0);
|
|
73
76
|
const [pendingGateway, setPendingGateway] = React.useState(null);
|
|
77
|
+
const [pendingSecret, setPendingSecret] = React.useState(null);
|
|
78
|
+
const [showInlineSecretCreate, setShowInlineSecretCreate] = React.useState(false);
|
|
79
|
+
const [showInlineGatewayConfigCreate, setShowInlineGatewayConfigCreate] = React.useState(false);
|
|
80
|
+
// Gateway attach form: when active, shows a mini-form to configure a gateway
|
|
81
|
+
const [gatewayFormActive, setGatewayFormActive] = React.useState(false);
|
|
82
|
+
const [gatewayFormField, setGatewayFormField] = React.useState("attach");
|
|
83
|
+
const gatewayFormFields = ["attach", "gateway", "envName", "secret"];
|
|
84
|
+
const gatewayFormFieldIndex = gatewayFormFields.indexOf(gatewayFormField);
|
|
74
85
|
const baseFields = [
|
|
75
86
|
{ key: "create", label: "Devbox Create", type: "action" },
|
|
76
87
|
{ key: "name", label: "Name", type: "text", placeholder: "my-devbox" },
|
|
@@ -119,9 +130,15 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
119
130
|
type: "picker",
|
|
120
131
|
placeholder: "Select a network policy...",
|
|
121
132
|
},
|
|
133
|
+
{
|
|
134
|
+
key: "tunnel_auth_mode",
|
|
135
|
+
label: "Tunnel (optional)",
|
|
136
|
+
type: "select",
|
|
137
|
+
placeholder: "none",
|
|
138
|
+
},
|
|
122
139
|
{
|
|
123
140
|
key: "gateways",
|
|
124
|
-
label: "
|
|
141
|
+
label: "AI Gateway Configs (optional)",
|
|
125
142
|
type: "gateways",
|
|
126
143
|
placeholder: "Configure API credential proxying...",
|
|
127
144
|
},
|
|
@@ -134,6 +151,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
134
151
|
// Select navigation handlers using shared hook
|
|
135
152
|
const handleArchitectureNav = useFormSelectNavigation(formData.architecture, architectures, (value) => setFormData({ ...formData, architecture: value }), currentField === "architecture");
|
|
136
153
|
const handleResourceSizeNav = useFormSelectNavigation(formData.resource_size || "SMALL", resourceSizes, (value) => setFormData({ ...formData, resource_size: value }), currentField === "resource_size");
|
|
154
|
+
const handleTunnelNav = useFormSelectNavigation(formData.tunnel_auth_mode, tunnelAuthModes, (value) => setFormData({ ...formData, tunnel_auth_mode: value }), currentField === "tunnel_auth_mode");
|
|
137
155
|
const handleSourceTypeNav = useFormSelectNavigation(sourceTypeToggle, sourceTypes, (value) => setSourceTypeToggle(value), currentField === "source");
|
|
138
156
|
// Main form input handler - active when not in metadata section
|
|
139
157
|
useInput((input, key) => {
|
|
@@ -228,6 +246,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
228
246
|
return;
|
|
229
247
|
if (handleResourceSizeNav(input, key))
|
|
230
248
|
return;
|
|
249
|
+
if (handleTunnelNav(input, key))
|
|
250
|
+
return;
|
|
231
251
|
if (handleSourceTypeNav(input, key))
|
|
232
252
|
return;
|
|
233
253
|
// Navigation (up/down arrows and tab/shift+tab)
|
|
@@ -247,7 +267,9 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
247
267
|
!showSnapshotPicker &&
|
|
248
268
|
!showNetworkPolicyPicker &&
|
|
249
269
|
!showGatewayPicker &&
|
|
250
|
-
!showSecretPicker
|
|
270
|
+
!showSecretPicker &&
|
|
271
|
+
!showInlineSecretCreate &&
|
|
272
|
+
!showInlineGatewayConfigCreate,
|
|
251
273
|
});
|
|
252
274
|
// Handle blueprint selection
|
|
253
275
|
const handleBlueprintSelect = React.useCallback((blueprints) => {
|
|
@@ -290,10 +312,21 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
290
312
|
const handleGatewaySelect = React.useCallback((configs) => {
|
|
291
313
|
if (configs.length > 0) {
|
|
292
314
|
const config = configs[0];
|
|
293
|
-
|
|
315
|
+
const configName = config.name || config.id;
|
|
316
|
+
setPendingGateway({
|
|
317
|
+
id: config.id,
|
|
318
|
+
name: configName,
|
|
319
|
+
endpoint: config.endpoint || "",
|
|
320
|
+
});
|
|
321
|
+
// Auto-fill ENV name from config name (uppercase, underscores, no GWS_ prefix)
|
|
322
|
+
const autoEnvName = configName
|
|
323
|
+
.toUpperCase()
|
|
324
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
325
|
+
.replace(/^_|_$/g, "");
|
|
326
|
+
setGatewayEnvPrefix(autoEnvName);
|
|
294
327
|
setShowGatewayPicker(false);
|
|
295
|
-
//
|
|
296
|
-
|
|
328
|
+
// Move to env name field in the form
|
|
329
|
+
setGatewayFormField("envName");
|
|
297
330
|
}
|
|
298
331
|
else {
|
|
299
332
|
setShowGatewayPicker(false);
|
|
@@ -301,26 +334,38 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
301
334
|
}, []);
|
|
302
335
|
// Handle secret selection for gateway
|
|
303
336
|
const handleSecretSelect = React.useCallback((secrets) => {
|
|
304
|
-
if (secrets.length > 0
|
|
337
|
+
if (secrets.length > 0) {
|
|
305
338
|
const secret = secrets[0];
|
|
306
|
-
|
|
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
|
-
}));
|
|
339
|
+
setPendingSecret({ id: secret.id, name: secret.name || secret.id });
|
|
317
340
|
}
|
|
318
341
|
setShowSecretPicker(false);
|
|
342
|
+
// Return to the form at the attach button
|
|
343
|
+
setGatewayFormField("attach");
|
|
344
|
+
}, []);
|
|
345
|
+
// Attach the configured gateway to the devbox
|
|
346
|
+
const handleAttachGateway = React.useCallback(() => {
|
|
347
|
+
if (!pendingGateway || !pendingSecret || !gatewayEnvPrefix.trim())
|
|
348
|
+
return;
|
|
349
|
+
const newGateway = {
|
|
350
|
+
envPrefix: gatewayEnvPrefix.trim(),
|
|
351
|
+
gateway: pendingGateway.id,
|
|
352
|
+
gatewayName: pendingGateway.name,
|
|
353
|
+
gatewayEndpoint: pendingGateway.endpoint,
|
|
354
|
+
secret: pendingSecret.id,
|
|
355
|
+
secretName: pendingSecret.name,
|
|
356
|
+
};
|
|
357
|
+
setFormData((prev) => ({
|
|
358
|
+
...prev,
|
|
359
|
+
gateways: [...prev.gateways, newGateway],
|
|
360
|
+
}));
|
|
361
|
+
// Reset form
|
|
319
362
|
setPendingGateway(null);
|
|
363
|
+
setPendingSecret(null);
|
|
320
364
|
setGatewayEnvPrefix("");
|
|
321
|
-
|
|
365
|
+
setGatewayFormActive(false);
|
|
366
|
+
setGatewayFormField("attach");
|
|
322
367
|
setSelectedGatewayIndex(0);
|
|
323
|
-
}, [pendingGateway, gatewayEnvPrefix]);
|
|
368
|
+
}, [pendingGateway, pendingSecret, gatewayEnvPrefix]);
|
|
324
369
|
// Handle clearing source
|
|
325
370
|
const handleClearSource = React.useCallback(() => {
|
|
326
371
|
setFormData((prev) => ({ ...prev, blueprint_id: "", snapshot_id: "" }));
|
|
@@ -418,24 +463,70 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
418
463
|
}, { isActive: inMetadataSection });
|
|
419
464
|
// Gateway section input handler - active when in gateway section
|
|
420
465
|
useInput((input, key) => {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
466
|
+
// === Gateway attach form mode ===
|
|
467
|
+
if (gatewayFormActive) {
|
|
468
|
+
// envName field is a text input - only handle navigation keys
|
|
469
|
+
if (gatewayFormField === "envName") {
|
|
470
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
471
|
+
setGatewayFormField("gateway");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (key.downArrow || (key.tab && !key.shift)) {
|
|
475
|
+
setGatewayFormField("secret");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (key.escape) {
|
|
479
|
+
// Cancel the form
|
|
480
|
+
setPendingGateway(null);
|
|
481
|
+
setPendingSecret(null);
|
|
482
|
+
setGatewayEnvPrefix("");
|
|
483
|
+
setGatewayFormActive(false);
|
|
484
|
+
setGatewayFormField("attach");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Let TextInput handle other keys
|
|
429
488
|
return;
|
|
430
489
|
}
|
|
431
|
-
|
|
490
|
+
// Navigation between form fields
|
|
491
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
492
|
+
const prevIdx = Math.max(0, gatewayFormFieldIndex - 1);
|
|
493
|
+
setGatewayFormField(gatewayFormFields[prevIdx]);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (key.downArrow || (key.tab && !key.shift)) {
|
|
497
|
+
const nextIdx = Math.min(gatewayFormFields.length - 1, gatewayFormFieldIndex + 1);
|
|
498
|
+
setGatewayFormField(gatewayFormFields[nextIdx]);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
// Enter on specific fields
|
|
502
|
+
if (key.return) {
|
|
503
|
+
if (gatewayFormField === "gateway") {
|
|
504
|
+
setShowGatewayPicker(true);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (gatewayFormField === "secret") {
|
|
508
|
+
setShowSecretPicker(true);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (gatewayFormField === "attach") {
|
|
512
|
+
handleAttachGateway();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (key.escape || input === "q") {
|
|
517
|
+
// Cancel the form
|
|
518
|
+
setPendingGateway(null);
|
|
519
|
+
setPendingSecret(null);
|
|
432
520
|
setGatewayEnvPrefix("");
|
|
433
|
-
|
|
521
|
+
setGatewayFormActive(false);
|
|
522
|
+
setGatewayFormField("attach");
|
|
434
523
|
return;
|
|
435
524
|
}
|
|
436
525
|
return;
|
|
437
526
|
}
|
|
438
|
-
//
|
|
527
|
+
// === List navigation mode (existing gateways + attach/done) ===
|
|
528
|
+
const gatewayCount = formData.gateways.length;
|
|
529
|
+
const maxIndex = gatewayCount + 1; // Attach + existing items + Done
|
|
439
530
|
if (key.upArrow && selectedGatewayIndex > 0) {
|
|
440
531
|
setSelectedGatewayIndex(selectedGatewayIndex - 1);
|
|
441
532
|
}
|
|
@@ -444,22 +535,23 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
444
535
|
}
|
|
445
536
|
else if (key.return) {
|
|
446
537
|
if (selectedGatewayIndex === 0) {
|
|
447
|
-
//
|
|
538
|
+
// Open the attach form
|
|
539
|
+
setPendingGateway(null);
|
|
540
|
+
setPendingSecret(null);
|
|
448
541
|
setGatewayEnvPrefix("");
|
|
449
|
-
|
|
542
|
+
setGatewayFormActive(true);
|
|
543
|
+
setGatewayFormField("gateway");
|
|
450
544
|
}
|
|
451
545
|
else if (selectedGatewayIndex === maxIndex) {
|
|
452
546
|
// Done
|
|
453
547
|
setInGatewaySection(false);
|
|
454
548
|
setSelectedGatewayIndex(0);
|
|
455
|
-
setGatewayEnvPrefix("");
|
|
456
|
-
setGatewayInputMode(null);
|
|
457
549
|
}
|
|
458
550
|
}
|
|
459
551
|
else if ((input === "d" || key.delete) &&
|
|
460
552
|
selectedGatewayIndex >= 1 &&
|
|
461
553
|
selectedGatewayIndex <= gatewayCount) {
|
|
462
|
-
//
|
|
554
|
+
// Remove gateway at index
|
|
463
555
|
const indexToDelete = selectedGatewayIndex - 1;
|
|
464
556
|
const newGateways = [...formData.gateways];
|
|
465
557
|
newGateways.splice(indexToDelete, 1);
|
|
@@ -472,10 +564,14 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
472
564
|
else if (key.escape || input === "q") {
|
|
473
565
|
setInGatewaySection(false);
|
|
474
566
|
setSelectedGatewayIndex(0);
|
|
475
|
-
setGatewayEnvPrefix("");
|
|
476
|
-
setGatewayInputMode(null);
|
|
477
567
|
}
|
|
478
|
-
}, {
|
|
568
|
+
}, {
|
|
569
|
+
isActive: inGatewaySection &&
|
|
570
|
+
!showGatewayPicker &&
|
|
571
|
+
!showSecretPicker &&
|
|
572
|
+
!showInlineSecretCreate &&
|
|
573
|
+
!showInlineGatewayConfigCreate,
|
|
574
|
+
});
|
|
479
575
|
// Validate custom resource configuration
|
|
480
576
|
const validateCustomResources = () => {
|
|
481
577
|
if (formData.resource_size !== "CUSTOM_SIZE") {
|
|
@@ -564,6 +660,12 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
564
660
|
}
|
|
565
661
|
createParams.gateways = gateways;
|
|
566
662
|
}
|
|
663
|
+
// Add tunnel configuration if not "none"
|
|
664
|
+
if (formData.tunnel_auth_mode !== "none") {
|
|
665
|
+
createParams.tunnel = {
|
|
666
|
+
auth_mode: formData.tunnel_auth_mode,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
567
669
|
const devbox = await client.devboxes.create(createParams);
|
|
568
670
|
setResult(devbox);
|
|
569
671
|
}
|
|
@@ -698,15 +800,23 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
698
800
|
return "Deny All";
|
|
699
801
|
return `Custom (${egress.allowed_hostnames?.length || 0})`;
|
|
700
802
|
};
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
803
|
+
const buildNetworkPolicyColumns = (tw) => {
|
|
804
|
+
const fixedWidth = 6;
|
|
805
|
+
const idWidth = 25;
|
|
806
|
+
const egressWidth = 15;
|
|
807
|
+
const timeWidth = 18;
|
|
808
|
+
const baseWidth = fixedWidth + idWidth + egressWidth + timeWidth;
|
|
809
|
+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
|
|
810
|
+
return [
|
|
811
|
+
createTextColumn("id", "ID", (policy) => policy.id, {
|
|
812
|
+
width: idWidth + 1,
|
|
813
|
+
color: colors.idColor,
|
|
814
|
+
}),
|
|
815
|
+
createTextColumn("name", "Name", (policy) => policy.name || "", { width: nameWidth }),
|
|
816
|
+
createTextColumn("egress", "Egress", (policy) => getEgressLabel(policy.egress), { width: egressWidth }),
|
|
817
|
+
createTextColumn("created", "Created", (policy) => policy.create_time_ms ? formatTimeAgo(policy.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
|
|
818
|
+
];
|
|
819
|
+
};
|
|
710
820
|
return (_jsx(ResourcePicker, { config: {
|
|
711
821
|
title: "Select Network Policy",
|
|
712
822
|
fetchPage: async (params) => {
|
|
@@ -723,7 +833,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
723
833
|
},
|
|
724
834
|
getItemId: (policy) => policy.id,
|
|
725
835
|
getItemLabel: (policy) => policy.name || policy.id,
|
|
726
|
-
columns:
|
|
836
|
+
columns: buildNetworkPolicyColumns,
|
|
727
837
|
mode: "single",
|
|
728
838
|
emptyMessage: "No network policies found",
|
|
729
839
|
searchPlaceholder: "Search network policies...",
|
|
@@ -734,19 +844,57 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
734
844
|
],
|
|
735
845
|
}, onSelect: handleNetworkPolicySelect, onCancel: () => setShowNetworkPolicyPicker(false), initialSelected: formData.network_policy_id ? [formData.network_policy_id] : [] }));
|
|
736
846
|
}
|
|
847
|
+
// Inline gateway config creation screen (from gateway attach flow)
|
|
848
|
+
if (showInlineGatewayConfigCreate) {
|
|
849
|
+
return (_jsx(GatewayConfigCreatePage, { onBack: () => {
|
|
850
|
+
setShowInlineGatewayConfigCreate(false);
|
|
851
|
+
// Return to gateway picker
|
|
852
|
+
setShowGatewayPicker(true);
|
|
853
|
+
}, onCreate: (config) => {
|
|
854
|
+
setShowInlineGatewayConfigCreate(false);
|
|
855
|
+
// Auto-select the newly created gateway config
|
|
856
|
+
const configName = config.name || config.id;
|
|
857
|
+
setPendingGateway({
|
|
858
|
+
id: config.id,
|
|
859
|
+
name: configName,
|
|
860
|
+
endpoint: config.endpoint || "",
|
|
861
|
+
});
|
|
862
|
+
const autoEnvName = configName
|
|
863
|
+
.toUpperCase()
|
|
864
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
865
|
+
.replace(/^_|_$/g, "");
|
|
866
|
+
setGatewayEnvPrefix(autoEnvName);
|
|
867
|
+
setShowGatewayPicker(false);
|
|
868
|
+
setGatewayFormField("envName");
|
|
869
|
+
} }));
|
|
870
|
+
}
|
|
737
871
|
// Gateway config picker screen
|
|
738
872
|
if (showGatewayPicker) {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
873
|
+
const buildGatewayColumns = (tw) => {
|
|
874
|
+
const fixedWidth = 6;
|
|
875
|
+
const idWidth = 25;
|
|
876
|
+
const timeWidth = 20;
|
|
877
|
+
const showEndpoint = tw >= 100;
|
|
878
|
+
const endpointWidth = Math.max(20, tw >= 140 ? 40 : 25);
|
|
879
|
+
const baseWidth = fixedWidth + idWidth + timeWidth;
|
|
880
|
+
const optionalWidth = showEndpoint ? endpointWidth : 0;
|
|
881
|
+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth - optionalWidth));
|
|
882
|
+
return [
|
|
883
|
+
createTextColumn("id", "ID", (config) => config.id, {
|
|
884
|
+
width: idWidth + 1,
|
|
885
|
+
color: colors.idColor,
|
|
886
|
+
}),
|
|
887
|
+
createTextColumn("name", "Name", (config) => config.name || "", { width: nameWidth }),
|
|
888
|
+
...(showEndpoint
|
|
889
|
+
? [
|
|
890
|
+
createTextColumn("endpoint", "Endpoint", (config) => config.endpoint || "", { width: endpointWidth, color: colors.textDim }),
|
|
891
|
+
]
|
|
892
|
+
: []),
|
|
893
|
+
createTextColumn("created", "Created", (config) => config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
|
|
894
|
+
];
|
|
895
|
+
};
|
|
748
896
|
return (_jsx(ResourcePicker, { config: {
|
|
749
|
-
title: "Select Gateway Config",
|
|
897
|
+
title: "Select AI Gateway Config",
|
|
750
898
|
fetchPage: async (params) => {
|
|
751
899
|
const result = await listGatewayConfigs({
|
|
752
900
|
limit: params.limit,
|
|
@@ -761,66 +909,111 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
761
909
|
},
|
|
762
910
|
getItemId: (config) => config.id,
|
|
763
911
|
getItemLabel: (config) => config.name || config.id,
|
|
764
|
-
columns:
|
|
912
|
+
columns: buildGatewayColumns,
|
|
765
913
|
mode: "single",
|
|
766
|
-
emptyMessage: "No gateway configs found",
|
|
767
|
-
searchPlaceholder: "Search gateway configs...",
|
|
914
|
+
emptyMessage: "No AI gateway configs found",
|
|
915
|
+
searchPlaceholder: "Search AI gateway configs...",
|
|
768
916
|
breadcrumbItems: [
|
|
769
917
|
{ label: "Devboxes" },
|
|
770
918
|
{ label: "Create" },
|
|
771
|
-
{ label:
|
|
919
|
+
{ label: "Attach AI Gateway Config" },
|
|
920
|
+
{ label: "Select Config", active: true },
|
|
772
921
|
],
|
|
922
|
+
onCreateNew: () => {
|
|
923
|
+
setShowGatewayPicker(false);
|
|
924
|
+
setShowInlineGatewayConfigCreate(true);
|
|
925
|
+
},
|
|
926
|
+
createNewLabel: "Create AI gateway config",
|
|
773
927
|
}, onSelect: handleGatewaySelect, onCancel: () => {
|
|
774
928
|
setShowGatewayPicker(false);
|
|
775
|
-
|
|
776
|
-
|
|
929
|
+
// Return to the form at the gateway field
|
|
930
|
+
setGatewayFormField("gateway");
|
|
777
931
|
}, initialSelected: [] }, "gateway-config-picker"));
|
|
778
932
|
}
|
|
933
|
+
// Inline secret creation screen (from gateway flow)
|
|
934
|
+
if (showInlineSecretCreate) {
|
|
935
|
+
return (_jsx(SecretCreatePage, { onBack: () => {
|
|
936
|
+
setShowInlineSecretCreate(false);
|
|
937
|
+
// Return to secret picker
|
|
938
|
+
setShowSecretPicker(true);
|
|
939
|
+
}, onCreate: (secret) => {
|
|
940
|
+
setShowInlineSecretCreate(false);
|
|
941
|
+
// Store as pending secret and return to the attach form
|
|
942
|
+
setPendingSecret({
|
|
943
|
+
id: secret.id,
|
|
944
|
+
name: secret.name || secret.id,
|
|
945
|
+
});
|
|
946
|
+
setShowSecretPicker(false);
|
|
947
|
+
setGatewayFormField("attach");
|
|
948
|
+
} }));
|
|
949
|
+
}
|
|
779
950
|
// Secret picker screen (for gateway)
|
|
780
951
|
if (showSecretPicker) {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
952
|
+
const buildSecretColumns = (tw) => {
|
|
953
|
+
const fixedWidth = 6;
|
|
954
|
+
const idWidth = 30;
|
|
955
|
+
const timeWidth = 20;
|
|
956
|
+
const baseWidth = fixedWidth + idWidth + timeWidth;
|
|
957
|
+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
|
|
958
|
+
return [
|
|
959
|
+
createTextColumn("id", "ID", (secret) => secret.id, {
|
|
960
|
+
width: idWidth + 1,
|
|
961
|
+
color: colors.idColor,
|
|
962
|
+
}),
|
|
963
|
+
createTextColumn("name", "Name", (secret) => secret.name || "", { width: nameWidth }),
|
|
964
|
+
createTextColumn("created", "Created", (secret) => secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
|
|
965
|
+
];
|
|
966
|
+
};
|
|
789
967
|
return (_jsx(ResourcePicker, { config: {
|
|
790
968
|
title: "Select Secret for Gateway",
|
|
791
969
|
fetchPage: async (params) => {
|
|
792
970
|
const client = getClient();
|
|
793
|
-
// Secrets API doesn't support cursor pagination,
|
|
971
|
+
// Secrets API doesn't support cursor pagination, so we fetch all
|
|
972
|
+
// and do client-side pagination by slicing to the requested page
|
|
794
973
|
const page = await client.secrets.list({
|
|
795
|
-
limit:
|
|
974
|
+
limit: 1000,
|
|
796
975
|
});
|
|
976
|
+
const allSecrets = (page.secrets || []).map((s) => ({
|
|
977
|
+
id: s.id,
|
|
978
|
+
name: s.name,
|
|
979
|
+
create_time_ms: s.create_time_ms,
|
|
980
|
+
}));
|
|
981
|
+
// Client-side cursor pagination
|
|
982
|
+
let startIdx = 0;
|
|
983
|
+
if (params.startingAt) {
|
|
984
|
+
const cursorIdx = allSecrets.findIndex((s) => s.id === params.startingAt);
|
|
985
|
+
if (cursorIdx >= 0) {
|
|
986
|
+
startIdx = cursorIdx + 1;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const sliced = allSecrets.slice(startIdx, startIdx + params.limit);
|
|
797
990
|
return {
|
|
798
|
-
items:
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
create_time_ms: s.create_time_ms,
|
|
802
|
-
})),
|
|
803
|
-
hasMore: false, // Secrets API doesn't support pagination
|
|
804
|
-
totalCount: page.total_count || 0,
|
|
991
|
+
items: sliced,
|
|
992
|
+
hasMore: startIdx + params.limit < allSecrets.length,
|
|
993
|
+
totalCount: allSecrets.length,
|
|
805
994
|
};
|
|
806
995
|
},
|
|
807
996
|
getItemId: (secret) => secret.id,
|
|
808
997
|
getItemLabel: (secret) => secret.name || secret.id,
|
|
809
|
-
columns:
|
|
998
|
+
columns: buildSecretColumns,
|
|
810
999
|
mode: "single",
|
|
811
1000
|
emptyMessage: "No secrets found",
|
|
812
1001
|
searchPlaceholder: "Search secrets...",
|
|
813
1002
|
breadcrumbItems: [
|
|
814
1003
|
{ label: "Devboxes" },
|
|
815
1004
|
{ label: "Create" },
|
|
816
|
-
{ label:
|
|
1005
|
+
{ label: "Attach AI Gateway Config" },
|
|
817
1006
|
{ label: "Select Secret", active: true },
|
|
818
1007
|
],
|
|
1008
|
+
onCreateNew: () => {
|
|
1009
|
+
setShowSecretPicker(false);
|
|
1010
|
+
setShowInlineSecretCreate(true);
|
|
1011
|
+
},
|
|
1012
|
+
createNewLabel: "Create secret",
|
|
819
1013
|
}, onSelect: handleSecretSelect, onCancel: () => {
|
|
820
1014
|
setShowSecretPicker(false);
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
setGatewayInputMode(null);
|
|
1015
|
+
// Return to the form at the secret field
|
|
1016
|
+
setGatewayFormField("secret");
|
|
824
1017
|
}, initialSelected: [] }, "secret-picker"));
|
|
825
1018
|
}
|
|
826
1019
|
// Form screen
|
|
@@ -835,7 +1028,20 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
835
1028
|
}
|
|
836
1029
|
if (field.type === "select") {
|
|
837
1030
|
const value = fieldData;
|
|
838
|
-
|
|
1031
|
+
let options;
|
|
1032
|
+
if (field.key === "architecture") {
|
|
1033
|
+
options = architectures;
|
|
1034
|
+
}
|
|
1035
|
+
else if (field.key === "resource_size") {
|
|
1036
|
+
options = resourceSizes;
|
|
1037
|
+
}
|
|
1038
|
+
else if (field.key === "tunnel_auth_mode") {
|
|
1039
|
+
options = tunnelAuthModes;
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
options = [];
|
|
1043
|
+
}
|
|
1044
|
+
return (_jsx(FormSelect, { label: field.label, value: value || "", options: options, onChange: (newValue) => setFormData({ ...formData, [field.key]: newValue }), isActive: isActive }, field.key));
|
|
839
1045
|
}
|
|
840
1046
|
if (field.type === "source") {
|
|
841
1047
|
// Check if either blueprint or snapshot is selected
|
|
@@ -902,34 +1108,59 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
902
1108
|
if (field.type === "gateways") {
|
|
903
1109
|
if (!inGatewaySection) {
|
|
904
1110
|
// 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, "
|
|
1111
|
+
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, " configured"] }), 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, " ENV: ", gw.envPrefix, " | Config:", " ", gw.gatewayName, " (", gw.gatewayEndpoint, ") | Secret:", " ", gw.secretName] }, idx))) }))] }, field.key));
|
|
906
1112
|
}
|
|
907
1113
|
// Expanded gateway section view
|
|
908
1114
|
const gatewayCount = formData.gateways.length;
|
|
909
1115
|
const maxGatewayIndex = gatewayCount + 1;
|
|
910
|
-
|
|
1116
|
+
const canAttach = !!pendingGateway && !!pendingSecret && !!gatewayEnvPrefix.trim();
|
|
1117
|
+
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, " Configure AI Gateway Configs for Devbox"] }), gatewayFormActive && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: colors.success, paddingX: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Attach AI Gateway Config" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: gatewayFormField === "attach"
|
|
1118
|
+
? canAttach
|
|
1119
|
+
? colors.success
|
|
1120
|
+
: colors.primary
|
|
1121
|
+
: colors.textDim, children: [gatewayFormField === "attach"
|
|
1122
|
+
? figures.pointer
|
|
1123
|
+
: " ", " "] }), _jsx(Text, { color: gatewayFormField === "attach"
|
|
1124
|
+
? canAttach
|
|
1125
|
+
? colors.success
|
|
1126
|
+
: colors.primary
|
|
1127
|
+
: colors.textDim, bold: gatewayFormField === "attach", children: canAttach
|
|
1128
|
+
? `${figures.tick} Attach Gateway`
|
|
1129
|
+
: `${figures.ellipsis} Attach Gateway (fill fields below)` })] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: gatewayFormField === "gateway"
|
|
1130
|
+
? colors.primary
|
|
1131
|
+
: colors.textDim, children: [gatewayFormField === "gateway" ? figures.pointer : " ", " ", "AI Gateway Config:", " "] }), pendingGateway ? (_jsxs(Text, { color: colors.success, children: [pendingGateway.name, " ", _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", pendingGateway.endpoint, ")"] })] })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: "(none selected)" })), gatewayFormField === "gateway" && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: gatewayFormField === "envName"
|
|
1132
|
+
? colors.primary
|
|
1133
|
+
: colors.textDim, children: [gatewayFormField === "envName" ? figures.pointer : " ", " ", "ENV Name:", " "] }), gatewayFormField === "envName" ? (_jsx(TextInput, { value: gatewayEnvPrefix || "", onChange: setGatewayEnvPrefix, placeholder: "ANTHROPIC" })) : (_jsx(Text, { color: gatewayEnvPrefix ? colors.text : colors.textDim, dimColor: !gatewayEnvPrefix, children: gatewayEnvPrefix || "(auto-filled from config)" }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: gatewayFormField === "secret"
|
|
1134
|
+
? colors.primary
|
|
1135
|
+
: colors.textDim, children: [gatewayFormField === "secret" ? figures.pointer : " ", " ", "Secret:", " "] }), pendingSecret ? (_jsx(Text, { color: colors.success, children: pendingSecret.name })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: "(none selected)" })), gatewayFormField === "secret" && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: gatewayFormField === "envName"
|
|
1136
|
+
? `Type to edit • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
|
|
1137
|
+
: gatewayFormField === "attach"
|
|
1138
|
+
? canAttach
|
|
1139
|
+
? `[Enter] Attach • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
|
|
1140
|
+
: `${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
|
|
1141
|
+
: `[Enter] Select • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel` }) })] })), !gatewayFormActive && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === 0
|
|
911
1142
|
? colors.primary
|
|
912
1143
|
: colors.textDim, children: [selectedGatewayIndex === 0
|
|
913
1144
|
? figures.pointer
|
|
914
1145
|
: " ", " "] }), _jsx(Text, { color: selectedGatewayIndex === 0
|
|
915
1146
|
? colors.success
|
|
916
|
-
: colors.textDim, bold: selectedGatewayIndex === 0, children: "+
|
|
1147
|
+
: colors.textDim, bold: selectedGatewayIndex === 0, children: "+ Attach AI gateway config" })] }), gatewayCount > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: formData.gateways.map((gw, index) => {
|
|
917
1148
|
const itemIndex = index + 1;
|
|
918
1149
|
const isGatewaySelected = selectedGatewayIndex === itemIndex;
|
|
919
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: isGatewaySelected
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1150
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: isGatewaySelected
|
|
1151
|
+
? colors.primary
|
|
1152
|
+
: colors.textDim, children: [isGatewaySelected
|
|
1153
|
+
? figures.pointer
|
|
1154
|
+
: " ", " "] }), _jsxs(Text, { color: isGatewaySelected
|
|
1155
|
+
? colors.primary
|
|
1156
|
+
: colors.textDim, bold: isGatewaySelected, children: ["ENV: ", gw.envPrefix] })] }), _jsxs(Box, { marginLeft: 3, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Gateway Config: ", gw.gatewayName, " (", gw.gateway, ")"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Endpoint: ", gw.gatewayEndpoint] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Secret: ", gw.secretName, " (", gw.secret, ")"] })] })] }, gw.envPrefix));
|
|
924
1157
|
}) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
|
|
925
1158
|
? colors.primary
|
|
926
1159
|
: colors.textDim, children: [selectedGatewayIndex === maxGatewayIndex
|
|
927
1160
|
? figures.pointer
|
|
928
1161
|
: " ", " "] }), _jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
|
|
929
1162
|
? colors.success
|
|
930
|
-
: colors.textDim, bold: selectedGatewayIndex === maxGatewayIndex, children: [figures.tick, " Done"] })] })
|
|
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));
|
|
1163
|
+
: 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: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedGatewayIndex === 0 ? "Attach" : selectedGatewayIndex === maxGatewayIndex ? "Done" : "Select"} • [d] Remove • [esc] Back` }) })] }))] }, field.key));
|
|
933
1164
|
}
|
|
934
1165
|
return null;
|
|
935
1166
|
}) }), formData.resource_size === "CUSTOM_SIZE" &&
|