@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* SecretCreatePage - Form for creating a new secret
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
import figures from "figures";
|
|
8
|
+
import { getClient } from "../utils/client.js";
|
|
9
|
+
import { SpinnerComponent } from "./Spinner.js";
|
|
10
|
+
import { ErrorMessage } from "./ErrorMessage.js";
|
|
11
|
+
import { SuccessMessage } from "./SuccessMessage.js";
|
|
12
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
13
|
+
import { NavigationTips } from "./NavigationTips.js";
|
|
14
|
+
import { FormTextInput, FormActionButton } from "./form/index.js";
|
|
15
|
+
import { colors } from "../utils/theme.js";
|
|
16
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
17
|
+
export const SecretCreatePage = ({ onBack, onCreate, }) => {
|
|
18
|
+
const [currentField, setCurrentField] = React.useState("submit");
|
|
19
|
+
const [formData, setFormData] = React.useState({
|
|
20
|
+
name: "",
|
|
21
|
+
value: "",
|
|
22
|
+
});
|
|
23
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
24
|
+
const [result, setResult] = React.useState(null);
|
|
25
|
+
const [error, setError] = React.useState(null);
|
|
26
|
+
const [validationError, setValidationError] = React.useState(null);
|
|
27
|
+
const fields = [
|
|
28
|
+
{ key: "submit", label: "Create Secret", type: "action" },
|
|
29
|
+
{ key: "name", label: "Name (required)", type: "text" },
|
|
30
|
+
{ key: "value", label: "Value (required)", type: "password" },
|
|
31
|
+
];
|
|
32
|
+
const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
|
|
33
|
+
// Handle Ctrl+C to exit
|
|
34
|
+
useExitOnCtrlC();
|
|
35
|
+
// Main form input handler
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
// Handle result screen
|
|
38
|
+
if (result) {
|
|
39
|
+
if (input === "q" || key.escape || key.return) {
|
|
40
|
+
if (onCreate) {
|
|
41
|
+
onCreate(result);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
onBack();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Handle error screen
|
|
50
|
+
if (error) {
|
|
51
|
+
if (input === "r" || key.return) {
|
|
52
|
+
// Retry - clear error and return to form
|
|
53
|
+
setError(null);
|
|
54
|
+
}
|
|
55
|
+
else if (input === "q" || key.escape) {
|
|
56
|
+
// Quit - go back to list
|
|
57
|
+
onBack();
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Handle submitting state
|
|
62
|
+
if (submitting) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Back to list
|
|
66
|
+
if (input === "q" || key.escape) {
|
|
67
|
+
onBack();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Submit form with Ctrl+S
|
|
71
|
+
if (input === "s" && key.ctrl) {
|
|
72
|
+
handleSubmit();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Handle Enter on any field to submit
|
|
76
|
+
if (key.return) {
|
|
77
|
+
handleSubmit();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Navigation between fields (up/down arrows and tab/shift+tab)
|
|
81
|
+
if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
|
|
82
|
+
setCurrentField(fields[currentFieldIndex - 1].key);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if ((key.downArrow || (key.tab && !key.shift)) &&
|
|
86
|
+
currentFieldIndex < fields.length - 1) {
|
|
87
|
+
setCurrentField(fields[currentFieldIndex + 1].key);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const handleSubmit = async () => {
|
|
92
|
+
// Validate required fields
|
|
93
|
+
if (!formData.name.trim()) {
|
|
94
|
+
setValidationError("Name is required");
|
|
95
|
+
setCurrentField("name");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!formData.value) {
|
|
99
|
+
setValidationError("Value is required");
|
|
100
|
+
setCurrentField("value");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
setSubmitting(true);
|
|
104
|
+
setError(null);
|
|
105
|
+
setValidationError(null);
|
|
106
|
+
try {
|
|
107
|
+
const client = getClient();
|
|
108
|
+
const secret = await client.secrets.create({
|
|
109
|
+
name: formData.name.trim(),
|
|
110
|
+
value: formData.value,
|
|
111
|
+
});
|
|
112
|
+
setResult(secret);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
setError(err);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
setSubmitting(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
// Result screen
|
|
122
|
+
if (result) {
|
|
123
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
124
|
+
{ label: "Settings" },
|
|
125
|
+
{ label: "Secrets" },
|
|
126
|
+
{ label: "Create", active: true },
|
|
127
|
+
] }), _jsx(SuccessMessage, { message: "Secret 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] }) })] }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { color: colors.info, children: [figures.info, " The secret value has been securely stored and cannot be retrieved."] }) }), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "View secret details" }] })] }));
|
|
128
|
+
}
|
|
129
|
+
// Error screen
|
|
130
|
+
if (error) {
|
|
131
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
132
|
+
{ label: "Settings" },
|
|
133
|
+
{ label: "Secrets" },
|
|
134
|
+
{ label: "Create", active: true },
|
|
135
|
+
] }), _jsx(ErrorMessage, { message: "Failed to create secret", error: error }), _jsx(NavigationTips, { tips: [
|
|
136
|
+
{ key: "Enter/r", label: "Retry" },
|
|
137
|
+
{ key: "q/esc", label: "Cancel" },
|
|
138
|
+
] })] }));
|
|
139
|
+
}
|
|
140
|
+
// Submitting screen
|
|
141
|
+
if (submitting) {
|
|
142
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
143
|
+
{ label: "Settings" },
|
|
144
|
+
{ label: "Secrets" },
|
|
145
|
+
{ label: "Create", active: true },
|
|
146
|
+
] }), _jsx(SpinnerComponent, { message: "Creating secret..." })] }));
|
|
147
|
+
}
|
|
148
|
+
// Form screen
|
|
149
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
150
|
+
{ label: "Settings" },
|
|
151
|
+
{ label: "Secrets" },
|
|
152
|
+
{ label: "Create", active: true },
|
|
153
|
+
] }), _jsx(Box, { borderStyle: "round", borderColor: colors.info, paddingX: 1, paddingY: 0, marginBottom: 1, children: _jsxs(Text, { color: colors.info, children: [figures.info, " ", _jsx(Text, { bold: true, children: "Note:" }), " Secret values are", " ", _jsx(Text, { bold: true, children: "write-only" }), ". Once created, the value cannot be retrieved or viewed. To change a secret, delete it and create a new one."] }) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
|
|
154
|
+
const isActive = currentField === field.key;
|
|
155
|
+
if (field.type === "action") {
|
|
156
|
+
return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: "[Enter to create]" }, field.key));
|
|
157
|
+
}
|
|
158
|
+
if (field.type === "text") {
|
|
159
|
+
const value = formData[field.key];
|
|
160
|
+
const hasError = field.key === "name" && validationError === "Name is required";
|
|
161
|
+
return (_jsx(FormTextInput, { label: field.label, value: value, onChange: (newValue) => {
|
|
162
|
+
setFormData({ ...formData, [field.key]: newValue });
|
|
163
|
+
if (validationError) {
|
|
164
|
+
setValidationError(null);
|
|
165
|
+
}
|
|
166
|
+
}, onSubmit: handleSubmit, isActive: isActive, placeholder: "my-secret-name", error: hasError ? validationError : undefined }, field.key));
|
|
167
|
+
}
|
|
168
|
+
if (field.type === "password") {
|
|
169
|
+
const value = formData[field.key];
|
|
170
|
+
const hasError = field.key === "value" && validationError === "Value is required";
|
|
171
|
+
return (_jsx(FormTextInput, { label: field.label, value: value, onChange: (newValue) => {
|
|
172
|
+
setFormData({ ...formData, [field.key]: newValue });
|
|
173
|
+
if (validationError) {
|
|
174
|
+
setValidationError(null);
|
|
175
|
+
}
|
|
176
|
+
}, onSubmit: handleSubmit, isActive: isActive, placeholder: "Enter secret value", error: hasError ? validationError : undefined, mask: "*" }, field.key));
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}) }), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
180
|
+
{ key: "Enter", label: "Create" },
|
|
181
|
+
{ key: "q", label: "Cancel" },
|
|
182
|
+
] })] }));
|
|
183
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput, useApp, useStdout } from "ink";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
6
|
+
import { NavigationTips } from "./NavigationTips.js";
|
|
7
|
+
import { colors } from "../utils/theme.js";
|
|
8
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
9
|
+
const settingsMenuItems = [
|
|
10
|
+
{
|
|
11
|
+
key: "network-policies",
|
|
12
|
+
label: "Network Policies",
|
|
13
|
+
description: "Manage egress network access rules",
|
|
14
|
+
icon: "◇",
|
|
15
|
+
color: colors.info,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: "gateway-configs",
|
|
19
|
+
label: "Gateway Configs",
|
|
20
|
+
description: "Configure API credential proxying",
|
|
21
|
+
icon: "⬡",
|
|
22
|
+
color: colors.success,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "secrets",
|
|
26
|
+
label: "Secrets",
|
|
27
|
+
description: "Manage sensitive values and credentials",
|
|
28
|
+
icon: "◆",
|
|
29
|
+
color: colors.warning,
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
export const SettingsMenu = ({ onSelect, onBack }) => {
|
|
33
|
+
const { exit } = useApp();
|
|
34
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
35
|
+
const { stdout } = useStdout();
|
|
36
|
+
// Get terminal dimensions for responsive layout
|
|
37
|
+
const getTerminalDimensions = React.useCallback(() => {
|
|
38
|
+
return {
|
|
39
|
+
height: stdout?.rows && stdout.rows > 0 ? stdout.rows : 20,
|
|
40
|
+
width: stdout?.columns && stdout.columns > 0 ? stdout.columns : 80,
|
|
41
|
+
};
|
|
42
|
+
}, [stdout]);
|
|
43
|
+
const [terminalDimensions, setTerminalDimensions] = React.useState(getTerminalDimensions);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
setTerminalDimensions(getTerminalDimensions());
|
|
46
|
+
if (!stdout)
|
|
47
|
+
return;
|
|
48
|
+
const handleResize = () => {
|
|
49
|
+
setTerminalDimensions(getTerminalDimensions());
|
|
50
|
+
};
|
|
51
|
+
stdout.on("resize", handleResize);
|
|
52
|
+
return () => {
|
|
53
|
+
stdout.off("resize", handleResize);
|
|
54
|
+
};
|
|
55
|
+
}, [stdout, getTerminalDimensions]);
|
|
56
|
+
const terminalWidth = terminalDimensions.width;
|
|
57
|
+
const isNarrow = terminalWidth < 70;
|
|
58
|
+
// Handle Ctrl+C to exit
|
|
59
|
+
useExitOnCtrlC();
|
|
60
|
+
useInput((input, key) => {
|
|
61
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
62
|
+
setSelectedIndex(selectedIndex - 1);
|
|
63
|
+
}
|
|
64
|
+
else if (key.downArrow && selectedIndex < settingsMenuItems.length - 1) {
|
|
65
|
+
setSelectedIndex(selectedIndex + 1);
|
|
66
|
+
}
|
|
67
|
+
else if (key.return) {
|
|
68
|
+
onSelect(settingsMenuItems[selectedIndex].key);
|
|
69
|
+
}
|
|
70
|
+
else if (key.escape) {
|
|
71
|
+
onBack();
|
|
72
|
+
}
|
|
73
|
+
else if (input === "n" || input === "1") {
|
|
74
|
+
onSelect("network-policies");
|
|
75
|
+
}
|
|
76
|
+
else if (input === "g" || input === "2") {
|
|
77
|
+
onSelect("gateway-configs");
|
|
78
|
+
}
|
|
79
|
+
else if (input === "s" || input === "3") {
|
|
80
|
+
onSelect("secrets");
|
|
81
|
+
}
|
|
82
|
+
else if (input === "q") {
|
|
83
|
+
exit();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home" }, { label: "Settings", active: true }] }), _jsxs(Box, { paddingX: 2, marginBottom: 1, children: [_jsx(Text, { color: colors.primary, bold: true, children: "Settings" }), _jsx(Text, { color: colors.textDim, dimColor: true, children: isNarrow ? "" : " • Configure your environment" })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: settingsMenuItems.map((item, index) => {
|
|
87
|
+
const isSelected = index === selectedIndex;
|
|
88
|
+
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));
|
|
89
|
+
}) }), _jsx(NavigationTips, { showArrows: true, paddingX: 2, tips: [
|
|
90
|
+
{ key: "1-3", label: "Quick select" },
|
|
91
|
+
{ key: "Enter", label: "Select" },
|
|
92
|
+
{ key: "Esc", label: "Back" },
|
|
93
|
+
{ key: "q", label: "Quit" },
|
|
94
|
+
] })] }));
|
|
95
|
+
};
|
|
@@ -3,6 +3,7 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import figures from "figures";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
5
|
import { getStatusDisplay } from "./StatusBadge.js";
|
|
6
|
+
import { formatTimeAgo } from "../utils/time.js";
|
|
6
7
|
// Format shutdown reason into human-readable text
|
|
7
8
|
const formatShutdownReason = (reason) => {
|
|
8
9
|
switch (reason) {
|
|
@@ -33,26 +34,6 @@ const formatShutdownReason = (reason) => {
|
|
|
33
34
|
return reason.replace(/_/g, " ");
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
|
-
// Format time ago in a succinct way
|
|
37
|
-
const formatTimeAgo = (timestamp) => {
|
|
38
|
-
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
39
|
-
if (seconds < 60)
|
|
40
|
-
return `${seconds}s ago`;
|
|
41
|
-
const minutes = Math.floor(seconds / 60);
|
|
42
|
-
if (minutes < 60)
|
|
43
|
-
return `${minutes}m ago`;
|
|
44
|
-
const hours = Math.floor(minutes / 60);
|
|
45
|
-
if (hours < 24)
|
|
46
|
-
return `${hours}h ago`;
|
|
47
|
-
const days = Math.floor(hours / 24);
|
|
48
|
-
if (days < 30)
|
|
49
|
-
return `${days}d ago`;
|
|
50
|
-
const months = Math.floor(days / 30);
|
|
51
|
-
if (months < 12)
|
|
52
|
-
return `${months}mo ago`;
|
|
53
|
-
const years = Math.floor(months / 12);
|
|
54
|
-
return `${years}y ago`;
|
|
55
|
-
};
|
|
56
37
|
// Format duration in a succinct way
|
|
57
38
|
const formatDuration = (milliseconds) => {
|
|
58
39
|
const seconds = Math.floor(milliseconds / 1000);
|
|
@@ -82,6 +82,13 @@ export const getStatusDisplay = (status) => {
|
|
|
82
82
|
label: "Failed",
|
|
83
83
|
};
|
|
84
84
|
// === BUILD STATES (for blueprints) ===
|
|
85
|
+
case "queued":
|
|
86
|
+
return {
|
|
87
|
+
icon: figures.ellipsis,
|
|
88
|
+
color: colors.warning,
|
|
89
|
+
text: "QUEUED ",
|
|
90
|
+
label: "Queued",
|
|
91
|
+
};
|
|
85
92
|
case "ready":
|
|
86
93
|
return {
|
|
87
94
|
icon: figures.tick,
|
|
@@ -104,6 +111,79 @@ export const getStatusDisplay = (status) => {
|
|
|
104
111
|
text: "BUILDING ",
|
|
105
112
|
label: "Building: In Progress",
|
|
106
113
|
};
|
|
114
|
+
// === BENCHMARK/SCENARIO STATES ===
|
|
115
|
+
case "completed":
|
|
116
|
+
return {
|
|
117
|
+
icon: figures.tick,
|
|
118
|
+
color: colors.success,
|
|
119
|
+
text: "COMPLETE ",
|
|
120
|
+
label: "Complete",
|
|
121
|
+
};
|
|
122
|
+
case "canceled":
|
|
123
|
+
return {
|
|
124
|
+
icon: figures.cross,
|
|
125
|
+
color: colors.textDim,
|
|
126
|
+
text: "CANCELED ",
|
|
127
|
+
label: "Canceled",
|
|
128
|
+
};
|
|
129
|
+
case "scoring":
|
|
130
|
+
return {
|
|
131
|
+
icon: figures.arrowUp,
|
|
132
|
+
color: colors.warning,
|
|
133
|
+
text: "SCORING ",
|
|
134
|
+
label: "Scoring",
|
|
135
|
+
};
|
|
136
|
+
case "scored":
|
|
137
|
+
return {
|
|
138
|
+
icon: figures.tick,
|
|
139
|
+
color: colors.info,
|
|
140
|
+
text: "SCORED ",
|
|
141
|
+
label: "Scored",
|
|
142
|
+
};
|
|
143
|
+
case "timeout":
|
|
144
|
+
return {
|
|
145
|
+
icon: figures.warning,
|
|
146
|
+
color: colors.error,
|
|
147
|
+
text: "TIMEOUT ",
|
|
148
|
+
label: "Timeout",
|
|
149
|
+
};
|
|
150
|
+
// === GENERIC STATES ===
|
|
151
|
+
case "active":
|
|
152
|
+
return {
|
|
153
|
+
icon: figures.tick,
|
|
154
|
+
color: colors.success,
|
|
155
|
+
text: "ACTIVE ",
|
|
156
|
+
label: "Active",
|
|
157
|
+
};
|
|
158
|
+
// === STORAGE OBJECT STATES ===
|
|
159
|
+
case "UPLOADING":
|
|
160
|
+
return {
|
|
161
|
+
icon: figures.arrowUp,
|
|
162
|
+
color: colors.warning,
|
|
163
|
+
text: "UPLOADING ",
|
|
164
|
+
label: "Uploading",
|
|
165
|
+
};
|
|
166
|
+
case "READ_ONLY":
|
|
167
|
+
return {
|
|
168
|
+
icon: figures.tick,
|
|
169
|
+
color: colors.success,
|
|
170
|
+
text: "READ_ONLY ",
|
|
171
|
+
label: "Read Only",
|
|
172
|
+
};
|
|
173
|
+
case "DELETED":
|
|
174
|
+
return {
|
|
175
|
+
icon: figures.cross,
|
|
176
|
+
color: colors.textDim,
|
|
177
|
+
text: "DELETED ",
|
|
178
|
+
label: "Deleted",
|
|
179
|
+
};
|
|
180
|
+
case "ERROR":
|
|
181
|
+
return {
|
|
182
|
+
icon: figures.warning,
|
|
183
|
+
color: colors.error,
|
|
184
|
+
text: "ERROR ",
|
|
185
|
+
label: "Error",
|
|
186
|
+
};
|
|
107
187
|
default:
|
|
108
188
|
// Truncate and pad any unknown status to 10 chars to match column width
|
|
109
189
|
const truncated = status.toUpperCase().slice(0, 10);
|
|
@@ -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";
|
|
@@ -167,42 +168,10 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
|
|
|
167
168
|
return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
|
|
168
169
|
})
|
|
169
170
|
.join("\n");
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let args;
|
|
175
|
-
if (platform === "darwin") {
|
|
176
|
-
command = "pbcopy";
|
|
177
|
-
args = [];
|
|
178
|
-
}
|
|
179
|
-
else if (platform === "win32") {
|
|
180
|
-
command = "clip";
|
|
181
|
-
args = [];
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
command = "xclip";
|
|
185
|
-
args = ["-selection", "clipboard"];
|
|
186
|
-
}
|
|
187
|
-
const proc = spawn(command, args);
|
|
188
|
-
proc.stdin.write(text);
|
|
189
|
-
proc.stdin.end();
|
|
190
|
-
proc.on("exit", (code) => {
|
|
191
|
-
if (code === 0) {
|
|
192
|
-
setCopyStatus("Copied!");
|
|
193
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
setCopyStatus("Failed");
|
|
197
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
proc.on("error", () => {
|
|
201
|
-
setCopyStatus("Not supported");
|
|
202
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
203
|
-
});
|
|
204
|
-
};
|
|
205
|
-
copyToClipboard(logsText);
|
|
171
|
+
copyToClipboard(logsText).then((status) => {
|
|
172
|
+
setCopyStatus(status);
|
|
173
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
174
|
+
});
|
|
206
175
|
}
|
|
207
176
|
else if (input === "q" || key.escape || key.return) {
|
|
208
177
|
onBack();
|
|
@@ -214,16 +183,13 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
|
|
|
214
183
|
const contentWidth = Math.max(40, terminalWidth - boxChrome);
|
|
215
184
|
// Helper to sanitize log message
|
|
216
185
|
const sanitizeMessage = (message) => {
|
|
217
|
-
const strippedAnsi = message.replace(
|
|
218
|
-
|
|
219
|
-
/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
220
|
-
return (strippedAnsi
|
|
186
|
+
const strippedAnsi = message.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
187
|
+
return strippedAnsi
|
|
221
188
|
.replace(/\r\n/g, " ")
|
|
222
189
|
.replace(/\n/g, " ")
|
|
223
190
|
.replace(/\r/g, " ")
|
|
224
191
|
.replace(/\t/g, " ")
|
|
225
|
-
|
|
226
|
-
.replace(/[\x00-\x1F]/g, ""));
|
|
192
|
+
.replace(/[\x00-\x1F]/g, "");
|
|
227
193
|
};
|
|
228
194
|
// Calculate visible logs
|
|
229
195
|
let visibleLogs;
|
|
@@ -3,6 +3,8 @@ import { Text } from "ink";
|
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import { FormField } from "./FormField.js";
|
|
5
5
|
import { colors } from "../../utils/theme.js";
|
|
6
|
-
export const FormTextInput = ({ label, value, onChange, isActive, placeholder, error, onSubmit, }) => {
|
|
7
|
-
|
|
6
|
+
export const FormTextInput = ({ label, value, onChange, isActive, placeholder, error, onSubmit, mask, }) => {
|
|
7
|
+
// Display value: use mask character if provided
|
|
8
|
+
const displayValue = mask && value ? mask.repeat(value.length) : value;
|
|
9
|
+
return (_jsx(FormField, { label: label, isActive: isActive, error: error, children: isActive ? (_jsx(TextInput, { value: value, onChange: onChange, placeholder: placeholder, onSubmit: onSubmit, mask: mask })) : (_jsx(Text, { color: error ? colors.error : colors.text, children: displayValue || "(empty)" })) }));
|
|
8
10
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk all sections and collect fields that have an action defined.
|
|
3
|
+
* Returns a flat list of references preserving section/field indices
|
|
4
|
+
* so the component can map selections back to the right field.
|
|
5
|
+
*/
|
|
6
|
+
export function collectActionableFields(sections) {
|
|
7
|
+
const refs = [];
|
|
8
|
+
sections.forEach((section, sectionIndex) => {
|
|
9
|
+
section.fields
|
|
10
|
+
.filter((field) => field.value !== undefined && field.value !== null)
|
|
11
|
+
.forEach((field, fieldIndex) => {
|
|
12
|
+
if (field.action) {
|
|
13
|
+
refs.push({ sectionIndex, fieldIndex, action: field.action });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
return refs;
|
|
18
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInputHandler - Declarative, mode-based input handling for Ink components.
|
|
3
|
+
*
|
|
4
|
+
* Replaces long imperative if/else chains in useInput callbacks with a
|
|
5
|
+
* structured system of ordered modes, each with a key-binding map.
|
|
6
|
+
* The first active mode wins; bindings are looked up by canonical key name.
|
|
7
|
+
*/
|
|
8
|
+
import { useInput } from "ink";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Key resolution
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Normalise Ink's (input, key) pair into a single canonical key name.
|
|
14
|
+
*
|
|
15
|
+
* Priority order (first match wins):
|
|
16
|
+
* 1. Special keys (arrows, enter, escape, etc.)
|
|
17
|
+
* 2. Ctrl+<char> combinations
|
|
18
|
+
* 3. The raw `input` string (printable character)
|
|
19
|
+
*/
|
|
20
|
+
export function resolveKeyName(input, key) {
|
|
21
|
+
// Special keys
|
|
22
|
+
if (key.upArrow)
|
|
23
|
+
return "up";
|
|
24
|
+
if (key.downArrow)
|
|
25
|
+
return "down";
|
|
26
|
+
if (key.leftArrow)
|
|
27
|
+
return "left";
|
|
28
|
+
if (key.rightArrow)
|
|
29
|
+
return "right";
|
|
30
|
+
if (key.return)
|
|
31
|
+
return "enter";
|
|
32
|
+
if (key.escape)
|
|
33
|
+
return "escape";
|
|
34
|
+
if (key.tab)
|
|
35
|
+
return "tab";
|
|
36
|
+
if (key.backspace)
|
|
37
|
+
return "backspace";
|
|
38
|
+
if (key.delete)
|
|
39
|
+
return "delete";
|
|
40
|
+
if (key.pageUp)
|
|
41
|
+
return "pageUp";
|
|
42
|
+
if (key.pageDown)
|
|
43
|
+
return "pageDown";
|
|
44
|
+
// Ctrl combinations (e.g. ctrl+c)
|
|
45
|
+
if (key.ctrl && input)
|
|
46
|
+
return `ctrl+${input}`;
|
|
47
|
+
// Printable character
|
|
48
|
+
return input;
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Hook
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Declarative input handler.
|
|
55
|
+
*
|
|
56
|
+
* @param modes Ordered array of input modes. The first mode whose `active()`
|
|
57
|
+
* returns `true` gets to handle the key event.
|
|
58
|
+
* @param options Optional settings forwarded to Ink's useInput.
|
|
59
|
+
*/
|
|
60
|
+
export function useInputHandler(modes, options) {
|
|
61
|
+
useInput((input, key) => {
|
|
62
|
+
const keyName = resolveKeyName(input, key);
|
|
63
|
+
for (const mode of modes) {
|
|
64
|
+
if (!mode.active())
|
|
65
|
+
continue;
|
|
66
|
+
// Try an exact binding match
|
|
67
|
+
const handler = mode.bindings[keyName];
|
|
68
|
+
if (handler) {
|
|
69
|
+
handler();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// No binding matched — try the dynamic fallback
|
|
73
|
+
if (mode.onUnmatched) {
|
|
74
|
+
mode.onUnmatched(input, key);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// captureAll: swallow the event silently
|
|
78
|
+
if (mode.captureAll)
|
|
79
|
+
return;
|
|
80
|
+
// Default: first active mode consumes the event even if nothing matched
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}, { isActive: options?.isActive ?? true });
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Preset binding helpers
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Common scroll bindings (j/k, arrows, page up/down).
|
|
90
|
+
* Spread into a mode's `bindings` to get standard scrolling behaviour.
|
|
91
|
+
*/
|
|
92
|
+
export function scrollBindings(getScroll, setScroll) {
|
|
93
|
+
return {
|
|
94
|
+
down: () => setScroll(getScroll() + 1),
|
|
95
|
+
up: () => setScroll(Math.max(0, getScroll() - 1)),
|
|
96
|
+
j: () => setScroll(getScroll() + 1),
|
|
97
|
+
k: () => setScroll(Math.max(0, getScroll() - 1)),
|
|
98
|
+
s: () => setScroll(getScroll() + 1),
|
|
99
|
+
w: () => setScroll(Math.max(0, getScroll() - 1)),
|
|
100
|
+
pageDown: () => setScroll(getScroll() + 10),
|
|
101
|
+
pageUp: () => setScroll(Math.max(0, getScroll() - 10)),
|
|
102
|
+
};
|
|
103
|
+
}
|