@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.
- 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/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +23 -3
- 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/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +220 -0
- package/dist/components/SecretCreatePage.js +2 -4
- package/dist/components/SettingsMenu.js +12 -2
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +9 -2
- 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 +79 -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 +6 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
- package/dist/screens/BenchmarkRunListScreen.js +21 -1
- 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/ScenarioRunDetailScreen.js +6 -0
- package/dist/screens/SettingsMenuScreen.js +3 -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 +47 -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 +63 -0
- package/dist/store/gatewayConfigStore.js +83 -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/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
|