@runloop/rl-cli 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +19 -5
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/blueprint/delete.js +21 -0
  4. package/dist/commands/blueprint/list.js +226 -174
  5. package/dist/commands/blueprint/prune.js +13 -28
  6. package/dist/commands/devbox/create.js +41 -0
  7. package/dist/commands/devbox/list.js +125 -109
  8. package/dist/commands/devbox/tunnel.js +4 -19
  9. package/dist/commands/gateway-config/create.js +44 -0
  10. package/dist/commands/gateway-config/delete.js +21 -0
  11. package/dist/commands/gateway-config/get.js +15 -0
  12. package/dist/commands/gateway-config/list.js +493 -0
  13. package/dist/commands/gateway-config/update.js +60 -0
  14. package/dist/commands/snapshot/list.js +11 -2
  15. package/dist/commands/snapshot/prune.js +265 -0
  16. package/dist/components/BenchmarkMenu.js +23 -3
  17. package/dist/components/DetailedInfoView.js +20 -0
  18. package/dist/components/DevboxActionsMenu.js +9 -61
  19. package/dist/components/DevboxCreatePage.js +531 -14
  20. package/dist/components/DevboxDetailPage.js +27 -22
  21. package/dist/components/GatewayConfigCreatePage.js +265 -0
  22. package/dist/components/LogsViewer.js +6 -40
  23. package/dist/components/ResourceDetailPage.js +143 -160
  24. package/dist/components/ResourceListView.js +3 -33
  25. package/dist/components/ResourcePicker.js +220 -0
  26. package/dist/components/SecretCreatePage.js +2 -4
  27. package/dist/components/SettingsMenu.js +12 -2
  28. package/dist/components/StateHistory.js +1 -20
  29. package/dist/components/StatusBadge.js +9 -2
  30. package/dist/components/StreamingLogsViewer.js +8 -42
  31. package/dist/components/form/FormTextInput.js +4 -2
  32. package/dist/components/resourceDetailTypes.js +18 -0
  33. package/dist/hooks/useInputHandler.js +103 -0
  34. package/dist/router/Router.js +79 -2
  35. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  36. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  37. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  38. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  39. package/dist/screens/BenchmarkListScreen.js +266 -0
  40. package/dist/screens/BenchmarkMenuScreen.js +6 -0
  41. package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
  42. package/dist/screens/BenchmarkRunListScreen.js +21 -1
  43. package/dist/screens/BlueprintDetailScreen.js +5 -1
  44. package/dist/screens/DevboxCreateScreen.js +2 -2
  45. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  46. package/dist/screens/GatewayConfigListScreen.js +7 -0
  47. package/dist/screens/ScenarioRunDetailScreen.js +6 -0
  48. package/dist/screens/SettingsMenuScreen.js +3 -0
  49. package/dist/screens/SnapshotDetailScreen.js +6 -0
  50. package/dist/services/agentService.js +42 -0
  51. package/dist/services/benchmarkJobService.js +122 -0
  52. package/dist/services/benchmarkService.js +47 -0
  53. package/dist/services/gatewayConfigService.js +114 -0
  54. package/dist/services/scenarioService.js +34 -0
  55. package/dist/store/benchmarkJobStore.js +66 -0
  56. package/dist/store/benchmarkStore.js +63 -0
  57. package/dist/store/gatewayConfigStore.js +83 -0
  58. package/dist/utils/browser.js +22 -0
  59. package/dist/utils/clipboard.js +41 -0
  60. package/dist/utils/commands.js +80 -0
  61. package/dist/utils/time.js +121 -0
  62. package/package.json +42 -43
@@ -14,6 +14,13 @@ const settingsMenuItems = [
14
14
  icon: "◇",
15
15
  color: colors.info,
16
16
  },
17
+ {
18
+ key: "gateway-configs",
19
+ label: "Gateway Configs",
20
+ description: "Configure API credential proxying",
21
+ icon: "⬡",
22
+ color: colors.success,
23
+ },
17
24
  {
18
25
  key: "secrets",
19
26
  label: "Secrets",
@@ -66,7 +73,10 @@ export const SettingsMenu = ({ onSelect, onBack }) => {
66
73
  else if (input === "n" || input === "1") {
67
74
  onSelect("network-policies");
68
75
  }
69
- else if (input === "s" || input === "2") {
76
+ else if (input === "g" || input === "2") {
77
+ onSelect("gateway-configs");
78
+ }
79
+ else if (input === "s" || input === "3") {
70
80
  onSelect("secrets");
71
81
  }
72
82
  else if (input === "q") {
@@ -77,7 +87,7 @@ export const SettingsMenu = ({ onSelect, onBack }) => {
77
87
  const isSelected = index === selectedIndex;
78
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));
79
89
  }) }), _jsx(NavigationTips, { showArrows: true, paddingX: 2, tips: [
80
- { key: "1-2", label: "Quick select" },
90
+ { key: "1-3", label: "Quick select" },
81
91
  { key: "Enter", label: "Select" },
82
92
  { key: "Esc", label: "Back" },
83
93
  { key: "q", label: "Quit" },
@@ -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,
@@ -109,8 +116,8 @@ export const getStatusDisplay = (status) => {
109
116
  return {
110
117
  icon: figures.tick,
111
118
  color: colors.success,
112
- text: "COMPLETED ",
113
- label: "Completed",
119
+ text: "COMPLETE ",
120
+ label: "Complete",
114
121
  };
115
122
  case "canceled":
116
123
  return {
@@ -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
- const copyToClipboard = async (text) => {
171
- const { spawn } = await import("child_process");
172
- const platform = process.platform;
173
- let command;
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
- // eslint-disable-next-line no-control-regex
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
- // eslint-disable-next-line no-control-regex
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
- return (_jsx(FormField, { label: label, isActive: isActive, error: error, children: isActive ? (_jsx(TextInput, { value: value, onChange: onChange, placeholder: placeholder, onSubmit: onSubmit })) : (_jsx(Text, { color: error ? colors.error : colors.text, children: value || "(empty)" })) }));
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
+ }
@@ -1,17 +1,71 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * Router - Manages screen navigation with clean mount/unmount lifecycle
4
4
  * Replaces conditional rendering pattern from menu.tsx
5
5
  */
6
6
  import React from "react";
7
+ import { Box, Text, useInput } from "ink";
8
+ import figures from "figures";
7
9
  import { useNavigation } from "../store/navigationStore.js";
8
10
  import { useDevboxStore } from "../store/devboxStore.js";
9
11
  import { useBlueprintStore } from "../store/blueprintStore.js";
10
12
  import { useSnapshotStore } from "../store/snapshotStore.js";
11
13
  import { useNetworkPolicyStore } from "../store/networkPolicyStore.js";
14
+ import { useGatewayConfigStore } from "../store/gatewayConfigStore.js";
12
15
  import { useObjectStore } from "../store/objectStore.js";
13
16
  import { useBenchmarkStore } from "../store/benchmarkStore.js";
17
+ import { useBenchmarkJobStore } from "../store/benchmarkJobStore.js";
14
18
  import { ErrorBoundary } from "../components/ErrorBoundary.js";
19
+ import { colors } from "../utils/theme.js";
20
+ // List of all known screens
21
+ const KNOWN_SCREENS = new Set([
22
+ "menu",
23
+ "settings-menu",
24
+ "devbox-list",
25
+ "devbox-detail",
26
+ "devbox-actions",
27
+ "devbox-exec",
28
+ "devbox-create",
29
+ "blueprint-list",
30
+ "blueprint-detail",
31
+ "blueprint-logs",
32
+ "snapshot-list",
33
+ "snapshot-detail",
34
+ "network-policy-list",
35
+ "network-policy-detail",
36
+ "network-policy-create",
37
+ "gateway-config-list",
38
+ "gateway-config-detail",
39
+ "gateway-config-create",
40
+ "secret-list",
41
+ "secret-detail",
42
+ "secret-create",
43
+ "object-list",
44
+ "object-detail",
45
+ "ssh-session",
46
+ "benchmark-menu",
47
+ "benchmark-list",
48
+ "benchmark-detail",
49
+ "benchmark-run-list",
50
+ "benchmark-run-detail",
51
+ "scenario-run-list",
52
+ "scenario-run-detail",
53
+ "benchmark-job-list",
54
+ "benchmark-job-detail",
55
+ "benchmark-job-create",
56
+ ]);
57
+ /**
58
+ * Fallback screen for unknown routes
59
+ */
60
+ function UnknownScreen({ screenName }) {
61
+ const { navigate } = useNavigation();
62
+ useInput((input, key) => {
63
+ if (key.return) {
64
+ navigate("menu");
65
+ }
66
+ });
67
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.warning, bold: true, children: [figures.warning, " Unknown Page"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.textDim, children: ["You've navigated to an unknown page: ", _jsx(Text, { color: colors.error, children: `"${screenName}"` })] }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: ["Press", " ", _jsx(Text, { color: colors.primary, bold: true, children: "Enter" }), " ", "to return to the main menu"] }) })] }));
68
+ }
15
69
  // Import screen components
16
70
  import { MenuScreen } from "../screens/MenuScreen.js";
17
71
  import { DevboxListScreen } from "../screens/DevboxListScreen.js";
@@ -27,6 +81,8 @@ import { SnapshotDetailScreen } from "../screens/SnapshotDetailScreen.js";
27
81
  import { NetworkPolicyListScreen } from "../screens/NetworkPolicyListScreen.js";
28
82
  import { NetworkPolicyDetailScreen } from "../screens/NetworkPolicyDetailScreen.js";
29
83
  import { NetworkPolicyCreateScreen } from "../screens/NetworkPolicyCreateScreen.js";
84
+ import { GatewayConfigListScreen } from "../screens/GatewayConfigListScreen.js";
85
+ import { GatewayConfigDetailScreen } from "../screens/GatewayConfigDetailScreen.js";
30
86
  import { SettingsMenuScreen } from "../screens/SettingsMenuScreen.js";
31
87
  import { SecretListScreen } from "../screens/SecretListScreen.js";
32
88
  import { SecretDetailScreen } from "../screens/SecretDetailScreen.js";
@@ -35,10 +91,15 @@ import { ObjectListScreen } from "../screens/ObjectListScreen.js";
35
91
  import { ObjectDetailScreen } from "../screens/ObjectDetailScreen.js";
36
92
  import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
37
93
  import { BenchmarkMenuScreen } from "../screens/BenchmarkMenuScreen.js";
94
+ import { BenchmarkListScreen } from "../screens/BenchmarkListScreen.js";
95
+ import { BenchmarkDetailScreen } from "../screens/BenchmarkDetailScreen.js";
38
96
  import { BenchmarkRunListScreen } from "../screens/BenchmarkRunListScreen.js";
39
97
  import { BenchmarkRunDetailScreen } from "../screens/BenchmarkRunDetailScreen.js";
40
98
  import { ScenarioRunListScreen } from "../screens/ScenarioRunListScreen.js";
41
99
  import { ScenarioRunDetailScreen } from "../screens/ScenarioRunDetailScreen.js";
100
+ import { BenchmarkJobListScreen } from "../screens/BenchmarkJobListScreen.js";
101
+ import { BenchmarkJobDetailScreen } from "../screens/BenchmarkJobDetailScreen.js";
102
+ import { BenchmarkJobCreateScreen } from "../screens/BenchmarkJobCreateScreen.js";
42
103
  /**
43
104
  * Router component that renders the current screen
44
105
  * Implements memory cleanup on route changes
@@ -86,6 +147,13 @@ export function Router() {
86
147
  useNetworkPolicyStore.getState().clearAll();
87
148
  }
88
149
  break;
150
+ case "gateway-config-list":
151
+ case "gateway-config-detail":
152
+ case "gateway-config-create":
153
+ if (!currentScreen.startsWith("gateway-config")) {
154
+ useGatewayConfigStore.getState().clearAll();
155
+ }
156
+ break;
89
157
  case "object-list":
90
158
  case "object-detail":
91
159
  if (!currentScreen.startsWith("object")) {
@@ -93,6 +161,8 @@ export function Router() {
93
161
  }
94
162
  break;
95
163
  case "benchmark-menu":
164
+ case "benchmark-list":
165
+ case "benchmark-detail":
96
166
  case "benchmark-run-list":
97
167
  case "benchmark-run-detail":
98
168
  case "scenario-run-list":
@@ -102,6 +172,13 @@ export function Router() {
102
172
  useBenchmarkStore.getState().clearAll();
103
173
  }
104
174
  break;
175
+ case "benchmark-job-list":
176
+ case "benchmark-job-detail":
177
+ case "benchmark-job-create":
178
+ if (!currentScreen.startsWith("benchmark-job")) {
179
+ useBenchmarkJobStore.getState().clearAll();
180
+ }
181
+ break;
105
182
  }
106
183
  }
107
184
  prevScreenRef.current = currentScreen;
@@ -110,5 +187,5 @@ export function Router() {
110
187
  // and mount new component, preventing race conditions during screen transitions.
111
188
  // The key ensures React treats this as a completely new component tree.
112
189
  // Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
113
- return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "settings-menu" && (_jsx(SettingsMenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-exec" && (_jsx(DevboxExecScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "secret-list" && (_jsx(SecretListScreen, { ...params }, currentScreen)), currentScreen === "secret-detail" && (_jsx(SecretDetailScreen, { ...params }, currentScreen)), currentScreen === "secret-create" && (_jsx(SecretCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen)), currentScreen === "benchmark-menu" && (_jsx(BenchmarkMenuScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-list" && (_jsx(BenchmarkRunListScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-detail" && (_jsx(BenchmarkRunDetailScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-list" && (_jsx(ScenarioRunListScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-detail" && (_jsx(ScenarioRunDetailScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
190
+ return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "settings-menu" && (_jsx(SettingsMenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-exec" && (_jsx(DevboxExecScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "gateway-config-list" && (_jsx(GatewayConfigListScreen, { ...params }, currentScreen)), currentScreen === "gateway-config-detail" && (_jsx(GatewayConfigDetailScreen, { ...params }, currentScreen)), currentScreen === "secret-list" && (_jsx(SecretListScreen, { ...params }, currentScreen)), currentScreen === "secret-detail" && (_jsx(SecretDetailScreen, { ...params }, currentScreen)), currentScreen === "secret-create" && (_jsx(SecretCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen)), currentScreen === "benchmark-menu" && (_jsx(BenchmarkMenuScreen, { ...params }, currentScreen)), currentScreen === "benchmark-list" && (_jsx(BenchmarkListScreen, { ...params }, currentScreen)), currentScreen === "benchmark-detail" && (_jsx(BenchmarkDetailScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-list" && (_jsx(BenchmarkRunListScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-detail" && (_jsx(BenchmarkRunDetailScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-list" && (_jsx(ScenarioRunListScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-detail" && (_jsx(ScenarioRunDetailScreen, { ...params }, currentScreen)), currentScreen === "benchmark-job-list" && (_jsx(BenchmarkJobListScreen, { ...params }, currentScreen)), currentScreen === "benchmark-job-detail" && (_jsx(BenchmarkJobDetailScreen, { ...params }, currentScreen)), currentScreen === "benchmark-job-create" && (_jsx(BenchmarkJobCreateScreen, { ...params }, currentScreen)), !KNOWN_SCREENS.has(currentScreen) && (_jsx(UnknownScreen, { screenName: currentScreen }, currentScreen))] }, `boundary-${currentScreen}`));
114
191
  }
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * BenchmarkDetailScreen - Detail page for benchmark definitions
4
+ * Uses the generic ResourceDetailPage component
5
+ */
6
+ import React from "react";
7
+ import { Text } from "ink";
8
+ import figures from "figures";
9
+ import { useNavigation } from "../store/navigationStore.js";
10
+ import { useBenchmarkStore } from "../store/benchmarkStore.js";
11
+ import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
12
+ import { getBenchmark } from "../services/benchmarkService.js";
13
+ import { SpinnerComponent } from "../components/Spinner.js";
14
+ import { ErrorMessage } from "../components/ErrorMessage.js";
15
+ import { Breadcrumb } from "../components/Breadcrumb.js";
16
+ import { colors } from "../utils/theme.js";
17
+ export function BenchmarkDetailScreen({ benchmarkId, }) {
18
+ const { goBack, navigate } = useNavigation();
19
+ const benchmarks = useBenchmarkStore((state) => state.benchmarks);
20
+ const [loading, setLoading] = React.useState(false);
21
+ const [error, setError] = React.useState(null);
22
+ const [fetchedBenchmark, setFetchedBenchmark] = React.useState(null);
23
+ // Find benchmark in store first
24
+ const benchmarkFromStore = benchmarks.find((b) => b.id === benchmarkId);
25
+ // Polling function
26
+ const pollBenchmark = React.useCallback(async () => {
27
+ if (!benchmarkId)
28
+ return null;
29
+ return getBenchmark(benchmarkId);
30
+ }, [benchmarkId]);
31
+ // Fetch benchmark from API if not in store
32
+ React.useEffect(() => {
33
+ if (benchmarkId && !loading && !fetchedBenchmark && !benchmarkFromStore) {
34
+ setLoading(true);
35
+ setError(null);
36
+ getBenchmark(benchmarkId)
37
+ .then((benchmark) => {
38
+ setFetchedBenchmark(benchmark);
39
+ setLoading(false);
40
+ })
41
+ .catch((err) => {
42
+ setError(err);
43
+ setLoading(false);
44
+ });
45
+ }
46
+ }, [benchmarkId, loading, fetchedBenchmark, benchmarkFromStore]);
47
+ // Use fetched benchmark for full details, fall back to store for basic display
48
+ const benchmark = fetchedBenchmark || benchmarkFromStore;
49
+ // Show loading state
50
+ if (!benchmark && benchmarkId && !error) {
51
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
52
+ { label: "Home" },
53
+ { label: "Benchmarks" },
54
+ { label: "Benchmark Definitions" },
55
+ { label: "Loading...", active: true },
56
+ ] }), _jsx(SpinnerComponent, { message: "Loading benchmark details..." })] }));
57
+ }
58
+ // Show error state
59
+ if (error && !benchmark) {
60
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
61
+ { label: "Home" },
62
+ { label: "Benchmarks" },
63
+ { label: "Benchmark Definitions" },
64
+ { label: "Error", active: true },
65
+ ] }), _jsx(ErrorMessage, { message: "Failed to load benchmark details", error: error })] }));
66
+ }
67
+ // Show not found error
68
+ if (!benchmark) {
69
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
70
+ { label: "Home" },
71
+ { label: "Benchmarks" },
72
+ { label: "Benchmark Definitions" },
73
+ { label: "Not Found", active: true },
74
+ ] }), _jsx(ErrorMessage, { message: `Benchmark ${benchmarkId || "unknown"} not found`, error: new Error("Benchmark not found") })] }));
75
+ }
76
+ // Build detail sections
77
+ const detailSections = [];
78
+ // Basic details section
79
+ const basicFields = [];
80
+ if (benchmark.created_at) {
81
+ basicFields.push({
82
+ label: "Created",
83
+ value: formatTimestamp(benchmark.created_at),
84
+ });
85
+ }
86
+ if (benchmark.description) {
87
+ basicFields.push({
88
+ label: "Description",
89
+ value: benchmark.description,
90
+ });
91
+ }
92
+ if (basicFields.length > 0) {
93
+ detailSections.push({
94
+ title: "Details",
95
+ icon: figures.squareSmallFilled,
96
+ color: colors.warning,
97
+ fields: basicFields,
98
+ });
99
+ }
100
+ // Metadata section
101
+ if (benchmark.metadata &&
102
+ Object.keys(benchmark.metadata).length > 0) {
103
+ const metadataFields = Object.entries(benchmark.metadata).map(([key, value]) => ({
104
+ label: key,
105
+ value: value,
106
+ }));
107
+ detailSections.push({
108
+ title: "Metadata",
109
+ icon: figures.identical,
110
+ color: colors.secondary,
111
+ fields: metadataFields,
112
+ });
113
+ }
114
+ // Operations available for benchmarks
115
+ const operations = [
116
+ {
117
+ key: "create-job",
118
+ label: "Create Benchmark Job",
119
+ color: colors.success,
120
+ icon: figures.play,
121
+ shortcut: "c",
122
+ },
123
+ ];
124
+ // Handle operation selection
125
+ const handleOperation = async (operation, resource) => {
126
+ switch (operation) {
127
+ case "create-job":
128
+ navigate("benchmark-job-create", { initialBenchmarkIds: resource.id });
129
+ break;
130
+ }
131
+ };
132
+ // Build detailed info lines for full details view
133
+ const buildDetailLines = (b) => {
134
+ const lines = [];
135
+ // Core Information
136
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Benchmark Details" }, "core-title"));
137
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", b.id] }, "core-id"));
138
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", b.name || "(none)"] }, "core-name"));
139
+ if (b.description) {
140
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Description: ", b.description] }, "core-desc"));
141
+ }
142
+ if (b.created_at) {
143
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(b.created_at).toLocaleString()] }, "core-created"));
144
+ }
145
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
146
+ // Metadata
147
+ if (b.metadata && Object.keys(b.metadata).length > 0) {
148
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
149
+ Object.entries(b.metadata).forEach(([key, value], idx) => {
150
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
151
+ });
152
+ lines.push(_jsx(Text, { children: " " }, "meta-space"));
153
+ }
154
+ // Raw JSON
155
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
156
+ const jsonLines = JSON.stringify(b, null, 2).split("\n");
157
+ jsonLines.forEach((line, idx) => {
158
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
159
+ });
160
+ return lines;
161
+ };
162
+ return (_jsx(ResourceDetailPage, { resource: benchmark, resourceType: "Benchmark Definitions", getDisplayName: (b) => b.name || b.id, getId: (b) => b.id, getStatus: (b) => b.status, detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, breadcrumbPrefix: [{ label: "Home" }, { label: "Benchmarks" }] }));
163
+ }