@runloop/rl-cli 0.0.2 → 0.1.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 +64 -29
- package/dist/cli.js +420 -76
- package/dist/commands/auth.js +12 -10
- package/dist/commands/blueprint/create.js +108 -0
- package/dist/commands/blueprint/get.js +37 -0
- package/dist/commands/blueprint/list.js +303 -224
- package/dist/commands/blueprint/logs.js +40 -0
- package/dist/commands/blueprint/preview.js +45 -0
- package/dist/commands/devbox/create.js +10 -9
- package/dist/commands/devbox/delete.js +8 -8
- package/dist/commands/devbox/download.js +49 -0
- package/dist/commands/devbox/exec.js +23 -13
- package/dist/commands/devbox/execAsync.js +43 -0
- package/dist/commands/devbox/get.js +37 -0
- package/dist/commands/devbox/getAsync.js +37 -0
- package/dist/commands/devbox/list.js +390 -205
- package/dist/commands/devbox/logs.js +40 -0
- package/dist/commands/devbox/read.js +49 -0
- package/dist/commands/devbox/resume.js +37 -0
- package/dist/commands/devbox/rsync.js +118 -0
- package/dist/commands/devbox/scp.js +122 -0
- package/dist/commands/devbox/shutdown.js +37 -0
- package/dist/commands/devbox/ssh.js +104 -0
- package/dist/commands/devbox/suspend.js +37 -0
- package/dist/commands/devbox/tunnel.js +120 -0
- package/dist/commands/devbox/upload.js +10 -10
- package/dist/commands/devbox/write.js +51 -0
- package/dist/commands/mcp-http.js +37 -0
- package/dist/commands/mcp-install.js +120 -0
- package/dist/commands/mcp.js +30 -0
- package/dist/commands/menu.js +70 -0
- package/dist/commands/object/delete.js +37 -0
- package/dist/commands/object/download.js +88 -0
- package/dist/commands/object/get.js +37 -0
- package/dist/commands/object/list.js +112 -0
- package/dist/commands/object/upload.js +130 -0
- package/dist/commands/snapshot/create.js +12 -11
- package/dist/commands/snapshot/delete.js +8 -8
- package/dist/commands/snapshot/list.js +59 -91
- package/dist/commands/snapshot/status.js +37 -0
- package/dist/components/ActionsPopup.js +16 -13
- package/dist/components/Banner.js +5 -8
- package/dist/components/Breadcrumb.js +6 -6
- package/dist/components/DetailView.js +7 -4
- package/dist/components/DevboxActionsMenu.js +347 -189
- package/dist/components/DevboxCard.js +15 -14
- package/dist/components/DevboxCreatePage.js +147 -113
- package/dist/components/DevboxDetailPage.js +182 -103
- package/dist/components/ErrorMessage.js +5 -4
- package/dist/components/Header.js +4 -3
- package/dist/components/MainMenu.js +72 -0
- package/dist/components/MetadataDisplay.js +17 -9
- package/dist/components/OperationsMenu.js +6 -5
- package/dist/components/ResourceActionsMenu.js +117 -0
- package/dist/components/ResourceListView.js +213 -0
- package/dist/components/Spinner.js +5 -4
- package/dist/components/StatusBadge.js +81 -31
- package/dist/components/SuccessMessage.js +4 -3
- package/dist/components/Table.example.js +53 -23
- package/dist/components/Table.js +19 -11
- package/dist/hooks/useCursorPagination.js +125 -0
- package/dist/mcp/server-http.js +416 -0
- package/dist/mcp/server.js +397 -0
- package/dist/utils/CommandExecutor.js +22 -6
- package/dist/utils/client.js +20 -3
- package/dist/utils/config.js +40 -4
- package/dist/utils/interactiveCommand.js +14 -0
- package/dist/utils/output.js +17 -17
- package/dist/utils/ssh.js +160 -0
- package/dist/utils/sshSession.js +29 -0
- package/dist/utils/theme.js +22 -0
- package/dist/utils/url.js +39 -0
- package/package.json +29 -4
|
@@ -1,62 +1,125 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React from
|
|
3
|
-
import { Box, Text, useInput, useApp, useStdout } from
|
|
4
|
-
import TextInput from
|
|
5
|
-
import figures from
|
|
6
|
-
import { getClient } from
|
|
7
|
-
import { Header } from
|
|
8
|
-
import { SpinnerComponent } from
|
|
9
|
-
import { ErrorMessage } from
|
|
10
|
-
import { SuccessMessage } from
|
|
11
|
-
import { Breadcrumb } from
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput, useApp, useStdout } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import figures from "figures";
|
|
6
|
+
import { getClient } from "../utils/client.js";
|
|
7
|
+
import { Header } from "./Header.js";
|
|
8
|
+
import { SpinnerComponent } from "./Spinner.js";
|
|
9
|
+
import { ErrorMessage } from "./ErrorMessage.js";
|
|
10
|
+
import { SuccessMessage } from "./SuccessMessage.js";
|
|
11
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
12
|
+
import { colors } from "../utils/theme.js";
|
|
12
13
|
export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
13
|
-
{ label:
|
|
14
|
-
{ label: devbox.name || devbox.id, active: true }
|
|
15
|
-
], initialOperation, initialOperationIndex = 0, }) => {
|
|
14
|
+
{ label: "Devboxes" },
|
|
15
|
+
{ label: devbox.name || devbox.id, active: true },
|
|
16
|
+
], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, onSSHRequest, }) => {
|
|
16
17
|
const { exit } = useApp();
|
|
17
18
|
const { stdout } = useStdout();
|
|
18
19
|
const [loading, setLoading] = React.useState(false);
|
|
19
20
|
const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
|
|
20
21
|
const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
|
|
21
|
-
const [operationInput, setOperationInput] = React.useState(
|
|
22
|
+
const [operationInput, setOperationInput] = React.useState("");
|
|
22
23
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
23
24
|
const [operationError, setOperationError] = React.useState(null);
|
|
24
|
-
const [logsWrapMode, setLogsWrapMode] = React.useState(
|
|
25
|
+
const [logsWrapMode, setLogsWrapMode] = React.useState(false);
|
|
25
26
|
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
26
27
|
const [execScroll, setExecScroll] = React.useState(0);
|
|
27
28
|
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
28
29
|
const allOperations = [
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
|
|
30
|
+
{
|
|
31
|
+
key: "logs",
|
|
32
|
+
label: "View Logs",
|
|
33
|
+
color: colors.info,
|
|
34
|
+
icon: figures.info,
|
|
35
|
+
shortcut: "l",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "exec",
|
|
39
|
+
label: "Execute Command",
|
|
40
|
+
color: colors.success,
|
|
41
|
+
icon: figures.play,
|
|
42
|
+
shortcut: "e",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "upload",
|
|
46
|
+
label: "Upload File",
|
|
47
|
+
color: colors.success,
|
|
48
|
+
icon: figures.arrowUp,
|
|
49
|
+
shortcut: "u",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "snapshot",
|
|
53
|
+
label: "Create Snapshot",
|
|
54
|
+
color: colors.warning,
|
|
55
|
+
icon: figures.circleFilled,
|
|
56
|
+
shortcut: "n",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "ssh",
|
|
60
|
+
label: "SSH onto the box",
|
|
61
|
+
color: colors.primary,
|
|
62
|
+
icon: figures.arrowRight,
|
|
63
|
+
shortcut: "s",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "tunnel",
|
|
67
|
+
label: "Open Tunnel",
|
|
68
|
+
color: colors.secondary,
|
|
69
|
+
icon: figures.pointerSmall,
|
|
70
|
+
shortcut: "t",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: "suspend",
|
|
74
|
+
label: "Suspend Devbox",
|
|
75
|
+
color: colors.warning,
|
|
76
|
+
icon: figures.squareSmallFilled,
|
|
77
|
+
shortcut: "p",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "resume",
|
|
81
|
+
label: "Resume Devbox",
|
|
82
|
+
color: colors.success,
|
|
83
|
+
icon: figures.play,
|
|
84
|
+
shortcut: "r",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "delete",
|
|
88
|
+
label: "Shutdown Devbox",
|
|
89
|
+
color: colors.error,
|
|
90
|
+
icon: figures.cross,
|
|
91
|
+
shortcut: "d",
|
|
92
|
+
},
|
|
38
93
|
];
|
|
39
94
|
// Filter operations based on devbox status
|
|
40
|
-
const operations = devbox
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
95
|
+
const operations = devbox
|
|
96
|
+
? allOperations.filter((op) => {
|
|
97
|
+
const status = devbox.status;
|
|
98
|
+
// When suspended: logs and resume
|
|
99
|
+
if (status === "suspended") {
|
|
100
|
+
return op.key === "resume" || op.key === "logs";
|
|
101
|
+
}
|
|
102
|
+
// When not running (shutdown, failure, etc): only logs
|
|
103
|
+
if (status !== "running" &&
|
|
104
|
+
status !== "provisioning" &&
|
|
105
|
+
status !== "initializing") {
|
|
106
|
+
return op.key === "logs";
|
|
107
|
+
}
|
|
108
|
+
// When running: everything except resume
|
|
109
|
+
if (status === "running") {
|
|
110
|
+
return op.key !== "resume";
|
|
111
|
+
}
|
|
112
|
+
// Default for transitional states (provisioning, initializing)
|
|
113
|
+
return op.key === "logs" || op.key === "delete";
|
|
114
|
+
})
|
|
115
|
+
: allOperations;
|
|
57
116
|
// Auto-execute operations that don't need input
|
|
58
117
|
React.useEffect(() => {
|
|
59
|
-
|
|
118
|
+
const autoExecuteOps = ["delete", "ssh", "logs", "suspend", "resume"];
|
|
119
|
+
if (executingOperation &&
|
|
120
|
+
autoExecuteOps.includes(executingOperation) &&
|
|
121
|
+
!loading &&
|
|
122
|
+
devbox) {
|
|
60
123
|
executeOperation();
|
|
61
124
|
}
|
|
62
125
|
}, [executingOperation]);
|
|
@@ -66,157 +129,216 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
66
129
|
if (key.return && operationInput.trim()) {
|
|
67
130
|
executeOperation();
|
|
68
131
|
}
|
|
69
|
-
else if (input ===
|
|
132
|
+
else if (input === "q" || key.escape) {
|
|
70
133
|
console.clear();
|
|
71
134
|
setExecutingOperation(null);
|
|
72
|
-
setOperationInput(
|
|
135
|
+
setOperationInput("");
|
|
73
136
|
}
|
|
74
137
|
return;
|
|
75
138
|
}
|
|
76
139
|
// Handle operation result display
|
|
77
140
|
if (operationResult || operationError) {
|
|
78
|
-
if (input ===
|
|
141
|
+
if (input === "q" || key.escape || key.return) {
|
|
79
142
|
console.clear();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
143
|
+
// If skipOperationsMenu is true, go back to parent instead of operations menu
|
|
144
|
+
if (skipOperationsMenu) {
|
|
145
|
+
onBack();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
setOperationResult(null);
|
|
149
|
+
setOperationError(null);
|
|
150
|
+
setExecutingOperation(null);
|
|
151
|
+
setOperationInput("");
|
|
152
|
+
setLogsWrapMode(true);
|
|
153
|
+
setLogsScroll(0);
|
|
154
|
+
setExecScroll(0);
|
|
155
|
+
setCopyStatus(null);
|
|
156
|
+
}
|
|
88
157
|
}
|
|
89
|
-
else if ((key.upArrow || input ===
|
|
158
|
+
else if ((key.upArrow || input === "k") &&
|
|
159
|
+
operationResult &&
|
|
160
|
+
typeof operationResult === "object" &&
|
|
161
|
+
operationResult.__customRender === "exec") {
|
|
90
162
|
setExecScroll(Math.max(0, execScroll - 1));
|
|
91
163
|
}
|
|
92
|
-
else if ((key.downArrow || input ===
|
|
164
|
+
else if ((key.downArrow || input === "j") &&
|
|
165
|
+
operationResult &&
|
|
166
|
+
typeof operationResult === "object" &&
|
|
167
|
+
operationResult.__customRender === "exec") {
|
|
93
168
|
setExecScroll(execScroll + 1);
|
|
94
169
|
}
|
|
95
|
-
else if (key.pageUp &&
|
|
170
|
+
else if (key.pageUp &&
|
|
171
|
+
operationResult &&
|
|
172
|
+
typeof operationResult === "object" &&
|
|
173
|
+
operationResult.__customRender === "exec") {
|
|
96
174
|
setExecScroll(Math.max(0, execScroll - 10));
|
|
97
175
|
}
|
|
98
|
-
else if (key.pageDown &&
|
|
176
|
+
else if (key.pageDown &&
|
|
177
|
+
operationResult &&
|
|
178
|
+
typeof operationResult === "object" &&
|
|
179
|
+
operationResult.__customRender === "exec") {
|
|
99
180
|
setExecScroll(execScroll + 10);
|
|
100
181
|
}
|
|
101
|
-
else if (input ===
|
|
182
|
+
else if (input === "g" &&
|
|
183
|
+
operationResult &&
|
|
184
|
+
typeof operationResult === "object" &&
|
|
185
|
+
operationResult.__customRender === "exec") {
|
|
102
186
|
setExecScroll(0);
|
|
103
187
|
}
|
|
104
|
-
else if (input ===
|
|
105
|
-
|
|
188
|
+
else if (input === "G" &&
|
|
189
|
+
operationResult &&
|
|
190
|
+
typeof operationResult === "object" &&
|
|
191
|
+
operationResult.__customRender === "exec") {
|
|
192
|
+
const lines = [
|
|
193
|
+
...(operationResult.stdout || "").split("\n"),
|
|
194
|
+
...(operationResult.stderr || "").split("\n"),
|
|
195
|
+
];
|
|
106
196
|
const terminalHeight = stdout?.rows || 30;
|
|
107
197
|
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
108
198
|
const maxScroll = Math.max(0, lines.length - viewportHeight);
|
|
109
199
|
setExecScroll(maxScroll);
|
|
110
200
|
}
|
|
111
|
-
else if (input ===
|
|
201
|
+
else if (input === "c" &&
|
|
202
|
+
operationResult &&
|
|
203
|
+
typeof operationResult === "object" &&
|
|
204
|
+
operationResult.__customRender === "exec") {
|
|
112
205
|
// Copy exec output to clipboard
|
|
113
|
-
const output = (operationResult.stdout ||
|
|
206
|
+
const output = (operationResult.stdout || "") +
|
|
207
|
+
(operationResult.stderr || "");
|
|
114
208
|
const copyToClipboard = async (text) => {
|
|
115
|
-
const { spawn } = await import(
|
|
209
|
+
const { spawn } = await import("child_process");
|
|
116
210
|
const platform = process.platform;
|
|
117
211
|
let command;
|
|
118
212
|
let args;
|
|
119
|
-
if (platform ===
|
|
120
|
-
command =
|
|
213
|
+
if (platform === "darwin") {
|
|
214
|
+
command = "pbcopy";
|
|
121
215
|
args = [];
|
|
122
216
|
}
|
|
123
|
-
else if (platform ===
|
|
124
|
-
command =
|
|
217
|
+
else if (platform === "win32") {
|
|
218
|
+
command = "clip";
|
|
125
219
|
args = [];
|
|
126
220
|
}
|
|
127
221
|
else {
|
|
128
|
-
command =
|
|
129
|
-
args = [
|
|
222
|
+
command = "xclip";
|
|
223
|
+
args = ["-selection", "clipboard"];
|
|
130
224
|
}
|
|
131
225
|
const proc = spawn(command, args);
|
|
132
226
|
proc.stdin.write(text);
|
|
133
227
|
proc.stdin.end();
|
|
134
|
-
proc.on(
|
|
228
|
+
proc.on("exit", (code) => {
|
|
135
229
|
if (code === 0) {
|
|
136
|
-
setCopyStatus(
|
|
230
|
+
setCopyStatus("Copied to clipboard!");
|
|
137
231
|
setTimeout(() => setCopyStatus(null), 2000);
|
|
138
232
|
}
|
|
139
233
|
else {
|
|
140
|
-
setCopyStatus(
|
|
234
|
+
setCopyStatus("Failed to copy");
|
|
141
235
|
setTimeout(() => setCopyStatus(null), 2000);
|
|
142
236
|
}
|
|
143
237
|
});
|
|
144
|
-
proc.on(
|
|
145
|
-
setCopyStatus(
|
|
238
|
+
proc.on("error", () => {
|
|
239
|
+
setCopyStatus("Copy not supported");
|
|
146
240
|
setTimeout(() => setCopyStatus(null), 2000);
|
|
147
241
|
});
|
|
148
242
|
};
|
|
149
243
|
copyToClipboard(output);
|
|
150
244
|
}
|
|
151
|
-
else if ((key.upArrow || input ===
|
|
245
|
+
else if ((key.upArrow || input === "k") &&
|
|
246
|
+
operationResult &&
|
|
247
|
+
typeof operationResult === "object" &&
|
|
248
|
+
operationResult.__customRender === "logs") {
|
|
152
249
|
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
153
250
|
}
|
|
154
|
-
else if ((key.downArrow || input ===
|
|
251
|
+
else if ((key.downArrow || input === "j") &&
|
|
252
|
+
operationResult &&
|
|
253
|
+
typeof operationResult === "object" &&
|
|
254
|
+
operationResult.__customRender === "logs") {
|
|
155
255
|
setLogsScroll(logsScroll + 1);
|
|
156
256
|
}
|
|
157
|
-
else if (key.pageUp &&
|
|
257
|
+
else if (key.pageUp &&
|
|
258
|
+
operationResult &&
|
|
259
|
+
typeof operationResult === "object" &&
|
|
260
|
+
operationResult.__customRender === "logs") {
|
|
158
261
|
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
159
262
|
}
|
|
160
|
-
else if (key.pageDown &&
|
|
263
|
+
else if (key.pageDown &&
|
|
264
|
+
operationResult &&
|
|
265
|
+
typeof operationResult === "object" &&
|
|
266
|
+
operationResult.__customRender === "logs") {
|
|
161
267
|
setLogsScroll(logsScroll + 10);
|
|
162
268
|
}
|
|
163
|
-
else if (input ===
|
|
269
|
+
else if (input === "g" &&
|
|
270
|
+
operationResult &&
|
|
271
|
+
typeof operationResult === "object" &&
|
|
272
|
+
operationResult.__customRender === "logs") {
|
|
164
273
|
setLogsScroll(0);
|
|
165
274
|
}
|
|
166
|
-
else if (input ===
|
|
275
|
+
else if (input === "G" &&
|
|
276
|
+
operationResult &&
|
|
277
|
+
typeof operationResult === "object" &&
|
|
278
|
+
operationResult.__customRender === "logs") {
|
|
167
279
|
const logs = operationResult.__logs || [];
|
|
168
280
|
const terminalHeight = stdout?.rows || 30;
|
|
169
281
|
const viewportHeight = Math.max(10, terminalHeight - 10);
|
|
170
282
|
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
171
283
|
setLogsScroll(maxScroll);
|
|
172
284
|
}
|
|
173
|
-
else if (input ===
|
|
285
|
+
else if (input === "w" &&
|
|
286
|
+
operationResult &&
|
|
287
|
+
typeof operationResult === "object" &&
|
|
288
|
+
operationResult.__customRender === "logs") {
|
|
174
289
|
setLogsWrapMode(!logsWrapMode);
|
|
175
290
|
}
|
|
176
|
-
else if (input ===
|
|
291
|
+
else if (input === "c" &&
|
|
292
|
+
operationResult &&
|
|
293
|
+
typeof operationResult === "object" &&
|
|
294
|
+
operationResult.__customRender === "logs") {
|
|
177
295
|
// Copy logs to clipboard
|
|
178
296
|
const logs = operationResult.__logs || [];
|
|
179
|
-
const logsText = logs
|
|
297
|
+
const logsText = logs
|
|
298
|
+
.map((log) => {
|
|
180
299
|
const time = new Date(log.timestamp_ms).toLocaleString();
|
|
181
|
-
const level = log.level ||
|
|
182
|
-
const source = log.source ||
|
|
183
|
-
const message = log.message ||
|
|
184
|
-
const cmd = log.cmd ? `[${log.cmd}] ` :
|
|
185
|
-
const exitCode = log.exit_code !== null && log.exit_code !== undefined
|
|
300
|
+
const level = log.level || "INFO";
|
|
301
|
+
const source = log.source || "exec";
|
|
302
|
+
const message = log.message || "";
|
|
303
|
+
const cmd = log.cmd ? `[${log.cmd}] ` : "";
|
|
304
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined
|
|
305
|
+
? `(${log.exit_code}) `
|
|
306
|
+
: "";
|
|
186
307
|
return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
|
|
187
|
-
})
|
|
308
|
+
})
|
|
309
|
+
.join("\n");
|
|
188
310
|
const copyToClipboard = async (text) => {
|
|
189
|
-
const { spawn } = await import(
|
|
311
|
+
const { spawn } = await import("child_process");
|
|
190
312
|
const platform = process.platform;
|
|
191
313
|
let command;
|
|
192
314
|
let args;
|
|
193
|
-
if (platform ===
|
|
194
|
-
command =
|
|
315
|
+
if (platform === "darwin") {
|
|
316
|
+
command = "pbcopy";
|
|
195
317
|
args = [];
|
|
196
318
|
}
|
|
197
|
-
else if (platform ===
|
|
198
|
-
command =
|
|
319
|
+
else if (platform === "win32") {
|
|
320
|
+
command = "clip";
|
|
199
321
|
args = [];
|
|
200
322
|
}
|
|
201
323
|
else {
|
|
202
|
-
command =
|
|
203
|
-
args = [
|
|
324
|
+
command = "xclip";
|
|
325
|
+
args = ["-selection", "clipboard"];
|
|
204
326
|
}
|
|
205
327
|
const proc = spawn(command, args);
|
|
206
328
|
proc.stdin.write(text);
|
|
207
329
|
proc.stdin.end();
|
|
208
|
-
proc.on(
|
|
330
|
+
proc.on("exit", (code) => {
|
|
209
331
|
if (code === 0) {
|
|
210
|
-
setCopyStatus(
|
|
332
|
+
setCopyStatus("Copied to clipboard!");
|
|
211
333
|
setTimeout(() => setCopyStatus(null), 2000);
|
|
212
334
|
}
|
|
213
335
|
else {
|
|
214
|
-
setCopyStatus(
|
|
336
|
+
setCopyStatus("Failed to copy");
|
|
215
337
|
setTimeout(() => setCopyStatus(null), 2000);
|
|
216
338
|
}
|
|
217
339
|
});
|
|
218
|
-
proc.on(
|
|
219
|
-
setCopyStatus(
|
|
340
|
+
proc.on("error", () => {
|
|
341
|
+
setCopyStatus("Copy not supported");
|
|
220
342
|
setTimeout(() => setCopyStatus(null), 2000);
|
|
221
343
|
});
|
|
222
344
|
};
|
|
@@ -225,7 +347,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
225
347
|
return;
|
|
226
348
|
}
|
|
227
349
|
// Operations selection mode
|
|
228
|
-
if (input ===
|
|
350
|
+
if (input === "q" || key.escape) {
|
|
229
351
|
console.clear();
|
|
230
352
|
onBack();
|
|
231
353
|
setSelectedOperation(0);
|
|
@@ -243,7 +365,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
243
365
|
}
|
|
244
366
|
else if (input) {
|
|
245
367
|
// Check if input matches any operation shortcut
|
|
246
|
-
const matchedOp = operations.find(op => op.shortcut === input);
|
|
368
|
+
const matchedOp = operations.find((op) => op.shortcut === input);
|
|
247
369
|
if (matchedOp) {
|
|
248
370
|
console.clear();
|
|
249
371
|
setExecutingOperation(matchedOp.key);
|
|
@@ -255,90 +377,104 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
255
377
|
try {
|
|
256
378
|
setLoading(true);
|
|
257
379
|
switch (executingOperation) {
|
|
258
|
-
case
|
|
380
|
+
case "exec":
|
|
259
381
|
const execResult = await client.devboxes.executeSync(devbox.id, {
|
|
260
382
|
command: operationInput,
|
|
261
383
|
});
|
|
262
384
|
// Format exec result for custom rendering
|
|
263
385
|
const formattedExecResult = {
|
|
264
|
-
__customRender:
|
|
386
|
+
__customRender: "exec",
|
|
265
387
|
command: operationInput,
|
|
266
|
-
stdout: execResult.stdout ||
|
|
267
|
-
stderr: execResult.stderr ||
|
|
388
|
+
stdout: execResult.stdout || "",
|
|
389
|
+
stderr: execResult.stderr || "",
|
|
268
390
|
exitCode: execResult.exit_code ?? 0,
|
|
269
391
|
};
|
|
270
392
|
setOperationResult(formattedExecResult);
|
|
271
393
|
break;
|
|
272
|
-
case
|
|
273
|
-
const fs = await import(
|
|
394
|
+
case "upload":
|
|
395
|
+
const fs = await import("fs");
|
|
274
396
|
const fileStream = fs.createReadStream(operationInput);
|
|
275
|
-
const filename = operationInput.split(
|
|
397
|
+
const filename = operationInput.split("/").pop() || "file";
|
|
276
398
|
await client.devboxes.uploadFile(devbox.id, {
|
|
277
399
|
path: filename,
|
|
278
400
|
file: fileStream,
|
|
279
401
|
});
|
|
280
402
|
setOperationResult(`File ${filename} uploaded successfully`);
|
|
281
403
|
break;
|
|
282
|
-
case
|
|
404
|
+
case "snapshot":
|
|
283
405
|
const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
|
|
284
406
|
name: operationInput || `snapshot-${Date.now()}`,
|
|
285
407
|
});
|
|
286
408
|
setOperationResult(`Snapshot created: ${snapshot.id}`);
|
|
287
409
|
break;
|
|
288
|
-
case
|
|
410
|
+
case "ssh":
|
|
289
411
|
const sshKey = await client.devboxes.createSSHKey(devbox.id);
|
|
290
|
-
const fsModule = await import(
|
|
291
|
-
const pathModule = await import(
|
|
292
|
-
const osModule = await import(
|
|
293
|
-
const sshDir = pathModule.join(osModule.homedir(),
|
|
412
|
+
const fsModule = await import("fs");
|
|
413
|
+
const pathModule = await import("path");
|
|
414
|
+
const osModule = await import("os");
|
|
415
|
+
const sshDir = pathModule.join(osModule.homedir(), ".runloop", "ssh_keys");
|
|
294
416
|
fsModule.mkdirSync(sshDir, { recursive: true });
|
|
295
417
|
const keyPath = pathModule.join(sshDir, `${devbox.id}.pem`);
|
|
296
|
-
fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
418
|
+
fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, {
|
|
419
|
+
mode: 0o600,
|
|
420
|
+
});
|
|
421
|
+
const sshUser = devbox.launch_parameters?.user_parameters?.username || "user";
|
|
422
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
423
|
+
const sshHost = env === "dev" ? "ssh.runloop.pro" : "ssh.runloop.ai";
|
|
424
|
+
const proxyCommand = `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshHost}:443 2>/dev/null`;
|
|
425
|
+
const sshConfig = {
|
|
300
426
|
keyPath,
|
|
301
427
|
proxyCommand,
|
|
302
428
|
sshUser,
|
|
303
429
|
url: sshKey.url,
|
|
304
|
-
|
|
430
|
+
devboxId: devbox.id,
|
|
431
|
+
devboxName: devbox.name || devbox.id,
|
|
305
432
|
};
|
|
306
|
-
|
|
433
|
+
// Notify parent that SSH is requested
|
|
434
|
+
if (onSSHRequest) {
|
|
435
|
+
onSSHRequest(sshConfig);
|
|
436
|
+
exit();
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
setOperationError(new Error("SSH session handler not configured"));
|
|
440
|
+
}
|
|
307
441
|
break;
|
|
308
|
-
case
|
|
442
|
+
case "logs":
|
|
309
443
|
const logsResult = await client.devboxes.logs.list(devbox.id);
|
|
310
444
|
if (logsResult.logs.length === 0) {
|
|
311
|
-
setOperationResult(
|
|
445
|
+
setOperationResult("No logs available for this devbox.");
|
|
312
446
|
}
|
|
313
447
|
else {
|
|
314
|
-
logsResult.__customRender =
|
|
448
|
+
logsResult.__customRender = "logs";
|
|
315
449
|
logsResult.__logs = logsResult.logs;
|
|
316
450
|
logsResult.__totalCount = logsResult.logs.length;
|
|
317
451
|
setOperationResult(logsResult);
|
|
318
452
|
}
|
|
319
453
|
break;
|
|
320
|
-
case
|
|
454
|
+
case "tunnel":
|
|
321
455
|
const port = parseInt(operationInput);
|
|
322
456
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
323
|
-
setOperationError(new Error(
|
|
457
|
+
setOperationError(new Error("Invalid port number. Please enter a port between 1 and 65535."));
|
|
324
458
|
}
|
|
325
459
|
else {
|
|
326
|
-
const tunnel = await client.devboxes.createTunnel(devbox.id, {
|
|
460
|
+
const tunnel = await client.devboxes.createTunnel(devbox.id, {
|
|
461
|
+
port,
|
|
462
|
+
});
|
|
327
463
|
setOperationResult(`Tunnel created!\n\n` +
|
|
328
464
|
`Local Port: ${port}\n` +
|
|
329
465
|
`Public URL: ${tunnel.url}\n\n` +
|
|
330
466
|
`You can now access port ${port} on the devbox via:\n${tunnel.url}`);
|
|
331
467
|
}
|
|
332
468
|
break;
|
|
333
|
-
case
|
|
469
|
+
case "suspend":
|
|
334
470
|
await client.devboxes.suspend(devbox.id);
|
|
335
471
|
setOperationResult(`Devbox ${devbox.id} suspended successfully`);
|
|
336
472
|
break;
|
|
337
|
-
case
|
|
473
|
+
case "resume":
|
|
338
474
|
await client.devboxes.resume(devbox.id);
|
|
339
475
|
setOperationResult(`Devbox ${devbox.id} resumed successfully`);
|
|
340
476
|
break;
|
|
341
|
-
case
|
|
477
|
+
case "delete":
|
|
342
478
|
await client.devboxes.shutdown(devbox.id);
|
|
343
479
|
setOperationResult(`Devbox ${devbox.id} shut down successfully`);
|
|
344
480
|
break;
|
|
@@ -351,18 +487,20 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
351
487
|
setLoading(false);
|
|
352
488
|
}
|
|
353
489
|
};
|
|
354
|
-
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
490
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label || "Operation";
|
|
355
491
|
// Operation result display
|
|
356
492
|
if (operationResult || operationError) {
|
|
357
493
|
// Check for custom exec rendering
|
|
358
|
-
if (operationResult &&
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
494
|
+
if (operationResult &&
|
|
495
|
+
typeof operationResult === "object" &&
|
|
496
|
+
operationResult.__customRender === "exec") {
|
|
497
|
+
const command = operationResult.command || "";
|
|
498
|
+
const stdout = operationResult.stdout || "";
|
|
499
|
+
const stderr = operationResult.stderr || "";
|
|
362
500
|
const exitCode = operationResult.exitCode;
|
|
363
|
-
const stdoutLines = stdout ? stdout.split(
|
|
364
|
-
const stderrLines = stderr ? stderr.split(
|
|
365
|
-
const allLines = [...stdoutLines, ...stderrLines].filter(line => line !==
|
|
501
|
+
const stdoutLines = stdout ? stdout.split("\n") : [];
|
|
502
|
+
const stderrLines = stderr ? stderr.split("\n") : [];
|
|
503
|
+
const allLines = [...stdoutLines, ...stderrLines].filter((line) => line !== "");
|
|
366
504
|
const terminalHeight = stdout?.rows || 30;
|
|
367
505
|
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
368
506
|
const maxScroll = Math.max(0, allLines.length - viewportHeight);
|
|
@@ -370,16 +508,21 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
370
508
|
const visibleLines = allLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
371
509
|
const hasMore = actualScroll + viewportHeight < allLines.length;
|
|
372
510
|
const hasLess = actualScroll > 0;
|
|
373
|
-
const exitCodeColor = exitCode === 0 ?
|
|
374
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
511
|
+
const exitCodeColor = exitCode === 0 ? colors.success : colors.error;
|
|
512
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
513
|
+
...breadcrumbItems,
|
|
514
|
+
{ label: "Execute Command", active: true },
|
|
515
|
+
] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Command:"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.text, children: command })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Exit Code:", " "] }), _jsx(Text, { color: exitCodeColor, bold: true, children: exitCode })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: [allLines.length === 0 && (_jsx(Text, { color: colors.textDim, dimColor: true, children: "No output" })), visibleLines.map((line, index) => {
|
|
375
516
|
const actualIndex = actualScroll + index;
|
|
376
517
|
const isStderr = actualIndex >= stdoutLines.length;
|
|
377
|
-
const lineColor = isStderr ?
|
|
518
|
+
const lineColor = isStderr ? colors.error : colors.text;
|
|
378
519
|
return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
|
|
379
|
-
}), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color:
|
|
520
|
+
}), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.primary, children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: colors.primary, children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "lines"] }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of", " ", allLines.length] })] })), stdout && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.success, dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.error, dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
380
521
|
}
|
|
381
522
|
// Check for custom logs rendering
|
|
382
|
-
if (operationResult &&
|
|
523
|
+
if (operationResult &&
|
|
524
|
+
typeof operationResult === "object" &&
|
|
525
|
+
operationResult.__customRender === "logs") {
|
|
383
526
|
const logs = operationResult.__logs || [];
|
|
384
527
|
const totalCount = operationResult.__totalCount || 0;
|
|
385
528
|
const terminalHeight = stdout?.rows || 30;
|
|
@@ -390,71 +533,86 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
390
533
|
const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
|
|
391
534
|
const hasMore = actualScroll + viewportHeight < logs.length;
|
|
392
535
|
const hasLess = actualScroll > 0;
|
|
393
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label:
|
|
536
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: "Logs", active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: [visibleLogs.map((log, index) => {
|
|
394
537
|
const time = new Date(log.timestamp_ms).toLocaleTimeString();
|
|
395
|
-
const level = log.level ? log.level[0].toUpperCase() :
|
|
396
|
-
const source = log.source ? log.source.substring(0, 8) :
|
|
397
|
-
const fullMessage = log.message ||
|
|
398
|
-
const cmd = log.cmd
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
levelColor =
|
|
538
|
+
const level = log.level ? log.level[0].toUpperCase() : "I";
|
|
539
|
+
const source = log.source ? log.source.substring(0, 8) : "exec";
|
|
540
|
+
const fullMessage = log.message || "";
|
|
541
|
+
const cmd = log.cmd
|
|
542
|
+
? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? "..." : ""}] `
|
|
543
|
+
: "";
|
|
544
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined
|
|
545
|
+
? `(${log.exit_code}) `
|
|
546
|
+
: "";
|
|
547
|
+
let levelColor = colors.textDim;
|
|
548
|
+
if (level === "E")
|
|
549
|
+
levelColor = colors.error;
|
|
550
|
+
else if (level === "W")
|
|
551
|
+
levelColor = colors.warning;
|
|
552
|
+
else if (level === "I")
|
|
553
|
+
levelColor = colors.primary;
|
|
407
554
|
if (logsWrapMode) {
|
|
408
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color:
|
|
555
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: colors.warning, children: exitCode }), cmd && (_jsx(Text, { color: colors.info, dimColor: true, children: cmd })), _jsx(Text, { children: fullMessage })] }, index));
|
|
409
556
|
}
|
|
410
557
|
else {
|
|
411
558
|
const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
|
|
412
559
|
const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
|
|
413
560
|
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
414
|
-
? fullMessage.substring(0, availableMessageWidth - 3) +
|
|
561
|
+
? fullMessage.substring(0, availableMessageWidth - 3) +
|
|
562
|
+
"..."
|
|
415
563
|
: fullMessage;
|
|
416
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color:
|
|
564
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: colors.warning, children: exitCode }), cmd && (_jsx(Text, { color: colors.info, dimColor: true, children: cmd })), _jsx(Text, { children: truncatedMessage })] }, index));
|
|
417
565
|
}
|
|
418
|
-
}), hasLess && (_jsx(Box, { children: _jsxs(Text, { color:
|
|
566
|
+
}), hasLess && (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total logs"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of", " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
419
567
|
}
|
|
420
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && _jsx(ErrorMessage, { message: "Operation failed", error: operationError }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color:
|
|
568
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
|
|
421
569
|
}
|
|
422
570
|
// Operation input mode
|
|
423
571
|
if (executingOperation && devbox) {
|
|
424
|
-
const needsInput = executingOperation ===
|
|
425
|
-
executingOperation ===
|
|
426
|
-
executingOperation ===
|
|
427
|
-
executingOperation ===
|
|
572
|
+
const needsInput = executingOperation === "exec" ||
|
|
573
|
+
executingOperation === "upload" ||
|
|
574
|
+
executingOperation === "snapshot" ||
|
|
575
|
+
executingOperation === "tunnel";
|
|
428
576
|
if (loading) {
|
|
429
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
577
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
578
|
+
...breadcrumbItems,
|
|
579
|
+
{ label: operationLabel, active: true },
|
|
580
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
430
581
|
}
|
|
431
582
|
if (!needsInput) {
|
|
432
583
|
const messages = {
|
|
433
|
-
ssh:
|
|
434
|
-
logs:
|
|
435
|
-
suspend:
|
|
436
|
-
resume:
|
|
437
|
-
delete:
|
|
584
|
+
ssh: "Creating SSH key...",
|
|
585
|
+
logs: "Fetching logs...",
|
|
586
|
+
suspend: "Suspending devbox...",
|
|
587
|
+
resume: "Resuming devbox...",
|
|
588
|
+
delete: "Shutting down devbox...",
|
|
438
589
|
};
|
|
439
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
590
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
591
|
+
...breadcrumbItems,
|
|
592
|
+
{ label: operationLabel, active: true },
|
|
593
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
|
|
440
594
|
}
|
|
441
595
|
const prompts = {
|
|
442
|
-
exec:
|
|
443
|
-
upload:
|
|
444
|
-
snapshot:
|
|
445
|
-
tunnel:
|
|
596
|
+
exec: "Command to execute:",
|
|
597
|
+
upload: "File path to upload:",
|
|
598
|
+
snapshot: "Snapshot name (optional):",
|
|
599
|
+
tunnel: "Port number to expose:",
|
|
446
600
|
};
|
|
447
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color:
|
|
448
|
-
?
|
|
449
|
-
: executingOperation ===
|
|
450
|
-
?
|
|
451
|
-
: executingOperation ===
|
|
452
|
-
?
|
|
453
|
-
:
|
|
601
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: devbox.name || devbox.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === "exec"
|
|
602
|
+
? "ls -la"
|
|
603
|
+
: executingOperation === "upload"
|
|
604
|
+
? "/path/to/file"
|
|
605
|
+
: executingOperation === "tunnel"
|
|
606
|
+
? "8080"
|
|
607
|
+
: "my-snapshot" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
608
|
+
}
|
|
609
|
+
// Operations selection mode - only show if not skipping
|
|
610
|
+
if (!skipOperationsMenu || !executingOperation) {
|
|
611
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
612
|
+
const isSelected = index === selectedOperation;
|
|
613
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
|
|
614
|
+
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
|
|
454
615
|
}
|
|
455
|
-
//
|
|
456
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }),
|
|
457
|
-
const isSelected = index === selectedOperation;
|
|
458
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: [isSelected ? figures.pointer : ' ', " "] }), _jsxs(Text, { color: isSelected ? op.color : 'gray', bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: "gray", dimColor: true, children: [" [", op.shortcut, "]"] })] }, op.key));
|
|
459
|
-
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
|
|
616
|
+
// If skipOperationsMenu is true and executingOperation is set, show loading while it executes
|
|
617
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
460
618
|
};
|