@runloop/rl-cli 0.1.2 → 0.3.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 +54 -10
- package/dist/cli.js +79 -72
- package/dist/commands/auth.js +2 -2
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +278 -230
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/config.js +118 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +46 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +37 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +12 -10
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +26 -67
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +64 -39
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +11 -48
- package/dist/components/DevboxActionsMenu.js +117 -207
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +104 -0
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +37 -33
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +15 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +71 -7
- package/dist/router/Router.js +70 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +101 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/client.js +4 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +208 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +153 -61
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +61 -0
- package/dist/utils/ssh.js +6 -3
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +185 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +162 -13
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +19 -17
|
@@ -1,31 +1,62 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Box, Text, useInput
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import figures from "figures";
|
|
6
|
-
import { getClient } from "../utils/client.js";
|
|
7
6
|
import { Header } from "./Header.js";
|
|
8
7
|
import { SpinnerComponent } from "./Spinner.js";
|
|
9
8
|
import { ErrorMessage } from "./ErrorMessage.js";
|
|
10
9
|
import { SuccessMessage } from "./SuccessMessage.js";
|
|
11
10
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
12
11
|
import { colors } from "../utils/theme.js";
|
|
12
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
13
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
14
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
15
|
+
import { getDevboxLogs, execCommand, suspendDevbox, resumeDevbox, shutdownDevbox, uploadFile, createSnapshot as createDevboxSnapshot, createTunnel, createSSHKey, } from "../services/devboxService.js";
|
|
16
|
+
import { LogsViewer } from "./LogsViewer.js";
|
|
13
17
|
export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
14
18
|
{ label: "Devboxes" },
|
|
15
19
|
{ label: devbox.name || devbox.id, active: true },
|
|
16
|
-
], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false,
|
|
17
|
-
const {
|
|
18
|
-
const { stdout } = useStdout();
|
|
20
|
+
], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, }) => {
|
|
21
|
+
const { navigate, currentScreen, params } = useNavigation();
|
|
19
22
|
const [loading, setLoading] = React.useState(false);
|
|
20
23
|
const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
|
|
21
24
|
const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
|
|
22
25
|
const [operationInput, setOperationInput] = React.useState("");
|
|
23
26
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
24
27
|
const [operationError, setOperationError] = React.useState(null);
|
|
25
|
-
const [logsWrapMode, setLogsWrapMode] = React.useState(false);
|
|
26
|
-
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
27
28
|
const [execScroll, setExecScroll] = React.useState(0);
|
|
28
29
|
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
30
|
+
// Calculate viewport for exec output:
|
|
31
|
+
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
32
|
+
// - Command header (border + 2 content + border + marginBottom): 5 lines
|
|
33
|
+
// - Output box borders: 2 lines
|
|
34
|
+
// - Stats bar (marginTop + content): 2 lines
|
|
35
|
+
// - Help bar (marginTop + content): 2 lines
|
|
36
|
+
// - Safety buffer: 1 line
|
|
37
|
+
// Total: 16 lines
|
|
38
|
+
const execViewport = useViewportHeight({ overhead: 16, minHeight: 10 });
|
|
39
|
+
// CRITICAL: Aggressive memory cleanup to prevent heap exhaustion
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
// Clear large data immediately when results are shown to free memory faster
|
|
42
|
+
if (operationResult || operationError) {
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
// After 100ms, if user hasn't acted, start aggressive cleanup
|
|
45
|
+
// This helps with memory without disrupting UX
|
|
46
|
+
}, 100);
|
|
47
|
+
return () => clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}, [operationResult, operationError]);
|
|
50
|
+
// Cleanup on unmount
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
return () => {
|
|
53
|
+
// Aggressively null out all large data structures
|
|
54
|
+
setOperationResult(null);
|
|
55
|
+
setOperationError(null);
|
|
56
|
+
setOperationInput("");
|
|
57
|
+
setLoading(false);
|
|
58
|
+
};
|
|
59
|
+
}, []);
|
|
29
60
|
const allOperations = [
|
|
30
61
|
{
|
|
31
62
|
key: "logs",
|
|
@@ -123,6 +154,8 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
123
154
|
executeOperation();
|
|
124
155
|
}
|
|
125
156
|
}, [executingOperation]);
|
|
157
|
+
// Handle Ctrl+C to exit
|
|
158
|
+
useExitOnCtrlC();
|
|
126
159
|
useInput((input, key) => {
|
|
127
160
|
// Handle operation input mode
|
|
128
161
|
if (executingOperation && !operationResult && !operationError) {
|
|
@@ -130,7 +163,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
130
163
|
executeOperation();
|
|
131
164
|
}
|
|
132
165
|
else if (input === "q" || key.escape) {
|
|
133
|
-
console.clear();
|
|
134
166
|
setExecutingOperation(null);
|
|
135
167
|
setOperationInput("");
|
|
136
168
|
}
|
|
@@ -139,20 +171,19 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
139
171
|
// Handle operation result display
|
|
140
172
|
if (operationResult || operationError) {
|
|
141
173
|
if (input === "q" || key.escape || key.return) {
|
|
142
|
-
|
|
174
|
+
// Clear large data structures immediately to prevent memory leaks
|
|
175
|
+
setOperationResult(null);
|
|
176
|
+
setOperationError(null);
|
|
177
|
+
setOperationInput("");
|
|
178
|
+
setExecScroll(0);
|
|
179
|
+
setCopyStatus(null);
|
|
143
180
|
// If skipOperationsMenu is true, go back to parent instead of operations menu
|
|
144
181
|
if (skipOperationsMenu) {
|
|
182
|
+
setExecutingOperation(null);
|
|
145
183
|
onBack();
|
|
146
184
|
}
|
|
147
185
|
else {
|
|
148
|
-
setOperationResult(null);
|
|
149
|
-
setOperationError(null);
|
|
150
186
|
setExecutingOperation(null);
|
|
151
|
-
setOperationInput("");
|
|
152
|
-
setLogsWrapMode(true);
|
|
153
|
-
setLogsScroll(0);
|
|
154
|
-
setExecScroll(0);
|
|
155
|
-
setCopyStatus(null);
|
|
156
187
|
}
|
|
157
188
|
}
|
|
158
189
|
else if ((key.upArrow || input === "k") &&
|
|
@@ -193,9 +224,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
193
224
|
...(operationResult.stdout || "").split("\n"),
|
|
194
225
|
...(operationResult.stderr || "").split("\n"),
|
|
195
226
|
];
|
|
196
|
-
const
|
|
197
|
-
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
198
|
-
const maxScroll = Math.max(0, lines.length - viewportHeight);
|
|
227
|
+
const maxScroll = Math.max(0, lines.length - execViewport.viewportHeight);
|
|
199
228
|
setExecScroll(maxScroll);
|
|
200
229
|
}
|
|
201
230
|
else if (input === "c" &&
|
|
@@ -242,115 +271,18 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
242
271
|
};
|
|
243
272
|
copyToClipboard(output);
|
|
244
273
|
}
|
|
245
|
-
else if ((key.upArrow || input === "k") &&
|
|
246
|
-
operationResult &&
|
|
247
|
-
typeof operationResult === "object" &&
|
|
248
|
-
operationResult.__customRender === "logs") {
|
|
249
|
-
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
250
|
-
}
|
|
251
|
-
else if ((key.downArrow || input === "j") &&
|
|
252
|
-
operationResult &&
|
|
253
|
-
typeof operationResult === "object" &&
|
|
254
|
-
operationResult.__customRender === "logs") {
|
|
255
|
-
setLogsScroll(logsScroll + 1);
|
|
256
|
-
}
|
|
257
|
-
else if (key.pageUp &&
|
|
258
|
-
operationResult &&
|
|
259
|
-
typeof operationResult === "object" &&
|
|
260
|
-
operationResult.__customRender === "logs") {
|
|
261
|
-
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
262
|
-
}
|
|
263
|
-
else if (key.pageDown &&
|
|
264
|
-
operationResult &&
|
|
265
|
-
typeof operationResult === "object" &&
|
|
266
|
-
operationResult.__customRender === "logs") {
|
|
267
|
-
setLogsScroll(logsScroll + 10);
|
|
268
|
-
}
|
|
269
|
-
else if (input === "g" &&
|
|
270
|
-
operationResult &&
|
|
271
|
-
typeof operationResult === "object" &&
|
|
272
|
-
operationResult.__customRender === "logs") {
|
|
273
|
-
setLogsScroll(0);
|
|
274
|
-
}
|
|
275
|
-
else if (input === "G" &&
|
|
276
|
-
operationResult &&
|
|
277
|
-
typeof operationResult === "object" &&
|
|
278
|
-
operationResult.__customRender === "logs") {
|
|
279
|
-
const logs = operationResult.__logs || [];
|
|
280
|
-
const terminalHeight = stdout?.rows || 30;
|
|
281
|
-
const viewportHeight = Math.max(10, terminalHeight - 10);
|
|
282
|
-
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
283
|
-
setLogsScroll(maxScroll);
|
|
284
|
-
}
|
|
285
|
-
else if (input === "w" &&
|
|
286
|
-
operationResult &&
|
|
287
|
-
typeof operationResult === "object" &&
|
|
288
|
-
operationResult.__customRender === "logs") {
|
|
289
|
-
setLogsWrapMode(!logsWrapMode);
|
|
290
|
-
}
|
|
291
|
-
else if (input === "c" &&
|
|
292
|
-
operationResult &&
|
|
293
|
-
typeof operationResult === "object" &&
|
|
294
|
-
operationResult.__customRender === "logs") {
|
|
295
|
-
// Copy logs to clipboard
|
|
296
|
-
const logs = operationResult.__logs || [];
|
|
297
|
-
const logsText = logs
|
|
298
|
-
.map((log) => {
|
|
299
|
-
const time = new Date(log.timestamp_ms).toLocaleString();
|
|
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
|
-
: "";
|
|
307
|
-
return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
|
|
308
|
-
})
|
|
309
|
-
.join("\n");
|
|
310
|
-
const copyToClipboard = async (text) => {
|
|
311
|
-
const { spawn } = await import("child_process");
|
|
312
|
-
const platform = process.platform;
|
|
313
|
-
let command;
|
|
314
|
-
let args;
|
|
315
|
-
if (platform === "darwin") {
|
|
316
|
-
command = "pbcopy";
|
|
317
|
-
args = [];
|
|
318
|
-
}
|
|
319
|
-
else if (platform === "win32") {
|
|
320
|
-
command = "clip";
|
|
321
|
-
args = [];
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
command = "xclip";
|
|
325
|
-
args = ["-selection", "clipboard"];
|
|
326
|
-
}
|
|
327
|
-
const proc = spawn(command, args);
|
|
328
|
-
proc.stdin.write(text);
|
|
329
|
-
proc.stdin.end();
|
|
330
|
-
proc.on("exit", (code) => {
|
|
331
|
-
if (code === 0) {
|
|
332
|
-
setCopyStatus("Copied to clipboard!");
|
|
333
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
setCopyStatus("Failed to copy");
|
|
337
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
proc.on("error", () => {
|
|
341
|
-
setCopyStatus("Copy not supported");
|
|
342
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
343
|
-
});
|
|
344
|
-
};
|
|
345
|
-
copyToClipboard(logsText);
|
|
346
|
-
}
|
|
347
274
|
return;
|
|
348
275
|
}
|
|
349
276
|
// Operations selection mode
|
|
350
277
|
if (input === "q" || key.escape) {
|
|
351
|
-
|
|
352
|
-
|
|
278
|
+
// Clear all state before going back to free memory
|
|
279
|
+
setOperationResult(null);
|
|
280
|
+
setOperationError(null);
|
|
281
|
+
setOperationInput("");
|
|
282
|
+
setExecutingOperation(null);
|
|
353
283
|
setSelectedOperation(0);
|
|
284
|
+
setLoading(false);
|
|
285
|
+
onBack();
|
|
354
286
|
}
|
|
355
287
|
else if (key.upArrow && selectedOperation > 0) {
|
|
356
288
|
setSelectedOperation(selectedOperation - 1);
|
|
@@ -359,7 +291,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
359
291
|
setSelectedOperation(selectedOperation + 1);
|
|
360
292
|
}
|
|
361
293
|
else if (key.return) {
|
|
362
|
-
console.clear();
|
|
363
294
|
const op = operations[selectedOperation].key;
|
|
364
295
|
setExecutingOperation(op);
|
|
365
296
|
}
|
|
@@ -367,20 +298,17 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
367
298
|
// Check if input matches any operation shortcut
|
|
368
299
|
const matchedOp = operations.find((op) => op.shortcut === input);
|
|
369
300
|
if (matchedOp) {
|
|
370
|
-
console.clear();
|
|
371
301
|
setExecutingOperation(matchedOp.key);
|
|
372
302
|
}
|
|
373
303
|
}
|
|
374
304
|
});
|
|
375
305
|
const executeOperation = async () => {
|
|
376
|
-
const client = getClient();
|
|
377
306
|
try {
|
|
378
307
|
setLoading(true);
|
|
379
308
|
switch (executingOperation) {
|
|
380
309
|
case "exec":
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
});
|
|
310
|
+
// Use service layer (already truncates output to prevent Yoga crashes)
|
|
311
|
+
const execResult = await execCommand(devbox.id, operationInput);
|
|
384
312
|
// Format exec result for custom rendering
|
|
385
313
|
const formattedExecResult = {
|
|
386
314
|
__customRender: "exec",
|
|
@@ -392,23 +320,19 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
392
320
|
setOperationResult(formattedExecResult);
|
|
393
321
|
break;
|
|
394
322
|
case "upload":
|
|
395
|
-
|
|
396
|
-
const fileStream = fs.createReadStream(operationInput);
|
|
323
|
+
// Use service layer
|
|
397
324
|
const filename = operationInput.split("/").pop() || "file";
|
|
398
|
-
await
|
|
399
|
-
path: filename,
|
|
400
|
-
file: fileStream,
|
|
401
|
-
});
|
|
325
|
+
await uploadFile(devbox.id, operationInput, filename);
|
|
402
326
|
setOperationResult(`File ${filename} uploaded successfully`);
|
|
403
327
|
break;
|
|
404
328
|
case "snapshot":
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
});
|
|
329
|
+
// Use service layer
|
|
330
|
+
const snapshot = await createDevboxSnapshot(devbox.id, operationInput || `snapshot-${Date.now()}`);
|
|
408
331
|
setOperationResult(`Snapshot created: ${snapshot.id}`);
|
|
409
332
|
break;
|
|
410
333
|
case "ssh":
|
|
411
|
-
|
|
334
|
+
// Use service layer
|
|
335
|
+
const sshKey = await createSSHKey(devbox.id);
|
|
412
336
|
const fsModule = await import("fs");
|
|
413
337
|
const pathModule = await import("path");
|
|
414
338
|
const osModule = await import("os");
|
|
@@ -421,45 +345,45 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
421
345
|
const sshUser = devbox.launch_parameters?.user_parameters?.username || "user";
|
|
422
346
|
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
423
347
|
const sshHost = env === "dev" ? "ssh.runloop.pro" : "ssh.runloop.ai";
|
|
424
|
-
|
|
425
|
-
|
|
348
|
+
// macOS openssl doesn't support -verify_quiet, use compatible flags
|
|
349
|
+
// servername should be %h (target hostname) - SSH will replace %h with the actual hostname from the SSH command
|
|
350
|
+
// This matches the reference implementation where servername is the target hostname
|
|
351
|
+
const proxyCommand = `openssl s_client -quiet -servername %h -connect ${sshHost}:443 2>/dev/null`;
|
|
352
|
+
// Navigate to SSH session screen
|
|
353
|
+
navigate("ssh-session", {
|
|
426
354
|
keyPath,
|
|
427
355
|
proxyCommand,
|
|
428
356
|
sshUser,
|
|
429
357
|
url: sshKey.url,
|
|
430
358
|
devboxId: devbox.id,
|
|
431
359
|
devboxName: devbox.name || devbox.id,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
onSSHRequest(sshConfig);
|
|
436
|
-
exit();
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
setOperationError(new Error("SSH session handler not configured"));
|
|
440
|
-
}
|
|
360
|
+
returnScreen: currentScreen,
|
|
361
|
+
returnParams: params,
|
|
362
|
+
});
|
|
441
363
|
break;
|
|
442
364
|
case "logs":
|
|
443
|
-
|
|
444
|
-
|
|
365
|
+
// Use service layer (already truncates and escapes log messages)
|
|
366
|
+
const logs = await getDevboxLogs(devbox.id);
|
|
367
|
+
if (logs.length === 0) {
|
|
445
368
|
setOperationResult("No logs available for this devbox.");
|
|
446
369
|
}
|
|
447
370
|
else {
|
|
448
|
-
logsResult
|
|
449
|
-
|
|
450
|
-
|
|
371
|
+
const logsResult = {
|
|
372
|
+
__customRender: "logs",
|
|
373
|
+
__logs: logs,
|
|
374
|
+
__totalCount: logs.length,
|
|
375
|
+
};
|
|
451
376
|
setOperationResult(logsResult);
|
|
452
377
|
}
|
|
453
378
|
break;
|
|
454
379
|
case "tunnel":
|
|
380
|
+
// Use service layer
|
|
455
381
|
const port = parseInt(operationInput);
|
|
456
382
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
457
383
|
setOperationError(new Error("Invalid port number. Please enter a port between 1 and 65535."));
|
|
458
384
|
}
|
|
459
385
|
else {
|
|
460
|
-
const tunnel = await
|
|
461
|
-
port,
|
|
462
|
-
});
|
|
386
|
+
const tunnel = await createTunnel(devbox.id, port);
|
|
463
387
|
setOperationResult(`Tunnel created!\n\n` +
|
|
464
388
|
`Local Port: ${port}\n` +
|
|
465
389
|
`Public URL: ${tunnel.url}\n\n` +
|
|
@@ -467,15 +391,18 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
467
391
|
}
|
|
468
392
|
break;
|
|
469
393
|
case "suspend":
|
|
470
|
-
|
|
394
|
+
// Use service layer
|
|
395
|
+
await suspendDevbox(devbox.id);
|
|
471
396
|
setOperationResult(`Devbox ${devbox.id} suspended successfully`);
|
|
472
397
|
break;
|
|
473
398
|
case "resume":
|
|
474
|
-
|
|
399
|
+
// Use service layer
|
|
400
|
+
await resumeDevbox(devbox.id);
|
|
475
401
|
setOperationResult(`Devbox ${devbox.id} resumed successfully`);
|
|
476
402
|
break;
|
|
477
403
|
case "delete":
|
|
478
|
-
|
|
404
|
+
// Use service layer
|
|
405
|
+
await shutdownDevbox(devbox.id);
|
|
479
406
|
setOperationResult(`Devbox ${devbox.id} shut down successfully`);
|
|
480
407
|
break;
|
|
481
408
|
}
|
|
@@ -501,8 +428,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
501
428
|
const stdoutLines = stdout ? stdout.split("\n") : [];
|
|
502
429
|
const stderrLines = stderr ? stderr.split("\n") : [];
|
|
503
430
|
const allLines = [...stdoutLines, ...stderrLines].filter((line) => line !== "");
|
|
504
|
-
const
|
|
505
|
-
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
431
|
+
const viewportHeight = execViewport.viewportHeight;
|
|
506
432
|
const maxScroll = Math.max(0, allLines.length - viewportHeight);
|
|
507
433
|
const actualScroll = Math.min(execScroll, maxScroll);
|
|
508
434
|
const visibleLines = allLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
@@ -512,58 +438,37 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
512
438
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
513
439
|
...breadcrumbItems,
|
|
514
440
|
{ 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
|
|
441
|
+
] }), _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.length > 500
|
|
442
|
+
? command.substring(0, 500) + "..."
|
|
443
|
+
: 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) => {
|
|
516
444
|
const actualIndex = actualScroll + index;
|
|
517
445
|
const isStderr = actualIndex >= stdoutLines.length;
|
|
518
446
|
const lineColor = isStderr ? colors.error : colors.text;
|
|
519
447
|
return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
|
|
520
|
-
})
|
|
448
|
+
})] }), _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] }), hasLess && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] })), hasMore && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }))] })), 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"] }) })] }));
|
|
521
449
|
}
|
|
522
450
|
// Check for custom logs rendering
|
|
523
451
|
if (operationResult &&
|
|
524
452
|
typeof operationResult === "object" &&
|
|
525
453
|
operationResult.__customRender === "logs") {
|
|
526
454
|
const logs = operationResult.__logs || [];
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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;
|
|
554
|
-
if (logsWrapMode) {
|
|
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));
|
|
556
|
-
}
|
|
557
|
-
else {
|
|
558
|
-
const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
|
|
559
|
-
const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
|
|
560
|
-
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
561
|
-
? fullMessage.substring(0, availableMessageWidth - 3) +
|
|
562
|
-
"..."
|
|
563
|
-
: fullMessage;
|
|
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));
|
|
565
|
-
}
|
|
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"] }) })] }));
|
|
455
|
+
return (_jsx(LogsViewer, { logs: logs, breadcrumbItems: [
|
|
456
|
+
...breadcrumbItems,
|
|
457
|
+
{ label: "Logs", active: true },
|
|
458
|
+
], onBack: () => {
|
|
459
|
+
// Clear large data structures immediately to prevent memory leaks
|
|
460
|
+
setOperationResult(null);
|
|
461
|
+
setOperationError(null);
|
|
462
|
+
setOperationInput("");
|
|
463
|
+
// If skipOperationsMenu is true, go back to parent instead of operations menu
|
|
464
|
+
if (skipOperationsMenu) {
|
|
465
|
+
setExecutingOperation(null);
|
|
466
|
+
onBack();
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
setExecutingOperation(null);
|
|
470
|
+
}
|
|
471
|
+
}, title: "Logs" }));
|
|
567
472
|
}
|
|
568
473
|
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" }) })] }));
|
|
569
474
|
}
|
|
@@ -598,7 +503,12 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
598
503
|
snapshot: "Snapshot name (optional):",
|
|
599
504
|
tunnel: "Port number to expose:",
|
|
600
505
|
};
|
|
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:
|
|
506
|
+
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: (() => {
|
|
507
|
+
const name = devbox.name || devbox.id;
|
|
508
|
+
return name.length > 100
|
|
509
|
+
? name.substring(0, 100) + "..."
|
|
510
|
+
: name;
|
|
511
|
+
})() }) }), _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
512
|
? "ls -la"
|
|
603
513
|
: executingOperation === "upload"
|
|
604
514
|
? "/path/to/file"
|
|
@@ -608,7 +518,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
608
518
|
}
|
|
609
519
|
// Operations selection mode - only show if not skipping
|
|
610
520
|
if (!skipOperationsMenu || !executingOperation) {
|
|
611
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems
|
|
521
|
+
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
522
|
const isSelected = index === selectedOperation;
|
|
613
523
|
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
524
|
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
@@ -10,6 +10,7 @@ import { SuccessMessage } from "./SuccessMessage.js";
|
|
|
10
10
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
11
11
|
import { MetadataDisplay } from "./MetadataDisplay.js";
|
|
12
12
|
import { colors } from "../utils/theme.js";
|
|
13
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
13
14
|
export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
14
15
|
const [currentField, setCurrentField] = React.useState("create");
|
|
15
16
|
const [formData, setFormData] = React.useState({
|
|
@@ -68,6 +69,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
|
68
69
|
"CUSTOM_SIZE",
|
|
69
70
|
];
|
|
70
71
|
const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
|
|
72
|
+
// Handle Ctrl+C to exit
|
|
73
|
+
useExitOnCtrlC();
|
|
71
74
|
useInput((input, key) => {
|
|
72
75
|
// Handle result screen
|
|
73
76
|
if (result) {
|
|
@@ -97,7 +100,6 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
|
97
100
|
}
|
|
98
101
|
// Back to list
|
|
99
102
|
if (input === "q" || key.escape) {
|
|
100
|
-
console.clear();
|
|
101
103
|
onBack();
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
@@ -241,10 +243,13 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
|
241
243
|
});
|
|
242
244
|
}
|
|
243
245
|
else if (currentField === "resource_size") {
|
|
244
|
-
|
|
246
|
+
// Find current index, defaulting to 0 if not found (e.g., empty string)
|
|
247
|
+
const currentSize = formData.resource_size || "SMALL";
|
|
248
|
+
const currentIndex = resourceSizes.indexOf(currentSize);
|
|
249
|
+
const safeIndex = currentIndex === -1 ? 0 : currentIndex;
|
|
245
250
|
const newIndex = key.leftArrow
|
|
246
|
-
? Math.max(0,
|
|
247
|
-
: Math.min(resourceSizes.length - 1,
|
|
251
|
+
? Math.max(0, safeIndex - 1)
|
|
252
|
+
: Math.min(resourceSizes.length - 1, safeIndex + 1);
|
|
248
253
|
setFormData({
|
|
249
254
|
...formData,
|
|
250
255
|
resource_size: resourceSizes[newIndex],
|
|
@@ -339,7 +344,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
|
339
344
|
};
|
|
340
345
|
// Result screen
|
|
341
346
|
if (result) {
|
|
342
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SuccessMessage, { message: "Devbox created successfully!",
|
|
347
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SuccessMessage, { message: "Devbox 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: ["Status: ", result.status] }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
|
|
343
348
|
}
|
|
344
349
|
// Error screen
|
|
345
350
|
if (error) {
|
|
@@ -350,7 +355,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
|
350
355
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
|
|
351
356
|
}
|
|
352
357
|
// Form screen
|
|
353
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field
|
|
358
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
|
|
354
359
|
const isActive = currentField === field.key;
|
|
355
360
|
const fieldData = formData[field.key];
|
|
356
361
|
if (field.type === "action") {
|