@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.
- package/README.md +19 -5
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +125 -109
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +44 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +15 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +60 -0
- package/dist/commands/menu.js +2 -1
- package/dist/commands/secret/list.js +379 -4
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +108 -0
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +9 -61
- package/dist/components/DevboxCreatePage.js +531 -14
- package/dist/components/DevboxDetailPage.js +27 -22
- package/dist/components/GatewayConfigCreatePage.js +265 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/MainMenu.js +63 -22
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +220 -0
- package/dist/components/SecretCreatePage.js +183 -0
- package/dist/components/SettingsMenu.js +95 -0
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +80 -0
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +99 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +29 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
- package/dist/screens/BenchmarkRunListScreen.js +275 -0
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/MenuScreen.js +5 -2
- package/dist/screens/ScenarioRunDetailScreen.js +226 -0
- package/dist/screens/ScenarioRunListScreen.js +245 -0
- package/dist/screens/SecretCreateScreen.js +7 -0
- package/dist/screens/SecretDetailScreen.js +198 -0
- package/dist/screens/SecretListScreen.js +7 -0
- package/dist/screens/SettingsMenuScreen.js +26 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +120 -0
- package/dist/services/gatewayConfigService.js +114 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +183 -0
- package/dist/store/betaFeatureStore.js +47 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/store/index.js +1 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +80 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/time.js +121 -0
- package/package.json +42 -43
|
@@ -12,26 +12,7 @@ import { ResourceDetailPage, } from "./ResourceDetailPage.js";
|
|
|
12
12
|
import { getDevboxUrl } from "../utils/url.js";
|
|
13
13
|
import { colors } from "../utils/theme.js";
|
|
14
14
|
import { getDevbox } from "../services/devboxService.js";
|
|
15
|
-
|
|
16
|
-
const formatTimeAgo = (timestamp) => {
|
|
17
|
-
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
18
|
-
if (seconds < 60)
|
|
19
|
-
return `${seconds}s ago`;
|
|
20
|
-
const minutes = Math.floor(seconds / 60);
|
|
21
|
-
if (minutes < 60)
|
|
22
|
-
return `${minutes}m ago`;
|
|
23
|
-
const hours = Math.floor(minutes / 60);
|
|
24
|
-
if (hours < 24)
|
|
25
|
-
return `${hours}h ago`;
|
|
26
|
-
const days = Math.floor(hours / 24);
|
|
27
|
-
if (days < 30)
|
|
28
|
-
return `${days}d ago`;
|
|
29
|
-
const months = Math.floor(days / 30);
|
|
30
|
-
if (months < 12)
|
|
31
|
-
return `${months}mo ago`;
|
|
32
|
-
const years = Math.floor(months / 12);
|
|
33
|
-
return `${years}y ago`;
|
|
34
|
-
};
|
|
15
|
+
import { formatTimeAgo } from "../utils/time.js";
|
|
35
16
|
export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
36
17
|
const [showActions, setShowActions] = React.useState(false);
|
|
37
18
|
const [selectedOperationKey, setSelectedOperationKey] = React.useState(null);
|
|
@@ -210,10 +191,28 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
210
191
|
});
|
|
211
192
|
}
|
|
212
193
|
// Source
|
|
213
|
-
if (devbox.blueprint_id
|
|
194
|
+
if (devbox.blueprint_id) {
|
|
195
|
+
detailFields.push({
|
|
196
|
+
label: "Source",
|
|
197
|
+
value: _jsx(Text, { color: colors.success, children: devbox.blueprint_id }),
|
|
198
|
+
action: {
|
|
199
|
+
type: "navigate",
|
|
200
|
+
screen: "blueprint-detail",
|
|
201
|
+
params: { blueprintId: devbox.blueprint_id },
|
|
202
|
+
hint: "View Blueprint",
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else if (devbox.snapshot_id) {
|
|
214
207
|
detailFields.push({
|
|
215
208
|
label: "Source",
|
|
216
|
-
value:
|
|
209
|
+
value: _jsx(Text, { color: colors.success, children: devbox.snapshot_id }),
|
|
210
|
+
action: {
|
|
211
|
+
type: "navigate",
|
|
212
|
+
screen: "snapshot-detail",
|
|
213
|
+
params: { snapshotId: devbox.snapshot_id },
|
|
214
|
+
hint: "View Snapshot",
|
|
215
|
+
},
|
|
217
216
|
});
|
|
218
217
|
}
|
|
219
218
|
// Network Policy
|
|
@@ -221,6 +220,12 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
221
220
|
detailFields.push({
|
|
222
221
|
label: "Network Policy",
|
|
223
222
|
value: _jsx(Text, { color: colors.info, children: lp.network_policy_id }),
|
|
223
|
+
action: {
|
|
224
|
+
type: "navigate",
|
|
225
|
+
screen: "network-policy-detail",
|
|
226
|
+
params: { networkPolicyId: lp.network_policy_id },
|
|
227
|
+
hint: "View Policy",
|
|
228
|
+
},
|
|
224
229
|
});
|
|
225
230
|
}
|
|
226
231
|
// Initiator
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { getClient } from "../utils/client.js";
|
|
6
|
+
import { SpinnerComponent } from "./Spinner.js";
|
|
7
|
+
import { ErrorMessage } from "./ErrorMessage.js";
|
|
8
|
+
import { SuccessMessage } from "./SuccessMessage.js";
|
|
9
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
10
|
+
import { NavigationTips } from "./NavigationTips.js";
|
|
11
|
+
import { FormTextInput, FormSelect, FormActionButton, useFormSelectNavigation, } from "./form/index.js";
|
|
12
|
+
import { colors } from "../utils/theme.js";
|
|
13
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
14
|
+
const authTypes = ["bearer", "header"];
|
|
15
|
+
export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) => {
|
|
16
|
+
const isEditing = !!initialConfig?.id;
|
|
17
|
+
const [currentField, setCurrentField] = React.useState("create");
|
|
18
|
+
// Normalize auth type from API to match our options (lowercase)
|
|
19
|
+
const normalizeAuthType = (type) => {
|
|
20
|
+
const normalized = (type || "").toLowerCase();
|
|
21
|
+
if (normalized === "header" || normalized === "bearer") {
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
return "bearer"; // default
|
|
25
|
+
};
|
|
26
|
+
const [formData, setFormData] = React.useState({
|
|
27
|
+
name: initialConfig?.name || "",
|
|
28
|
+
endpoint: initialConfig?.endpoint || "",
|
|
29
|
+
auth_type: normalizeAuthType(initialConfig?.auth_mechanism?.type),
|
|
30
|
+
auth_key: initialConfig?.auth_mechanism?.key || "",
|
|
31
|
+
description: initialConfig?.description || "",
|
|
32
|
+
});
|
|
33
|
+
const [creating, setCreating] = React.useState(false);
|
|
34
|
+
const [result, setResult] = React.useState(null);
|
|
35
|
+
const [error, setError] = React.useState(null);
|
|
36
|
+
const fields = [
|
|
37
|
+
{
|
|
38
|
+
key: "create",
|
|
39
|
+
label: isEditing ? "Update Gateway Config" : "Create Gateway Config",
|
|
40
|
+
type: "action",
|
|
41
|
+
},
|
|
42
|
+
{ key: "name", label: "Name", type: "text", placeholder: "my-gateway" },
|
|
43
|
+
{
|
|
44
|
+
key: "endpoint",
|
|
45
|
+
label: "Endpoint URL",
|
|
46
|
+
type: "text",
|
|
47
|
+
placeholder: "https://api.example.com",
|
|
48
|
+
},
|
|
49
|
+
{ key: "auth_type", label: "Auth Type", type: "select" },
|
|
50
|
+
{
|
|
51
|
+
key: "auth_key",
|
|
52
|
+
label: "Auth Header Key (for header type)",
|
|
53
|
+
type: "text",
|
|
54
|
+
placeholder: "x-api-key",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "description",
|
|
58
|
+
label: "Description (optional)",
|
|
59
|
+
type: "text",
|
|
60
|
+
placeholder: "Gateway for...",
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
|
|
64
|
+
// Handle Ctrl+C to exit
|
|
65
|
+
useExitOnCtrlC();
|
|
66
|
+
// Select navigation handlers using shared hook
|
|
67
|
+
const handleAuthTypeNav = useFormSelectNavigation(formData.auth_type, authTypes, (value) => {
|
|
68
|
+
setFormData({
|
|
69
|
+
...formData,
|
|
70
|
+
auth_type: value,
|
|
71
|
+
// Clear auth_key if switching from header to bearer
|
|
72
|
+
auth_key: value !== "header" ? "" : formData.auth_key,
|
|
73
|
+
});
|
|
74
|
+
// If switching away from header and currently on auth_key field, move to next field
|
|
75
|
+
if (value !== "header" && currentField === "auth_key") {
|
|
76
|
+
setCurrentField("description");
|
|
77
|
+
}
|
|
78
|
+
}, currentField === "auth_type");
|
|
79
|
+
// Main form input handler
|
|
80
|
+
useInput((input, key) => {
|
|
81
|
+
// Handle result screen
|
|
82
|
+
if (result) {
|
|
83
|
+
if (input === "q" || key.escape || key.return) {
|
|
84
|
+
if (onCreate) {
|
|
85
|
+
onCreate(result);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
onBack();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Handle error screen
|
|
94
|
+
if (error) {
|
|
95
|
+
if (input === "r" || key.return) {
|
|
96
|
+
// Retry - clear error and return to form
|
|
97
|
+
setError(null);
|
|
98
|
+
}
|
|
99
|
+
else if (input === "q" || key.escape) {
|
|
100
|
+
// Quit - go back to list
|
|
101
|
+
onBack();
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Handle creating state
|
|
106
|
+
if (creating) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Back to list
|
|
110
|
+
if (input === "q" || key.escape) {
|
|
111
|
+
onBack();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Submit form with Ctrl+S
|
|
115
|
+
if (input === "s" && key.ctrl) {
|
|
116
|
+
handleCreate();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Handle Enter on any field to submit
|
|
120
|
+
if (key.return) {
|
|
121
|
+
handleCreate();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Handle select field navigation using shared hooks
|
|
125
|
+
if (handleAuthTypeNav(input, key))
|
|
126
|
+
return;
|
|
127
|
+
// Navigation (up/down arrows and tab/shift+tab)
|
|
128
|
+
// Skip auth_key field if auth_type is not "header"
|
|
129
|
+
const getNextField = (direction) => {
|
|
130
|
+
let nextIndex = direction === "up" ? currentFieldIndex - 1 : currentFieldIndex + 1;
|
|
131
|
+
while (nextIndex >= 0 && nextIndex < fields.length) {
|
|
132
|
+
const nextField = fields[nextIndex].key;
|
|
133
|
+
// Skip auth_key if auth_type is not header
|
|
134
|
+
if (nextField === "auth_key" && formData.auth_type !== "header") {
|
|
135
|
+
nextIndex = direction === "up" ? nextIndex - 1 : nextIndex + 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
return nextField;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
|
|
143
|
+
const nextField = getNextField("up");
|
|
144
|
+
if (nextField) {
|
|
145
|
+
setCurrentField(nextField);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if ((key.downArrow || (key.tab && !key.shift)) &&
|
|
150
|
+
currentFieldIndex < fields.length - 1) {
|
|
151
|
+
const nextField = getNextField("down");
|
|
152
|
+
if (nextField) {
|
|
153
|
+
setCurrentField(nextField);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}, { isActive: true });
|
|
158
|
+
const handleCreate = async () => {
|
|
159
|
+
// Validate required fields
|
|
160
|
+
if (!formData.name.trim()) {
|
|
161
|
+
setError(new Error("Name is required"));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!formData.endpoint.trim()) {
|
|
165
|
+
setError(new Error("Endpoint URL is required"));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (formData.auth_type === "header" && !formData.auth_key.trim()) {
|
|
169
|
+
setError(new Error("Auth header key is required for header auth type"));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
setCreating(true);
|
|
173
|
+
setError(null);
|
|
174
|
+
try {
|
|
175
|
+
const client = getClient();
|
|
176
|
+
const authMechanism = {
|
|
177
|
+
type: formData.auth_type,
|
|
178
|
+
};
|
|
179
|
+
if (formData.auth_type === "header" && formData.auth_key.trim()) {
|
|
180
|
+
authMechanism.key = formData.auth_key.trim();
|
|
181
|
+
}
|
|
182
|
+
let config;
|
|
183
|
+
if (isEditing && initialConfig?.id) {
|
|
184
|
+
// Update existing config
|
|
185
|
+
config = await client.gatewayConfigs.update(initialConfig.id, {
|
|
186
|
+
name: formData.name.trim(),
|
|
187
|
+
endpoint: formData.endpoint.trim(),
|
|
188
|
+
auth_mechanism: authMechanism,
|
|
189
|
+
description: formData.description.trim() || undefined,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Create new config
|
|
194
|
+
config = await client.gatewayConfigs.create({
|
|
195
|
+
name: formData.name.trim(),
|
|
196
|
+
endpoint: formData.endpoint.trim(),
|
|
197
|
+
auth_mechanism: authMechanism,
|
|
198
|
+
description: formData.description.trim() || undefined,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
setResult(config);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
setError(err);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
setCreating(false);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
// Result screen
|
|
211
|
+
if (result) {
|
|
212
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
213
|
+
{ label: "Gateway Configs" },
|
|
214
|
+
{ label: isEditing ? "Update" : "Create", active: true },
|
|
215
|
+
] }), _jsx(SuccessMessage, { message: `Gateway config ${isEditing ? "updated" : "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: ["Endpoint: ", result.endpoint] }) })] }), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Return to list" }] })] }));
|
|
216
|
+
}
|
|
217
|
+
// Error screen
|
|
218
|
+
if (error) {
|
|
219
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
220
|
+
{ label: "Gateway Configs" },
|
|
221
|
+
{ label: isEditing ? "Update" : "Create", active: true },
|
|
222
|
+
] }), _jsx(ErrorMessage, { message: `Failed to ${isEditing ? "update" : "create"} gateway config`, error: error }), _jsx(NavigationTips, { tips: [
|
|
223
|
+
{ key: "Enter/r", label: "Retry" },
|
|
224
|
+
{ key: "q/esc", label: "Cancel" },
|
|
225
|
+
] })] }));
|
|
226
|
+
}
|
|
227
|
+
// Creating screen
|
|
228
|
+
if (creating) {
|
|
229
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
230
|
+
{ label: "Gateway Configs" },
|
|
231
|
+
{ label: isEditing ? "Update" : "Create", active: true },
|
|
232
|
+
] }), _jsx(SpinnerComponent, { message: `${isEditing ? "Updating" : "Creating"} gateway config...` })] }));
|
|
233
|
+
}
|
|
234
|
+
// Form screen
|
|
235
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
236
|
+
{ label: "Gateway Configs" },
|
|
237
|
+
{ label: isEditing ? "Update" : "Create", active: true },
|
|
238
|
+
] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
|
|
239
|
+
const isActive = currentField === field.key;
|
|
240
|
+
const fieldData = formData[field.key];
|
|
241
|
+
if (field.type === "action") {
|
|
242
|
+
return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: `[Enter to ${isEditing ? "update" : "create"}]` }, field.key));
|
|
243
|
+
}
|
|
244
|
+
if (field.type === "text") {
|
|
245
|
+
// Skip auth_key field if auth type is bearer
|
|
246
|
+
if (field.key === "auth_key" && formData.auth_type !== "header") {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
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));
|
|
250
|
+
}
|
|
251
|
+
if (field.type === "select") {
|
|
252
|
+
const value = fieldData;
|
|
253
|
+
return (_jsx(FormSelect, { label: field.label, value: value || "", options: authTypes, onChange: (newValue) => setFormData({
|
|
254
|
+
...formData,
|
|
255
|
+
[field.key]: newValue,
|
|
256
|
+
// Clear auth_key if switching from header to bearer
|
|
257
|
+
auth_key: newValue !== "header" ? "" : formData.auth_key,
|
|
258
|
+
}), isActive: isActive }, field.key));
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}) }), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.info, " Auth Types:"] }) }), _jsxs(Box, { marginLeft: 4, flexDirection: "column", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: "\u2022 bearer: Uses Bearer token authentication" }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "\u2022 header: Uses custom header (specify header key)" })] }), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
262
|
+
{ key: "Enter", label: isEditing ? "Update" : "Create" },
|
|
263
|
+
{ key: "q", label: "Cancel" },
|
|
264
|
+
] })] }));
|
|
265
|
+
};
|
|
@@ -9,6 +9,7 @@ import figures from "figures";
|
|
|
9
9
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
10
10
|
import { NavigationTips } from "./NavigationTips.js";
|
|
11
11
|
import { colors } from "../utils/theme.js";
|
|
12
|
+
import { copyToClipboard } from "../utils/clipboard.js";
|
|
12
13
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
13
14
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
14
15
|
import { parseAnyLogEntry } from "../utils/logFormatter.js";
|
|
@@ -71,42 +72,10 @@ export const LogsViewer = ({ logs, breadcrumbItems = [{ label: "Logs", active: t
|
|
|
71
72
|
return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
|
|
72
73
|
})
|
|
73
74
|
.join("\n");
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
let args;
|
|
79
|
-
if (platform === "darwin") {
|
|
80
|
-
command = "pbcopy";
|
|
81
|
-
args = [];
|
|
82
|
-
}
|
|
83
|
-
else if (platform === "win32") {
|
|
84
|
-
command = "clip";
|
|
85
|
-
args = [];
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
command = "xclip";
|
|
89
|
-
args = ["-selection", "clipboard"];
|
|
90
|
-
}
|
|
91
|
-
const proc = spawn(command, args);
|
|
92
|
-
proc.stdin.write(text);
|
|
93
|
-
proc.stdin.end();
|
|
94
|
-
proc.on("exit", (code) => {
|
|
95
|
-
if (code === 0) {
|
|
96
|
-
setCopyStatus("Copied to clipboard!");
|
|
97
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
setCopyStatus("Failed to copy");
|
|
101
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
proc.on("error", () => {
|
|
105
|
-
setCopyStatus("Copy not supported");
|
|
106
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
107
|
-
});
|
|
108
|
-
};
|
|
109
|
-
copyToClipboard(logsText);
|
|
75
|
+
copyToClipboard(logsText).then((status) => {
|
|
76
|
+
setCopyStatus(status);
|
|
77
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
78
|
+
});
|
|
110
79
|
}
|
|
111
80
|
else if (input === "q" || key.escape || key.return) {
|
|
112
81
|
onBack();
|
|
@@ -121,9 +90,7 @@ export const LogsViewer = ({ logs, breadcrumbItems = [{ label: "Logs", active: t
|
|
|
121
90
|
// Helper to sanitize log message
|
|
122
91
|
const sanitizeMessage = (message) => {
|
|
123
92
|
// Strip ANSI escape sequences (colors, cursor movement, etc.)
|
|
124
|
-
const strippedAnsi = message.replace(
|
|
125
|
-
// eslint-disable-next-line no-control-regex
|
|
126
|
-
/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
93
|
+
const strippedAnsi = message.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
127
94
|
// Replace control characters with spaces
|
|
128
95
|
return (strippedAnsi
|
|
129
96
|
.replace(/\r\n/g, " ")
|
|
@@ -131,7 +98,6 @@ export const LogsViewer = ({ logs, breadcrumbItems = [{ label: "Logs", active: t
|
|
|
131
98
|
.replace(/\r/g, " ")
|
|
132
99
|
.replace(/\t/g, " ")
|
|
133
100
|
// Remove any other control characters (ASCII 0-31 except space)
|
|
134
|
-
// eslint-disable-next-line no-control-regex
|
|
135
101
|
.replace(/[\x00-\x1F]/g, ""));
|
|
136
102
|
};
|
|
137
103
|
// Helper to calculate how many lines a log entry will take when wrapped
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useInput, useApp, useStdout } from "ink";
|
|
4
4
|
import figures from "figures";
|
|
@@ -10,7 +10,8 @@ import { colors } from "../utils/theme.js";
|
|
|
10
10
|
import { execCommand } from "../utils/exec.js";
|
|
11
11
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
12
12
|
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
|
|
13
|
-
|
|
13
|
+
import { useBetaFeatures } from "../store/betaFeatureStore.js";
|
|
14
|
+
const allMenuItems = [
|
|
14
15
|
{
|
|
15
16
|
key: "devboxes",
|
|
16
17
|
label: "Devboxes",
|
|
@@ -40,10 +41,18 @@ const menuItems = [
|
|
|
40
41
|
color: colors.secondary,
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
|
-
key: "
|
|
44
|
-
label: "
|
|
45
|
-
description: "
|
|
46
|
-
icon: "
|
|
44
|
+
key: "benchmarks",
|
|
45
|
+
label: "Benchmarks",
|
|
46
|
+
description: "Performance testing and evaluation",
|
|
47
|
+
icon: "▶",
|
|
48
|
+
color: colors.success,
|
|
49
|
+
betaFeature: "benchmarks",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "settings",
|
|
53
|
+
label: "Settings",
|
|
54
|
+
description: "Network policies, secrets, and more",
|
|
55
|
+
icon: "⚙",
|
|
47
56
|
color: colors.info,
|
|
48
57
|
},
|
|
49
58
|
];
|
|
@@ -56,10 +65,22 @@ function getLayoutMode(height) {
|
|
|
56
65
|
return "compact"; // No banner + simple items + short descriptions
|
|
57
66
|
return "minimal"; // No banner + labels only
|
|
58
67
|
}
|
|
68
|
+
// Helper component for rendering beta badge
|
|
69
|
+
const BetaBadge = () => (_jsxs(Text, { color: colors.warning, bold: true, children: [" ", "[BETA]"] }));
|
|
59
70
|
export const MainMenu = ({ onSelect }) => {
|
|
60
71
|
const { exit } = useApp();
|
|
61
72
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
62
73
|
const { stdout } = useStdout();
|
|
74
|
+
const { isFeatureEnabled } = useBetaFeatures();
|
|
75
|
+
// Filter menu items based on beta feature flags
|
|
76
|
+
const menuItems = React.useMemo(() => {
|
|
77
|
+
return allMenuItems.filter((item) => {
|
|
78
|
+
if (item.betaFeature) {
|
|
79
|
+
return isFeatureEnabled(item.betaFeature);
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}, [isFeatureEnabled]);
|
|
63
84
|
// Get raw terminal dimensions, responding to resize events
|
|
64
85
|
// Default to 20 rows / 80 cols if we can't detect
|
|
65
86
|
const getTerminalDimensions = React.useCallback(() => {
|
|
@@ -89,6 +110,19 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
89
110
|
const { updateAvailable } = useUpdateCheck();
|
|
90
111
|
// Handle Ctrl+C to exit
|
|
91
112
|
useExitOnCtrlC();
|
|
113
|
+
// Helper to select menu item by key (if available in filtered list)
|
|
114
|
+
const selectByKey = React.useCallback((key) => {
|
|
115
|
+
if (menuItems.some((item) => item.key === key)) {
|
|
116
|
+
onSelect(key);
|
|
117
|
+
}
|
|
118
|
+
}, [menuItems, onSelect]);
|
|
119
|
+
// Helper to select menu item by number (1-indexed, based on filtered list)
|
|
120
|
+
const selectByNumber = React.useCallback((num) => {
|
|
121
|
+
const index = num - 1;
|
|
122
|
+
if (index >= 0 && index < menuItems.length) {
|
|
123
|
+
onSelect(menuItems[index].key);
|
|
124
|
+
}
|
|
125
|
+
}, [menuItems, onSelect]);
|
|
92
126
|
useInput((input, key) => {
|
|
93
127
|
if (key.upArrow && selectedIndex > 0) {
|
|
94
128
|
setSelectedIndex(selectedIndex - 1);
|
|
@@ -102,20 +136,26 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
102
136
|
else if (key.escape) {
|
|
103
137
|
exit();
|
|
104
138
|
}
|
|
105
|
-
else if (input === "d"
|
|
106
|
-
|
|
139
|
+
else if (input === "d") {
|
|
140
|
+
selectByKey("devboxes");
|
|
141
|
+
}
|
|
142
|
+
else if (input === "b") {
|
|
143
|
+
selectByKey("blueprints");
|
|
144
|
+
}
|
|
145
|
+
else if (input === "s") {
|
|
146
|
+
selectByKey("snapshots");
|
|
107
147
|
}
|
|
108
|
-
else if (input === "
|
|
109
|
-
|
|
148
|
+
else if (input === "o") {
|
|
149
|
+
selectByKey("objects");
|
|
110
150
|
}
|
|
111
|
-
else if (input === "
|
|
112
|
-
|
|
151
|
+
else if (input === "e") {
|
|
152
|
+
selectByKey("benchmarks");
|
|
113
153
|
}
|
|
114
|
-
else if (input === "
|
|
115
|
-
|
|
154
|
+
else if (input === "n") {
|
|
155
|
+
selectByKey("settings");
|
|
116
156
|
}
|
|
117
|
-
else if (input
|
|
118
|
-
|
|
157
|
+
else if (input >= "1" && input <= "9") {
|
|
158
|
+
selectByNumber(parseInt(input, 10));
|
|
119
159
|
}
|
|
120
160
|
else if (input === "u" && updateAvailable) {
|
|
121
161
|
// Release terminal and exec into update command (never returns)
|
|
@@ -127,8 +167,9 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
127
167
|
});
|
|
128
168
|
const layoutMode = getLayoutMode(terminalHeight);
|
|
129
169
|
// Navigation tips for all layouts
|
|
170
|
+
const quickSelectRange = `1-${menuItems.length}`;
|
|
130
171
|
const navTips = (_jsx(NavigationTips, { showArrows: true, paddingX: 2, tips: [
|
|
131
|
-
{ key:
|
|
172
|
+
{ key: quickSelectRange, label: "Quick select" },
|
|
132
173
|
{ key: "Enter", label: "Select" },
|
|
133
174
|
{ key: "Esc", label: "Quit" },
|
|
134
175
|
{ key: "u", label: "Update", condition: !!updateAvailable },
|
|
@@ -137,14 +178,14 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
137
178
|
if (layoutMode === "minimal") {
|
|
138
179
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingX: 2, children: [_jsx(Text, { color: colors.primary, bold: true, children: "RUNLOOP" }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "v", VERSION] })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
|
|
139
180
|
const isSelected = index === selectedIndex;
|
|
140
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsxs(Text, { color: item.color, children: [" ", item.icon, " "] }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
181
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsxs(Text, { color: item.color, children: [" ", item.icon, " "] }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), item.betaFeature && _jsx(BetaBadge, {}), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
141
182
|
}) }), navTips] }));
|
|
142
183
|
}
|
|
143
184
|
// Compact layout - no banner, simple items with descriptions (or no descriptions if narrow)
|
|
144
185
|
if (layoutMode === "compact") {
|
|
145
186
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }], showVersionCheck: true }), _jsxs(Box, { paddingX: 2, children: [_jsx(Text, { color: colors.primary, bold: true, children: "RUNLOOP.ai" }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 v", VERSION] })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
|
|
146
187
|
const isSelected = index === selectedIndex;
|
|
147
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsxs(Text, { color: item.color, children: [" ", item.icon, " "] }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsx(Text, { color: colors.textDim, dimColor: true, children: isNarrow
|
|
188
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsxs(Text, { color: item.color, children: [" ", item.icon, " "] }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), item.betaFeature && _jsx(BetaBadge, {}), _jsx(Text, { color: colors.textDim, dimColor: true, children: isNarrow
|
|
148
189
|
? ` [${index + 1}]`
|
|
149
190
|
: ` - ${item.description} [${index + 1}]` })] }, item.key));
|
|
150
191
|
}) }), navTips] }));
|
|
@@ -155,7 +196,7 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
155
196
|
? ` • v${VERSION}`
|
|
156
197
|
: ` • Cloud development environments • v${VERSION}` })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
|
|
157
198
|
const isSelected = index === selectedIndex;
|
|
158
|
-
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), !isNarrow && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
199
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), item.betaFeature && _jsx(BetaBadge, {}), !isNarrow && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
159
200
|
}) }), navTips] }));
|
|
160
201
|
}
|
|
161
202
|
// Full layout - big banner, bordered items (or simple items if narrow)
|
|
@@ -163,11 +204,11 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
163
204
|
// Narrow layout - no borders, compact items
|
|
164
205
|
_jsx(Box, { flexDirection: "column", marginTop: 1, children: menuItems.map((item, index) => {
|
|
165
206
|
const isSelected = index === selectedIndex;
|
|
166
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsxs(Text, { color: item.color, children: [" ", item.icon, " "] }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
207
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsxs(Text, { color: item.color, children: [" ", item.icon, " "] }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), item.betaFeature && _jsx(BetaBadge, {}), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
167
208
|
}) })) : (
|
|
168
209
|
// Wide layout - bordered items with descriptions
|
|
169
210
|
menuItems.map((item, index) => {
|
|
170
211
|
const isSelected = index === selectedIndex;
|
|
171
|
-
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [isSelected && (_jsxs(_Fragment, { children: [_jsx(Text, { color: item.color, bold: true, children: figures.pointer }), _jsx(Text, { children: " " })] })), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
|
|
212
|
+
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [isSelected && (_jsxs(_Fragment, { children: [_jsx(Text, { color: item.color, bold: true, children: figures.pointer }), _jsx(Text, { children: " " })] })), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), item.betaFeature && _jsx(BetaBadge, {}), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
|
|
172
213
|
}))] }), navTips] }));
|
|
173
214
|
};
|