@runloop/rl-cli 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +21 -7
  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 +142 -110
  8. package/dist/commands/devbox/rsync.js +69 -41
  9. package/dist/commands/devbox/scp.js +180 -39
  10. package/dist/commands/devbox/tunnel.js +4 -19
  11. package/dist/commands/gateway-config/create.js +53 -0
  12. package/dist/commands/gateway-config/delete.js +21 -0
  13. package/dist/commands/gateway-config/get.js +18 -0
  14. package/dist/commands/gateway-config/list.js +493 -0
  15. package/dist/commands/gateway-config/update.js +70 -0
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +23 -3
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +26 -62
  21. package/dist/components/DevboxCreatePage.js +763 -15
  22. package/dist/components/DevboxDetailPage.js +73 -24
  23. package/dist/components/GatewayConfigCreatePage.js +272 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/ResourceDetailPage.js +143 -160
  26. package/dist/components/ResourceListView.js +3 -33
  27. package/dist/components/ResourcePicker.js +234 -0
  28. package/dist/components/SecretCreatePage.js +71 -27
  29. package/dist/components/SettingsMenu.js +12 -2
  30. package/dist/components/StateHistory.js +1 -20
  31. package/dist/components/StatusBadge.js +9 -2
  32. package/dist/components/StreamingLogsViewer.js +8 -42
  33. package/dist/components/form/FormTextInput.js +4 -2
  34. package/dist/components/resourceDetailTypes.js +18 -0
  35. package/dist/hooks/useInputHandler.js +103 -0
  36. package/dist/router/Router.js +79 -2
  37. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  38. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  39. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  40. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  41. package/dist/screens/BenchmarkListScreen.js +266 -0
  42. package/dist/screens/BenchmarkMenuScreen.js +6 -0
  43. package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
  44. package/dist/screens/BenchmarkRunListScreen.js +21 -1
  45. package/dist/screens/BlueprintDetailScreen.js +5 -1
  46. package/dist/screens/DevboxCreateScreen.js +2 -2
  47. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  48. package/dist/screens/GatewayConfigListScreen.js +7 -0
  49. package/dist/screens/ScenarioRunDetailScreen.js +6 -0
  50. package/dist/screens/SecretDetailScreen.js +26 -2
  51. package/dist/screens/SettingsMenuScreen.js +3 -0
  52. package/dist/screens/SnapshotDetailScreen.js +6 -0
  53. package/dist/services/agentService.js +42 -0
  54. package/dist/services/benchmarkJobService.js +122 -0
  55. package/dist/services/benchmarkService.js +47 -0
  56. package/dist/services/gatewayConfigService.js +153 -0
  57. package/dist/services/scenarioService.js +34 -0
  58. package/dist/store/benchmarkJobStore.js +66 -0
  59. package/dist/store/benchmarkStore.js +63 -0
  60. package/dist/store/gatewayConfigStore.js +83 -0
  61. package/dist/utils/browser.js +22 -0
  62. package/dist/utils/clipboard.js +41 -0
  63. package/dist/utils/commands.js +105 -9
  64. package/dist/utils/gatewayConfigValidation.js +58 -0
  65. package/dist/utils/time.js +121 -0
  66. package/package.json +43 -43
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * DevboxDetailPage - Detail page for devboxes
4
4
  * Uses the generic ResourceDetailPage component with devbox-specific customizations
@@ -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
- // Format time ago in a succinct way
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);
@@ -104,7 +85,9 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
104
85
  ];
105
86
  // Filter operations based on devbox status
106
87
  const getFilteredOperations = (devbox) => {
107
- return allOperations.filter((op) => {
88
+ const hasTunnel = !!(devbox.tunnel && devbox.tunnel.tunnel_key);
89
+ return allOperations
90
+ .filter((op) => {
108
91
  const status = devbox.status;
109
92
  // When suspended: logs and resume
110
93
  if (status === "suspended") {
@@ -122,6 +105,20 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
122
105
  }
123
106
  // Default for transitional states (provisioning, initializing)
124
107
  return op.key === "logs" || op.key === "delete";
108
+ })
109
+ .map((op) => {
110
+ // Dynamic tunnel label based on whether tunnel is active
111
+ if (op.key === "tunnel") {
112
+ return hasTunnel
113
+ ? {
114
+ ...op,
115
+ label: "Tunnel (Active)",
116
+ color: colors.success,
117
+ icon: figures.tick,
118
+ }
119
+ : op;
120
+ }
121
+ return op;
125
122
  });
126
123
  };
127
124
  // Build detail sections for the devbox
@@ -210,10 +207,28 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
210
207
  });
211
208
  }
212
209
  // Source
213
- if (devbox.blueprint_id || devbox.snapshot_id) {
210
+ if (devbox.blueprint_id) {
211
+ detailFields.push({
212
+ label: "Source",
213
+ value: _jsx(Text, { color: colors.success, children: devbox.blueprint_id }),
214
+ action: {
215
+ type: "navigate",
216
+ screen: "blueprint-detail",
217
+ params: { blueprintId: devbox.blueprint_id },
218
+ hint: "View Blueprint",
219
+ },
220
+ });
221
+ }
222
+ else if (devbox.snapshot_id) {
214
223
  detailFields.push({
215
224
  label: "Source",
216
- value: (_jsx(Text, { color: colors.success, children: devbox.blueprint_id || devbox.snapshot_id })),
225
+ value: _jsx(Text, { color: colors.success, children: devbox.snapshot_id }),
226
+ action: {
227
+ type: "navigate",
228
+ screen: "snapshot-detail",
229
+ params: { snapshotId: devbox.snapshot_id },
230
+ hint: "View Snapshot",
231
+ },
217
232
  });
218
233
  }
219
234
  // Network Policy
@@ -221,6 +236,28 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
221
236
  detailFields.push({
222
237
  label: "Network Policy",
223
238
  value: _jsx(Text, { color: colors.info, children: lp.network_policy_id }),
239
+ action: {
240
+ type: "navigate",
241
+ screen: "network-policy-detail",
242
+ params: { networkPolicyId: lp.network_policy_id },
243
+ hint: "View Policy",
244
+ },
245
+ });
246
+ }
247
+ // Tunnel status - always show when running
248
+ if (devbox.tunnel && devbox.tunnel.tunnel_key) {
249
+ const tunnelKey = devbox.tunnel.tunnel_key;
250
+ const authMode = devbox.tunnel.auth_mode;
251
+ const tunnelUrl = `https://{port}-${tunnelKey}.tunnel.runloop.ai`;
252
+ detailFields.push({
253
+ label: "Tunnel",
254
+ value: (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.success, bold: true, children: [figures.tick, " Active"] }), _jsx(Text, { color: colors.textDim, children: " \u2022 " }), _jsx(Text, { color: colors.success, children: tunnelUrl }), authMode === "authenticated" && (_jsx(Text, { color: colors.warning, children: " (authenticated)" }))] })),
255
+ });
256
+ }
257
+ else if (devbox.status === "running") {
258
+ detailFields.push({
259
+ label: "Tunnel",
260
+ value: (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.cross, " Off"] })),
224
261
  });
225
262
  }
226
263
  // Initiator
@@ -350,6 +387,18 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
350
387
  }
351
388
  lines.push(_jsx(Text, { children: " " }, "launch-space"));
352
389
  }
390
+ // Tunnel Information
391
+ if (devbox.tunnel && devbox.tunnel.tunnel_key) {
392
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Tunnel" }, "tunnel-title"));
393
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Tunnel Key: ", devbox.tunnel.tunnel_key] }, "tunnel-key"));
394
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Auth Mode: ", devbox.tunnel.auth_mode] }, "tunnel-auth"));
395
+ const tunnelUrl = `https://{port}-${devbox.tunnel.tunnel_key}.tunnel.runloop.ai`;
396
+ lines.push(_jsxs(Text, { color: colors.success, children: [" ", "Tunnel URL: ", tunnelUrl] }, "tunnel-url"));
397
+ if (devbox.tunnel.auth_token) {
398
+ lines.push(_jsxs(Text, { color: colors.warning, children: [" ", "Auth Token: ", devbox.tunnel.auth_token] }, "tunnel-token"));
399
+ }
400
+ lines.push(_jsx(Text, { children: " " }, "tunnel-space"));
401
+ }
353
402
  // Source
354
403
  if (devbox.blueprint_id || devbox.snapshot_id) {
355
404
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Source" }, "source-title"));
@@ -0,0 +1,272 @@
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
+ import { validateGatewayConfig } from "../utils/gatewayConfigValidation.js";
15
+ const authTypes = ["bearer", "header"];
16
+ export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) => {
17
+ const isEditing = !!initialConfig?.id;
18
+ const [currentField, setCurrentField] = React.useState("create");
19
+ // Normalize auth type from API to match our options (lowercase)
20
+ const normalizeAuthType = (type) => {
21
+ const normalized = (type || "").toLowerCase();
22
+ if (normalized === "header" || normalized === "bearer") {
23
+ return normalized;
24
+ }
25
+ return "bearer"; // default
26
+ };
27
+ const [formData, setFormData] = React.useState({
28
+ name: initialConfig?.name || "",
29
+ endpoint: initialConfig?.endpoint || "",
30
+ auth_type: normalizeAuthType(initialConfig?.auth_mechanism?.type),
31
+ auth_key: initialConfig?.auth_mechanism?.key || "",
32
+ description: initialConfig?.description || "",
33
+ });
34
+ const [creating, setCreating] = React.useState(false);
35
+ const [result, setResult] = React.useState(null);
36
+ const [error, setError] = React.useState(null);
37
+ const fields = [
38
+ {
39
+ key: "create",
40
+ label: isEditing
41
+ ? "Update AI Gateway Config"
42
+ : "Create AI Gateway Config",
43
+ type: "action",
44
+ },
45
+ {
46
+ key: "name",
47
+ label: "Name (required)",
48
+ type: "text",
49
+ placeholder: "my-gateway",
50
+ },
51
+ {
52
+ key: "endpoint",
53
+ label: "Endpoint URL (required)",
54
+ type: "text",
55
+ placeholder: "https://api.example.com",
56
+ },
57
+ { key: "auth_type", label: "Auth Type", type: "select" },
58
+ {
59
+ key: "auth_key",
60
+ label: "Auth Header Key (for header type)",
61
+ type: "text",
62
+ placeholder: "x-api-key",
63
+ },
64
+ {
65
+ key: "description",
66
+ label: "Description (optional)",
67
+ type: "text",
68
+ placeholder: "Gateway for...",
69
+ },
70
+ ];
71
+ const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
72
+ // Handle Ctrl+C to exit
73
+ useExitOnCtrlC();
74
+ // Select navigation handlers using shared hook
75
+ const handleAuthTypeNav = useFormSelectNavigation(formData.auth_type, authTypes, (value) => {
76
+ setFormData({
77
+ ...formData,
78
+ auth_type: value,
79
+ // Clear auth_key if switching from header to bearer
80
+ auth_key: value !== "header" ? "" : formData.auth_key,
81
+ });
82
+ // If switching away from header and currently on auth_key field, move to next field
83
+ if (value !== "header" && currentField === "auth_key") {
84
+ setCurrentField("description");
85
+ }
86
+ }, currentField === "auth_type");
87
+ // Main form input handler
88
+ useInput((input, key) => {
89
+ // Handle result screen
90
+ if (result) {
91
+ if (input === "q" || key.escape || key.return) {
92
+ if (onCreate) {
93
+ onCreate(result);
94
+ }
95
+ else {
96
+ onBack();
97
+ }
98
+ }
99
+ return;
100
+ }
101
+ // Handle error screen
102
+ if (error) {
103
+ if (input === "r" || key.return) {
104
+ // Retry - clear error and return to form
105
+ setError(null);
106
+ }
107
+ else if (input === "q" || key.escape) {
108
+ // Quit - go back to list
109
+ onBack();
110
+ }
111
+ return;
112
+ }
113
+ // Handle creating state
114
+ if (creating) {
115
+ return;
116
+ }
117
+ // Back to list
118
+ if (input === "q" || key.escape) {
119
+ onBack();
120
+ return;
121
+ }
122
+ // Submit form with Ctrl+S
123
+ if (input === "s" && key.ctrl) {
124
+ handleCreate();
125
+ return;
126
+ }
127
+ // Handle Enter on any field to submit
128
+ if (key.return) {
129
+ handleCreate();
130
+ return;
131
+ }
132
+ // Handle select field navigation using shared hooks
133
+ if (handleAuthTypeNav(input, key))
134
+ return;
135
+ // Navigation (up/down arrows and tab/shift+tab)
136
+ // Skip auth_key field if auth_type is not "header"
137
+ const getNextField = (direction) => {
138
+ let nextIndex = direction === "up" ? currentFieldIndex - 1 : currentFieldIndex + 1;
139
+ while (nextIndex >= 0 && nextIndex < fields.length) {
140
+ const nextField = fields[nextIndex].key;
141
+ // Skip auth_key if auth_type is not header
142
+ if (nextField === "auth_key" && formData.auth_type !== "header") {
143
+ nextIndex = direction === "up" ? nextIndex - 1 : nextIndex + 1;
144
+ continue;
145
+ }
146
+ return nextField;
147
+ }
148
+ return null;
149
+ };
150
+ if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
151
+ const nextField = getNextField("up");
152
+ if (nextField) {
153
+ setCurrentField(nextField);
154
+ }
155
+ return;
156
+ }
157
+ if ((key.downArrow || (key.tab && !key.shift)) &&
158
+ currentFieldIndex < fields.length - 1) {
159
+ const nextField = getNextField("down");
160
+ if (nextField) {
161
+ setCurrentField(nextField);
162
+ }
163
+ return;
164
+ }
165
+ }, { isActive: true });
166
+ const handleCreate = async () => {
167
+ // Validate using shared validation
168
+ const validation = validateGatewayConfig({
169
+ name: formData.name,
170
+ endpoint: formData.endpoint,
171
+ authType: formData.auth_type,
172
+ authKey: formData.auth_key,
173
+ }, { requireName: true, requireEndpoint: true });
174
+ if (!validation.valid) {
175
+ setError(new Error(validation.errors.join("\n")));
176
+ return;
177
+ }
178
+ const { sanitized } = validation;
179
+ setCreating(true);
180
+ setError(null);
181
+ try {
182
+ const client = getClient();
183
+ const authMechanism = {
184
+ type: sanitized.authType,
185
+ };
186
+ if (sanitized.authType === "header" && sanitized.authKey) {
187
+ authMechanism.key = sanitized.authKey;
188
+ }
189
+ let config;
190
+ if (isEditing && initialConfig?.id) {
191
+ // Update existing config
192
+ config = await client.gatewayConfigs.update(initialConfig.id, {
193
+ name: sanitized.name,
194
+ endpoint: sanitized.endpoint,
195
+ auth_mechanism: authMechanism,
196
+ description: formData.description.trim() || undefined,
197
+ });
198
+ }
199
+ else {
200
+ // Create new config
201
+ config = await client.gatewayConfigs.create({
202
+ name: sanitized.name,
203
+ endpoint: sanitized.endpoint,
204
+ auth_mechanism: authMechanism,
205
+ description: formData.description.trim() || undefined,
206
+ });
207
+ }
208
+ setResult(config);
209
+ }
210
+ catch (err) {
211
+ setError(err);
212
+ }
213
+ finally {
214
+ setCreating(false);
215
+ }
216
+ };
217
+ // Result screen
218
+ if (result) {
219
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
220
+ { label: "AI Gateway Configs" },
221
+ { label: isEditing ? "Update" : "Create", active: true },
222
+ ] }), _jsx(SuccessMessage, { message: `AI 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" }] })] }));
223
+ }
224
+ // Error screen
225
+ if (error) {
226
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
227
+ { label: "AI Gateway Configs" },
228
+ { label: isEditing ? "Update" : "Create", active: true },
229
+ ] }), _jsx(ErrorMessage, { message: `Failed to ${isEditing ? "update" : "create"} AI gateway config`, error: error }), _jsx(NavigationTips, { tips: [
230
+ { key: "Enter/r", label: "Retry" },
231
+ { key: "q/esc", label: "Cancel" },
232
+ ] })] }));
233
+ }
234
+ // Creating screen
235
+ if (creating) {
236
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
237
+ { label: "AI Gateway Configs" },
238
+ { label: isEditing ? "Update" : "Create", active: true },
239
+ ] }), _jsx(SpinnerComponent, { message: `${isEditing ? "Updating" : "Creating"} AI gateway config...` })] }));
240
+ }
241
+ // Form screen
242
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
243
+ { label: "AI Gateway Configs" },
244
+ { label: isEditing ? "Update" : "Create", active: true },
245
+ ] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
246
+ const isActive = currentField === field.key;
247
+ const fieldData = formData[field.key];
248
+ if (field.type === "action") {
249
+ return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: `[Enter to ${isEditing ? "update" : "create"}]` }, field.key));
250
+ }
251
+ if (field.type === "text") {
252
+ // Skip auth_key field if auth type is bearer
253
+ if (field.key === "auth_key" && formData.auth_type !== "header") {
254
+ return null;
255
+ }
256
+ 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));
257
+ }
258
+ if (field.type === "select") {
259
+ const value = fieldData;
260
+ return (_jsx(FormSelect, { label: field.label, value: value || "", options: authTypes, onChange: (newValue) => setFormData({
261
+ ...formData,
262
+ [field.key]: newValue,
263
+ // Clear auth_key if switching from header to bearer
264
+ auth_key: newValue !== "header" ? "" : formData.auth_key,
265
+ }), isActive: isActive }, field.key));
266
+ }
267
+ return null;
268
+ }) }), _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: [
269
+ { key: "Enter", label: isEditing ? "Update" : "Create" },
270
+ { key: "q", label: "Cancel" },
271
+ ] })] }));
272
+ };
@@ -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
- const copyToClipboard = async (text) => {
75
- const { spawn } = await import("child_process");
76
- const platform = process.platform;
77
- let command;
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