@runloop/rl-cli 0.0.1 → 0.0.3
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/dist/cli.js +59 -13
- package/dist/commands/auth.js +2 -1
- package/dist/commands/blueprint/list.js +18 -7
- package/dist/commands/devbox/create.js +1 -2
- package/dist/commands/devbox/list.js +307 -75
- package/dist/commands/menu.js +70 -0
- package/dist/commands/snapshot/list.js +18 -9
- package/dist/components/ActionsPopup.js +42 -0
- package/dist/components/Banner.js +3 -6
- package/dist/components/Breadcrumb.js +3 -4
- package/dist/components/DevboxActionsMenu.js +481 -0
- package/dist/components/DevboxCreatePage.js +15 -7
- package/dist/components/DevboxDetailPage.js +57 -364
- package/dist/components/MainMenu.js +71 -0
- package/dist/components/StatusBadge.js +15 -12
- package/dist/utils/CommandExecutor.js +12 -0
- package/dist/utils/client.js +17 -0
- package/dist/utils/interactiveCommand.js +14 -0
- package/dist/utils/sshSession.js +25 -0
- package/dist/utils/url.js +39 -0
- package/package.json +2 -1
|
@@ -1,10 +1,8 @@
|
|
|
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, useStdout } from 'ink';
|
|
3
|
+
import { Box, Text, useInput, useStdout, useApp } from 'ink';
|
|
4
4
|
import figures from 'figures';
|
|
5
5
|
import { getClient } from '../../utils/client.js';
|
|
6
|
-
import { Header } from '../../components/Header.js';
|
|
7
|
-
import { Banner } from '../../components/Banner.js';
|
|
8
6
|
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
9
7
|
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
10
8
|
import { StatusBadge } from '../../components/StatusBadge.js';
|
|
@@ -33,8 +31,9 @@ const formatTimeAgo = (timestamp) => {
|
|
|
33
31
|
const years = Math.floor(months / 12);
|
|
34
32
|
return `${years}y ago`;
|
|
35
33
|
};
|
|
36
|
-
const ListSnapshotsUI = ({ devboxId }) => {
|
|
34
|
+
const ListSnapshotsUI = ({ devboxId, onBack, onExit }) => {
|
|
37
35
|
const { stdout } = useStdout();
|
|
36
|
+
const { exit: inkExit } = useApp();
|
|
38
37
|
const [loading, setLoading] = React.useState(true);
|
|
39
38
|
const [snapshots, setSnapshots] = React.useState([]);
|
|
40
39
|
const [error, setError] = React.useState(null);
|
|
@@ -89,8 +88,16 @@ const ListSnapshotsUI = ({ devboxId }) => {
|
|
|
89
88
|
setCurrentPage(currentPage - 1);
|
|
90
89
|
setSelectedIndex(0);
|
|
91
90
|
}
|
|
92
|
-
else if (
|
|
93
|
-
|
|
91
|
+
else if (key.escape) {
|
|
92
|
+
if (onBack) {
|
|
93
|
+
onBack();
|
|
94
|
+
}
|
|
95
|
+
else if (onExit) {
|
|
96
|
+
onExit();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
inkExit();
|
|
100
|
+
}
|
|
94
101
|
}
|
|
95
102
|
});
|
|
96
103
|
const totalPages = Math.ceil(snapshots.length / PAGE_SIZE);
|
|
@@ -99,17 +106,19 @@ const ListSnapshotsUI = ({ devboxId }) => {
|
|
|
99
106
|
const currentSnapshots = snapshots.slice(startIndex, endIndex);
|
|
100
107
|
const ready = snapshots.filter((s) => s.status === 'ready').length;
|
|
101
108
|
const pending = snapshots.filter((s) => s.status !== 'ready').length;
|
|
102
|
-
return (_jsxs(_Fragment, { children: [_jsx(
|
|
109
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
103
110
|
{ label: 'Snapshots', active: !devboxId },
|
|
104
111
|
...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
|
|
105
|
-
] }),
|
|
112
|
+
] }), loading && _jsx(SpinnerComponent, { message: "Loading snapshots..." }), !loading && !error && snapshots.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No snapshots found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln snapshot create <devbox-id>" })] })), !loading && !error && snapshots.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "green", children: [figures.tick, " ", ready] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: [figures.ellipsis, " ", pending] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: [figures.hamburger, " ", snapshots.length, snapshots.length >= MAX_FETCH && '+'] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Page ", currentPage + 1, "/", totalPages] })] }))] }), _jsx(Table, { data: currentSnapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, columns: [
|
|
106
113
|
createComponentColumn('status', 'Status', (snapshot) => _jsx(StatusBadge, { status: snapshot.status, showText: false }), { width: 2 }),
|
|
107
114
|
createTextColumn('id', 'ID', (snapshot) => showFullId ? snapshot.id : snapshot.id.slice(0, 13), { width: showFullId ? idWidth : 15, color: 'gray', dimColor: true, bold: false }),
|
|
108
115
|
createTextColumn('name', 'Name', (snapshot) => snapshot.name || '(unnamed)', { width: nameWidth }),
|
|
109
116
|
createTextColumn('devbox', 'Devbox', (snapshot) => snapshot.devbox_id || '', { width: devboxWidth, color: 'cyan', dimColor: true, bold: false, visible: showDevboxId }),
|
|
110
117
|
createTextColumn('created', 'Created', (snapshot) => snapshot.created_at ? formatTimeAgo(new Date(snapshot.created_at).getTime()) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
|
|
111
|
-
] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[
|
|
118
|
+
] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[Esc] Back"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list snapshots", error: error })] }));
|
|
112
119
|
};
|
|
120
|
+
// Export the UI component for use in the main menu
|
|
121
|
+
export { ListSnapshotsUI };
|
|
113
122
|
export async function listSnapshots(options) {
|
|
114
123
|
const executor = createExecutor(options);
|
|
115
124
|
await executor.executeList(async () => {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import figures from 'figures';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
export const ActionsPopup = ({ devbox, operations, selectedOperation, onClose, }) => {
|
|
6
|
+
// Calculate the maximum width needed
|
|
7
|
+
const maxLabelLength = Math.max(...operations.map(op => op.label.length));
|
|
8
|
+
const contentWidth = maxLabelLength + 12; // Content + icon + pointer + shortcuts
|
|
9
|
+
// Strip ANSI codes to get real length, then pad
|
|
10
|
+
const stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, '');
|
|
11
|
+
const bgLine = (content) => {
|
|
12
|
+
const cleanLength = stripAnsi(content).length;
|
|
13
|
+
const padding = Math.max(0, contentWidth - cleanLength);
|
|
14
|
+
return chalk.bgBlack(content + ' '.repeat(padding));
|
|
15
|
+
};
|
|
16
|
+
// Render all lines with background
|
|
17
|
+
const lines = [
|
|
18
|
+
bgLine(chalk.cyan.bold(` ${figures.play} Quick Actions`)),
|
|
19
|
+
chalk.bgBlack(' '.repeat(contentWidth)),
|
|
20
|
+
...operations.map((op, index) => {
|
|
21
|
+
const isSelected = index === selectedOperation;
|
|
22
|
+
const pointer = isSelected ? figures.pointer : ' ';
|
|
23
|
+
const content = ` ${pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
|
|
24
|
+
let styled;
|
|
25
|
+
if (isSelected) {
|
|
26
|
+
const colorFn = chalk[op.color];
|
|
27
|
+
styled = typeof colorFn === 'function' ? colorFn.bold(content) : chalk.white.bold(content);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
styled = chalk.gray(content);
|
|
31
|
+
}
|
|
32
|
+
return bgLine(styled);
|
|
33
|
+
}),
|
|
34
|
+
chalk.bgBlack(' '.repeat(contentWidth)),
|
|
35
|
+
bgLine(chalk.gray.dim(` ${figures.arrowUp}${figures.arrowDown} Nav • [Enter]`)),
|
|
36
|
+
bgLine(chalk.gray.dim(` [Esc] Close`)),
|
|
37
|
+
];
|
|
38
|
+
// Draw custom border with background to fill gaps
|
|
39
|
+
const borderTop = chalk.cyan('╭' + '─'.repeat(contentWidth) + '╮');
|
|
40
|
+
const borderBottom = chalk.cyan('╰' + '─'.repeat(contentWidth) + '╯');
|
|
41
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), lines.map((line, i) => (_jsxs(Text, { children: [chalk.cyan('│'), line, chalk.cyan('│')] }, i))), _jsx(Text, { children: borderBottom })] }) }));
|
|
42
|
+
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { Box
|
|
3
|
+
import { Box } from 'ink';
|
|
4
|
+
import BigText from 'ink-big-text';
|
|
4
5
|
import Gradient from 'ink-gradient';
|
|
5
6
|
export const Banner = React.memo(() => {
|
|
6
|
-
return (_jsx(Box, { flexDirection: "column",
|
|
7
|
-
╦═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔═╗
|
|
8
|
-
╠╦╝║ ║║║║║ ║ ║║ ║╠═╝
|
|
9
|
-
╩╚═╚═╝╝╚╝╩═╝╚═╝╚═╝╩ .ai
|
|
10
|
-
` }) }) }));
|
|
7
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", children: _jsx(Gradient, { name: "vice", children: _jsx(BigText, { text: "RUNLOOP.ai", font: "simple3d" }) }) }));
|
|
11
8
|
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import figures from 'figures';
|
|
5
4
|
export const Breadcrumb = React.memo(({ items }) => {
|
|
6
|
-
const
|
|
7
|
-
const isDevEnvironment =
|
|
8
|
-
return (
|
|
5
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
6
|
+
const isDevEnvironment = env === 'dev';
|
|
7
|
+
return (_jsx(Box, { marginBottom: 1, paddingX: 2, paddingY: 0, children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: "cyan", bold: true, children: "rl" }), isDevEnvironment && _jsx(Text, { color: "redBright", bold: true, children: " (dev)" }), _jsx(Text, { color: "gray", dimColor: true, children: " \u203A " }), items.map((item, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? 'white' : 'gray', bold: item.active, dimColor: !item.active, children: item.label }), index < items.length - 1 && (_jsx(Text, { color: "gray", dimColor: true, children: " \u203A " }))] }, index)))] }) }));
|
|
9
8
|
});
|
|
@@ -0,0 +1,481 @@
|
|
|
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, 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
|
+
export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
13
|
+
{ label: 'Devboxes' },
|
|
14
|
+
{ label: devbox.name || devbox.id, active: true }
|
|
15
|
+
], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, onSSHRequest, }) => {
|
|
16
|
+
const { exit } = useApp();
|
|
17
|
+
const { stdout } = useStdout();
|
|
18
|
+
const [loading, setLoading] = React.useState(false);
|
|
19
|
+
const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
|
|
20
|
+
const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
|
|
21
|
+
const [operationInput, setOperationInput] = React.useState('');
|
|
22
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
23
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
24
|
+
const [logsWrapMode, setLogsWrapMode] = React.useState(false);
|
|
25
|
+
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
26
|
+
const [execScroll, setExecScroll] = React.useState(0);
|
|
27
|
+
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
28
|
+
const allOperations = [
|
|
29
|
+
{ key: 'logs', label: 'View Logs', color: 'blue', icon: figures.info, shortcut: 'l' },
|
|
30
|
+
{ key: 'exec', label: 'Execute Command', color: 'green', icon: figures.play, shortcut: 'e' },
|
|
31
|
+
{ key: 'upload', label: 'Upload File', color: 'green', icon: figures.arrowUp, shortcut: 'u' },
|
|
32
|
+
{ key: 'snapshot', label: 'Create Snapshot', color: 'yellow', icon: figures.circleFilled, shortcut: 'n' },
|
|
33
|
+
{ key: 'ssh', label: 'SSH onto the box', color: 'cyan', icon: figures.arrowRight, shortcut: 's' },
|
|
34
|
+
{ key: 'tunnel', label: 'Open Tunnel', color: 'magenta', icon: figures.pointerSmall, shortcut: 't' },
|
|
35
|
+
{ key: 'suspend', label: 'Suspend Devbox', color: 'yellow', icon: figures.squareSmallFilled, shortcut: 'p' },
|
|
36
|
+
{ key: 'resume', label: 'Resume Devbox', color: 'green', icon: figures.play, shortcut: 'r' },
|
|
37
|
+
{ key: 'delete', label: 'Shutdown Devbox', color: 'red', icon: figures.cross, shortcut: 'd' },
|
|
38
|
+
];
|
|
39
|
+
// Filter operations based on devbox status
|
|
40
|
+
const operations = devbox ? allOperations.filter(op => {
|
|
41
|
+
const status = devbox.status;
|
|
42
|
+
// When suspended: logs and resume
|
|
43
|
+
if (status === 'suspended') {
|
|
44
|
+
return op.key === 'resume' || op.key === 'logs';
|
|
45
|
+
}
|
|
46
|
+
// When not running (shutdown, failure, etc): only logs
|
|
47
|
+
if (status !== 'running' && status !== 'provisioning' && status !== 'initializing') {
|
|
48
|
+
return op.key === 'logs';
|
|
49
|
+
}
|
|
50
|
+
// When running: everything except resume
|
|
51
|
+
if (status === 'running') {
|
|
52
|
+
return op.key !== 'resume';
|
|
53
|
+
}
|
|
54
|
+
// Default for transitional states (provisioning, initializing)
|
|
55
|
+
return op.key === 'logs' || op.key === 'delete';
|
|
56
|
+
}) : allOperations;
|
|
57
|
+
// Auto-execute operations that don't need input
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
const autoExecuteOps = ['delete', 'ssh', 'logs', 'suspend', 'resume'];
|
|
60
|
+
if (executingOperation && autoExecuteOps.includes(executingOperation) && !loading && devbox) {
|
|
61
|
+
executeOperation();
|
|
62
|
+
}
|
|
63
|
+
}, [executingOperation]);
|
|
64
|
+
useInput((input, key) => {
|
|
65
|
+
// Handle operation input mode
|
|
66
|
+
if (executingOperation && !operationResult && !operationError) {
|
|
67
|
+
if (key.return && operationInput.trim()) {
|
|
68
|
+
executeOperation();
|
|
69
|
+
}
|
|
70
|
+
else if (input === 'q' || key.escape) {
|
|
71
|
+
console.clear();
|
|
72
|
+
setExecutingOperation(null);
|
|
73
|
+
setOperationInput('');
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Handle operation result display
|
|
78
|
+
if (operationResult || operationError) {
|
|
79
|
+
if (input === 'q' || key.escape || key.return) {
|
|
80
|
+
console.clear();
|
|
81
|
+
// If skipOperationsMenu is true, go back to parent instead of operations menu
|
|
82
|
+
if (skipOperationsMenu) {
|
|
83
|
+
onBack();
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
setOperationResult(null);
|
|
87
|
+
setOperationError(null);
|
|
88
|
+
setExecutingOperation(null);
|
|
89
|
+
setOperationInput('');
|
|
90
|
+
setLogsWrapMode(true);
|
|
91
|
+
setLogsScroll(0);
|
|
92
|
+
setExecScroll(0);
|
|
93
|
+
setCopyStatus(null);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
97
|
+
setExecScroll(Math.max(0, execScroll - 1));
|
|
98
|
+
}
|
|
99
|
+
else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
100
|
+
setExecScroll(execScroll + 1);
|
|
101
|
+
}
|
|
102
|
+
else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
103
|
+
setExecScroll(Math.max(0, execScroll - 10));
|
|
104
|
+
}
|
|
105
|
+
else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
106
|
+
setExecScroll(execScroll + 10);
|
|
107
|
+
}
|
|
108
|
+
else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
109
|
+
setExecScroll(0);
|
|
110
|
+
}
|
|
111
|
+
else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
112
|
+
const lines = [...(operationResult.stdout || '').split('\n'), ...(operationResult.stderr || '').split('\n')];
|
|
113
|
+
const terminalHeight = stdout?.rows || 30;
|
|
114
|
+
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
115
|
+
const maxScroll = Math.max(0, lines.length - viewportHeight);
|
|
116
|
+
setExecScroll(maxScroll);
|
|
117
|
+
}
|
|
118
|
+
else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
119
|
+
// Copy exec output to clipboard
|
|
120
|
+
const output = (operationResult.stdout || '') + (operationResult.stderr || '');
|
|
121
|
+
const copyToClipboard = async (text) => {
|
|
122
|
+
const { spawn } = await import('child_process');
|
|
123
|
+
const platform = process.platform;
|
|
124
|
+
let command;
|
|
125
|
+
let args;
|
|
126
|
+
if (platform === 'darwin') {
|
|
127
|
+
command = 'pbcopy';
|
|
128
|
+
args = [];
|
|
129
|
+
}
|
|
130
|
+
else if (platform === 'win32') {
|
|
131
|
+
command = 'clip';
|
|
132
|
+
args = [];
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
command = 'xclip';
|
|
136
|
+
args = ['-selection', 'clipboard'];
|
|
137
|
+
}
|
|
138
|
+
const proc = spawn(command, args);
|
|
139
|
+
proc.stdin.write(text);
|
|
140
|
+
proc.stdin.end();
|
|
141
|
+
proc.on('exit', (code) => {
|
|
142
|
+
if (code === 0) {
|
|
143
|
+
setCopyStatus('Copied to clipboard!');
|
|
144
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
setCopyStatus('Failed to copy');
|
|
148
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
proc.on('error', () => {
|
|
152
|
+
setCopyStatus('Copy not supported');
|
|
153
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
copyToClipboard(output);
|
|
157
|
+
}
|
|
158
|
+
else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
159
|
+
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
160
|
+
}
|
|
161
|
+
else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
162
|
+
setLogsScroll(logsScroll + 1);
|
|
163
|
+
}
|
|
164
|
+
else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
165
|
+
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
166
|
+
}
|
|
167
|
+
else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
168
|
+
setLogsScroll(logsScroll + 10);
|
|
169
|
+
}
|
|
170
|
+
else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
171
|
+
setLogsScroll(0);
|
|
172
|
+
}
|
|
173
|
+
else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
174
|
+
const logs = operationResult.__logs || [];
|
|
175
|
+
const terminalHeight = stdout?.rows || 30;
|
|
176
|
+
const viewportHeight = Math.max(10, terminalHeight - 10);
|
|
177
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
178
|
+
setLogsScroll(maxScroll);
|
|
179
|
+
}
|
|
180
|
+
else if (input === 'w' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
181
|
+
setLogsWrapMode(!logsWrapMode);
|
|
182
|
+
}
|
|
183
|
+
else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
184
|
+
// Copy logs to clipboard
|
|
185
|
+
const logs = operationResult.__logs || [];
|
|
186
|
+
const logsText = logs.map((log) => {
|
|
187
|
+
const time = new Date(log.timestamp_ms).toLocaleString();
|
|
188
|
+
const level = log.level || 'INFO';
|
|
189
|
+
const source = log.source || 'exec';
|
|
190
|
+
const message = log.message || '';
|
|
191
|
+
const cmd = log.cmd ? `[${log.cmd}] ` : '';
|
|
192
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
|
|
193
|
+
return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
|
|
194
|
+
}).join('\n');
|
|
195
|
+
const copyToClipboard = async (text) => {
|
|
196
|
+
const { spawn } = await import('child_process');
|
|
197
|
+
const platform = process.platform;
|
|
198
|
+
let command;
|
|
199
|
+
let args;
|
|
200
|
+
if (platform === 'darwin') {
|
|
201
|
+
command = 'pbcopy';
|
|
202
|
+
args = [];
|
|
203
|
+
}
|
|
204
|
+
else if (platform === 'win32') {
|
|
205
|
+
command = 'clip';
|
|
206
|
+
args = [];
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
command = 'xclip';
|
|
210
|
+
args = ['-selection', 'clipboard'];
|
|
211
|
+
}
|
|
212
|
+
const proc = spawn(command, args);
|
|
213
|
+
proc.stdin.write(text);
|
|
214
|
+
proc.stdin.end();
|
|
215
|
+
proc.on('exit', (code) => {
|
|
216
|
+
if (code === 0) {
|
|
217
|
+
setCopyStatus('Copied to clipboard!');
|
|
218
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
setCopyStatus('Failed to copy');
|
|
222
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
proc.on('error', () => {
|
|
226
|
+
setCopyStatus('Copy not supported');
|
|
227
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
copyToClipboard(logsText);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Operations selection mode
|
|
235
|
+
if (input === 'q' || key.escape) {
|
|
236
|
+
console.clear();
|
|
237
|
+
onBack();
|
|
238
|
+
setSelectedOperation(0);
|
|
239
|
+
}
|
|
240
|
+
else if (key.upArrow && selectedOperation > 0) {
|
|
241
|
+
setSelectedOperation(selectedOperation - 1);
|
|
242
|
+
}
|
|
243
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
244
|
+
setSelectedOperation(selectedOperation + 1);
|
|
245
|
+
}
|
|
246
|
+
else if (key.return) {
|
|
247
|
+
console.clear();
|
|
248
|
+
const op = operations[selectedOperation].key;
|
|
249
|
+
setExecutingOperation(op);
|
|
250
|
+
}
|
|
251
|
+
else if (input) {
|
|
252
|
+
// Check if input matches any operation shortcut
|
|
253
|
+
const matchedOp = operations.find(op => op.shortcut === input);
|
|
254
|
+
if (matchedOp) {
|
|
255
|
+
console.clear();
|
|
256
|
+
setExecutingOperation(matchedOp.key);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const executeOperation = async () => {
|
|
261
|
+
const client = getClient();
|
|
262
|
+
try {
|
|
263
|
+
setLoading(true);
|
|
264
|
+
switch (executingOperation) {
|
|
265
|
+
case 'exec':
|
|
266
|
+
const execResult = await client.devboxes.executeSync(devbox.id, {
|
|
267
|
+
command: operationInput,
|
|
268
|
+
});
|
|
269
|
+
// Format exec result for custom rendering
|
|
270
|
+
const formattedExecResult = {
|
|
271
|
+
__customRender: 'exec',
|
|
272
|
+
command: operationInput,
|
|
273
|
+
stdout: execResult.stdout || '',
|
|
274
|
+
stderr: execResult.stderr || '',
|
|
275
|
+
exitCode: execResult.exit_code ?? 0,
|
|
276
|
+
};
|
|
277
|
+
setOperationResult(formattedExecResult);
|
|
278
|
+
break;
|
|
279
|
+
case 'upload':
|
|
280
|
+
const fs = await import('fs');
|
|
281
|
+
const fileStream = fs.createReadStream(operationInput);
|
|
282
|
+
const filename = operationInput.split('/').pop() || 'file';
|
|
283
|
+
await client.devboxes.uploadFile(devbox.id, {
|
|
284
|
+
path: filename,
|
|
285
|
+
file: fileStream,
|
|
286
|
+
});
|
|
287
|
+
setOperationResult(`File ${filename} uploaded successfully`);
|
|
288
|
+
break;
|
|
289
|
+
case 'snapshot':
|
|
290
|
+
const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
|
|
291
|
+
name: operationInput || `snapshot-${Date.now()}`,
|
|
292
|
+
});
|
|
293
|
+
setOperationResult(`Snapshot created: ${snapshot.id}`);
|
|
294
|
+
break;
|
|
295
|
+
case 'ssh':
|
|
296
|
+
const sshKey = await client.devboxes.createSSHKey(devbox.id);
|
|
297
|
+
const fsModule = await import('fs');
|
|
298
|
+
const pathModule = await import('path');
|
|
299
|
+
const osModule = await import('os');
|
|
300
|
+
const sshDir = pathModule.join(osModule.homedir(), '.runloop', 'ssh_keys');
|
|
301
|
+
fsModule.mkdirSync(sshDir, { recursive: true });
|
|
302
|
+
const keyPath = pathModule.join(sshDir, `${devbox.id}.pem`);
|
|
303
|
+
fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, { mode: 0o600 });
|
|
304
|
+
const sshUser = devbox.launch_parameters?.user_parameters?.username || 'user';
|
|
305
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
306
|
+
const sshHost = env === 'dev' ? 'ssh.runloop.pro' : 'ssh.runloop.ai';
|
|
307
|
+
const proxyCommand = `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshHost}:443 2>/dev/null`;
|
|
308
|
+
const sshConfig = {
|
|
309
|
+
keyPath,
|
|
310
|
+
proxyCommand,
|
|
311
|
+
sshUser,
|
|
312
|
+
url: sshKey.url,
|
|
313
|
+
devboxId: devbox.id,
|
|
314
|
+
devboxName: devbox.name || devbox.id
|
|
315
|
+
};
|
|
316
|
+
// Notify parent that SSH is requested
|
|
317
|
+
if (onSSHRequest) {
|
|
318
|
+
onSSHRequest(sshConfig);
|
|
319
|
+
exit();
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
setOperationError(new Error('SSH session handler not configured'));
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
case 'logs':
|
|
326
|
+
const logsResult = await client.devboxes.logs.list(devbox.id);
|
|
327
|
+
if (logsResult.logs.length === 0) {
|
|
328
|
+
setOperationResult('No logs available for this devbox.');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
logsResult.__customRender = 'logs';
|
|
332
|
+
logsResult.__logs = logsResult.logs;
|
|
333
|
+
logsResult.__totalCount = logsResult.logs.length;
|
|
334
|
+
setOperationResult(logsResult);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
case 'tunnel':
|
|
338
|
+
const port = parseInt(operationInput);
|
|
339
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
340
|
+
setOperationError(new Error('Invalid port number. Please enter a port between 1 and 65535.'));
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const tunnel = await client.devboxes.createTunnel(devbox.id, { port });
|
|
344
|
+
setOperationResult(`Tunnel created!\n\n` +
|
|
345
|
+
`Local Port: ${port}\n` +
|
|
346
|
+
`Public URL: ${tunnel.url}\n\n` +
|
|
347
|
+
`You can now access port ${port} on the devbox via:\n${tunnel.url}`);
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
case 'suspend':
|
|
351
|
+
await client.devboxes.suspend(devbox.id);
|
|
352
|
+
setOperationResult(`Devbox ${devbox.id} suspended successfully`);
|
|
353
|
+
break;
|
|
354
|
+
case 'resume':
|
|
355
|
+
await client.devboxes.resume(devbox.id);
|
|
356
|
+
setOperationResult(`Devbox ${devbox.id} resumed successfully`);
|
|
357
|
+
break;
|
|
358
|
+
case 'delete':
|
|
359
|
+
await client.devboxes.shutdown(devbox.id);
|
|
360
|
+
setOperationResult(`Devbox ${devbox.id} shut down successfully`);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
setOperationError(err);
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
setLoading(false);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
|
|
372
|
+
// Operation result display
|
|
373
|
+
if (operationResult || operationError) {
|
|
374
|
+
// Check for custom exec rendering
|
|
375
|
+
if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
376
|
+
const command = operationResult.command || '';
|
|
377
|
+
const stdout = operationResult.stdout || '';
|
|
378
|
+
const stderr = operationResult.stderr || '';
|
|
379
|
+
const exitCode = operationResult.exitCode;
|
|
380
|
+
const stdoutLines = stdout ? stdout.split('\n') : [];
|
|
381
|
+
const stderrLines = stderr ? stderr.split('\n') : [];
|
|
382
|
+
const allLines = [...stdoutLines, ...stderrLines].filter(line => line !== '');
|
|
383
|
+
const terminalHeight = stdout?.rows || 30;
|
|
384
|
+
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
385
|
+
const maxScroll = Math.max(0, allLines.length - viewportHeight);
|
|
386
|
+
const actualScroll = Math.min(execScroll, maxScroll);
|
|
387
|
+
const visibleLines = allLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
388
|
+
const hasMore = actualScroll + viewportHeight < allLines.length;
|
|
389
|
+
const hasLess = actualScroll > 0;
|
|
390
|
+
const exitCodeColor = exitCode === 0 ? 'green' : 'red';
|
|
391
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: 'Execute Command', active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Command:"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: "white", children: command })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "Exit Code: " }), _jsx(Text, { color: exitCodeColor, bold: true, children: exitCode })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [allLines.length === 0 && (_jsx(Text, { color: "gray", dimColor: true, children: "No output" })), visibleLines.map((line, index) => {
|
|
392
|
+
const actualIndex = actualScroll + index;
|
|
393
|
+
const isStderr = actualIndex >= stdoutLines.length;
|
|
394
|
+
const lineColor = isStderr ? 'red' : 'white';
|
|
395
|
+
return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
|
|
396
|
+
}), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsx(Text, { color: "gray", dimColor: true, children: " lines" }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of ", allLines.length] })] })), stdout && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "green", dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "red", dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "green", bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
397
|
+
}
|
|
398
|
+
// Check for custom logs rendering
|
|
399
|
+
if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
400
|
+
const logs = operationResult.__logs || [];
|
|
401
|
+
const totalCount = operationResult.__totalCount || 0;
|
|
402
|
+
const terminalHeight = stdout?.rows || 30;
|
|
403
|
+
const terminalWidth = stdout?.columns || 120;
|
|
404
|
+
const viewportHeight = Math.max(10, terminalHeight - 10);
|
|
405
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
406
|
+
const actualScroll = Math.min(logsScroll, maxScroll);
|
|
407
|
+
const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
|
|
408
|
+
const hasMore = actualScroll + viewportHeight < logs.length;
|
|
409
|
+
const hasLess = actualScroll > 0;
|
|
410
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: 'Logs', active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [visibleLogs.map((log, index) => {
|
|
411
|
+
const time = new Date(log.timestamp_ms).toLocaleTimeString();
|
|
412
|
+
const level = log.level ? log.level[0].toUpperCase() : 'I';
|
|
413
|
+
const source = log.source ? log.source.substring(0, 8) : 'exec';
|
|
414
|
+
const fullMessage = log.message || '';
|
|
415
|
+
const cmd = log.cmd ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? '...' : ''}] ` : '';
|
|
416
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
|
|
417
|
+
let levelColor = 'gray';
|
|
418
|
+
if (level === 'E')
|
|
419
|
+
levelColor = 'red';
|
|
420
|
+
else if (level === 'W')
|
|
421
|
+
levelColor = 'yellow';
|
|
422
|
+
else if (level === 'I')
|
|
423
|
+
levelColor = 'cyan';
|
|
424
|
+
if (logsWrapMode) {
|
|
425
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: fullMessage })] }, index));
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
|
|
429
|
+
const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
|
|
430
|
+
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
431
|
+
? fullMessage.substring(0, availableMessageWidth - 3) + '...'
|
|
432
|
+
: fullMessage;
|
|
433
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: truncatedMessage })] }, index));
|
|
434
|
+
}
|
|
435
|
+
}), hasLess && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", totalCount] }), _jsx(Text, { color: "gray", dimColor: true, children: " total logs" }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of ", logs.length] }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: logsWrapMode ? 'green' : 'gray', bold: logsWrapMode, children: logsWrapMode ? 'Wrap: ON' : 'Wrap: OFF' }), copyStatus && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "green", bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: "gray", 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"] }) })] }));
|
|
436
|
+
}
|
|
437
|
+
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: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
|
|
438
|
+
}
|
|
439
|
+
// Operation input mode
|
|
440
|
+
if (executingOperation && devbox) {
|
|
441
|
+
const needsInput = executingOperation === 'exec' ||
|
|
442
|
+
executingOperation === 'upload' ||
|
|
443
|
+
executingOperation === 'snapshot' ||
|
|
444
|
+
executingOperation === 'tunnel';
|
|
445
|
+
if (loading) {
|
|
446
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
447
|
+
}
|
|
448
|
+
if (!needsInput) {
|
|
449
|
+
const messages = {
|
|
450
|
+
ssh: 'Creating SSH key...',
|
|
451
|
+
logs: 'Fetching logs...',
|
|
452
|
+
suspend: 'Suspending devbox...',
|
|
453
|
+
resume: 'Resuming devbox...',
|
|
454
|
+
delete: 'Shutting down devbox...',
|
|
455
|
+
};
|
|
456
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
|
|
457
|
+
}
|
|
458
|
+
const prompts = {
|
|
459
|
+
exec: 'Command to execute:',
|
|
460
|
+
upload: 'File path to upload:',
|
|
461
|
+
snapshot: 'Snapshot name (optional):',
|
|
462
|
+
tunnel: 'Port number to expose:',
|
|
463
|
+
};
|
|
464
|
+
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: "cyan", bold: true, children: devbox.name || devbox.id }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === 'exec'
|
|
465
|
+
? 'ls -la'
|
|
466
|
+
: executingOperation === 'upload'
|
|
467
|
+
? '/path/to/file'
|
|
468
|
+
: executingOperation === 'tunnel'
|
|
469
|
+
? '8080'
|
|
470
|
+
: 'my-snapshot' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
471
|
+
}
|
|
472
|
+
// Operations selection mode - only show if not skipping
|
|
473
|
+
if (!skipOperationsMenu || !executingOperation) {
|
|
474
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
475
|
+
const isSelected = index === selectedOperation;
|
|
476
|
+
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));
|
|
477
|
+
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
|
|
478
|
+
}
|
|
479
|
+
// If skipOperationsMenu is true and executingOperation is set, show loading while it executes
|
|
480
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
481
|
+
};
|