@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* DevboxDetailScreen - Pure UI component for devbox details
|
|
4
|
+
* Refactored from components/DevboxDetailPage.tsx
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
8
|
+
import { useDevboxStore } from "../store/devboxStore.js";
|
|
9
|
+
import { DevboxDetailPage } from "../components/DevboxDetailPage.js";
|
|
10
|
+
import { getDevbox } from "../services/devboxService.js";
|
|
11
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
12
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
13
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
14
|
+
export function DevboxDetailScreen({ devboxId }) {
|
|
15
|
+
const { goBack } = useNavigation();
|
|
16
|
+
const devboxes = useDevboxStore((state) => state.devboxes);
|
|
17
|
+
const setDevboxesInStore = useDevboxStore((state) => state.setDevboxes);
|
|
18
|
+
const [loading, setLoading] = React.useState(false);
|
|
19
|
+
const [error, setError] = React.useState(null);
|
|
20
|
+
const [fetchedDevbox, setFetchedDevbox] = React.useState(null);
|
|
21
|
+
// Find devbox in store first
|
|
22
|
+
const devboxFromStore = devboxes.find((d) => d.id === devboxId);
|
|
23
|
+
// Fetch devbox from API if not in store
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (!devboxFromStore && devboxId && !loading && !fetchedDevbox) {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
getDevbox(devboxId)
|
|
29
|
+
.then((devbox) => {
|
|
30
|
+
setFetchedDevbox(devbox);
|
|
31
|
+
// Cache it in store for future access
|
|
32
|
+
setDevboxesInStore([devbox]);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
})
|
|
35
|
+
.catch((err) => {
|
|
36
|
+
setError(err);
|
|
37
|
+
setLoading(false);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}, [devboxFromStore, devboxId, loading, fetchedDevbox, setDevboxesInStore]);
|
|
41
|
+
// Use devbox from store or fetched devbox
|
|
42
|
+
const devbox = devboxFromStore || fetchedDevbox;
|
|
43
|
+
// Show loading state while fetching
|
|
44
|
+
if (loading) {
|
|
45
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Loading...", active: true }] }), _jsx(SpinnerComponent, { message: "Loading devbox details..." })] }));
|
|
46
|
+
}
|
|
47
|
+
// Show error state if fetch failed
|
|
48
|
+
if (error) {
|
|
49
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Error", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load devbox details", error: error })] }));
|
|
50
|
+
}
|
|
51
|
+
// Show error if no devbox found and not loading
|
|
52
|
+
if (!devbox && !loading) {
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Not Found", active: true }] }), _jsx(ErrorMessage, { message: `Devbox ${devboxId || "unknown"} not found`, error: new Error("Devbox not found in cache and could not be fetched") })] }));
|
|
54
|
+
}
|
|
55
|
+
// At this point devbox is guaranteed to exist (loading check above handles the null case)
|
|
56
|
+
if (!devbox) {
|
|
57
|
+
return null; // TypeScript guard - should never reach here
|
|
58
|
+
}
|
|
59
|
+
return _jsx(DevboxDetailPage, { devbox: devbox, onBack: goBack });
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* DevboxListScreen - Pure UI component using devboxStore
|
|
4
|
+
* Simplified version for now - wraps existing component
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
8
|
+
import { ListDevboxesUI } from "../commands/devbox/list.js";
|
|
9
|
+
export function DevboxListScreen({ status, focusDevboxId, }) {
|
|
10
|
+
const { goBack, navigate } = useNavigation();
|
|
11
|
+
// If focusDevboxId is provided, navigate directly to detail screen
|
|
12
|
+
// instead of letting ListDevboxesUI handle it internally
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (focusDevboxId) {
|
|
15
|
+
navigate("devbox-detail", { devboxId: focusDevboxId });
|
|
16
|
+
}
|
|
17
|
+
}, [focusDevboxId, navigate]);
|
|
18
|
+
// Navigation callback to handle detail view via Router
|
|
19
|
+
const handleNavigateToDetail = React.useCallback((devboxId) => {
|
|
20
|
+
navigate("devbox-detail", { devboxId });
|
|
21
|
+
}, [navigate]);
|
|
22
|
+
return (_jsx(ListDevboxesUI, { status: status, onBack: goBack, onExit: goBack, onNavigateToDetail: handleNavigateToDetail }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* LogsSessionScreen - Logs viewer using custom InteractiveSpawn
|
|
4
|
+
* Runs the CLI logs command as a subprocess within the Ink UI without exiting
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { InteractiveSpawn } from "../components/InteractiveSpawn.js";
|
|
9
|
+
import { useNavigation, } from "../store/navigationStore.js";
|
|
10
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
11
|
+
import { colors } from "../utils/theme.js";
|
|
12
|
+
import figures from "figures";
|
|
13
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
export function LogsSessionScreen() {
|
|
17
|
+
const { params, navigate } = useNavigation();
|
|
18
|
+
// Handle Ctrl+C to exit (before logs command runs or on error)
|
|
19
|
+
useExitOnCtrlC();
|
|
20
|
+
// Extract params
|
|
21
|
+
const devboxId = params.devboxId;
|
|
22
|
+
const devboxName = params.devboxName || params.devboxId || "devbox";
|
|
23
|
+
const returnScreen = params.returnScreen || "devbox-list";
|
|
24
|
+
const returnParams = params.returnParams || {};
|
|
25
|
+
// Get the path to the CLI executable
|
|
26
|
+
const cliPath = React.useMemo(() => {
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
// When compiled, this file is in dist/screens/, so go up one level to dist/cli.js
|
|
30
|
+
return join(__dirname, "../cli.js");
|
|
31
|
+
}, []);
|
|
32
|
+
// Validate required params
|
|
33
|
+
if (!devboxId) {
|
|
34
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Logs", active: true }] }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Text, { color: colors.error, children: [figures.cross, " Missing devbox ID. Returning..."] }) })] }));
|
|
35
|
+
}
|
|
36
|
+
// Build CLI command args
|
|
37
|
+
const cliArgs = React.useMemo(() => ["devbox", "logs", devboxId], [devboxId]);
|
|
38
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Logs", active: true }] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.info, " Viewing logs for ", devboxName, "..."] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Ctrl+C to exit" })] }), _jsx(InteractiveSpawn, { command: "node", args: [cliPath, ...cliArgs], onExit: (_code) => {
|
|
39
|
+
// Navigate back to previous screen when logs command exits
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
navigate(returnScreen, returnParams || {});
|
|
42
|
+
}, 100);
|
|
43
|
+
}, onError: (_error) => {
|
|
44
|
+
// On error, navigate back as well
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
navigate(returnScreen, returnParams || {});
|
|
47
|
+
}, 100);
|
|
48
|
+
} })] }));
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { MainMenu } from "../components/MainMenu.js";
|
|
4
|
+
export function MenuScreen() {
|
|
5
|
+
const { navigate } = useNavigation();
|
|
6
|
+
const handleSelect = (key) => {
|
|
7
|
+
switch (key) {
|
|
8
|
+
case "devboxes":
|
|
9
|
+
navigate("devbox-list");
|
|
10
|
+
break;
|
|
11
|
+
case "blueprints":
|
|
12
|
+
navigate("blueprint-list");
|
|
13
|
+
break;
|
|
14
|
+
case "snapshots":
|
|
15
|
+
navigate("snapshot-list");
|
|
16
|
+
break;
|
|
17
|
+
default:
|
|
18
|
+
// Fallback for any other screen names
|
|
19
|
+
navigate(key);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
return _jsx(MainMenu, { onSelect: handleSelect });
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* SSHSessionScreen - SSH session using custom InteractiveSpawn
|
|
4
|
+
* Runs SSH as a subprocess within the Ink UI without exiting
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { InteractiveSpawn } from "../components/InteractiveSpawn.js";
|
|
9
|
+
import { useNavigation, } from "../store/navigationStore.js";
|
|
10
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
11
|
+
import { colors } from "../utils/theme.js";
|
|
12
|
+
import figures from "figures";
|
|
13
|
+
export function SSHSessionScreen() {
|
|
14
|
+
const { params, replace } = useNavigation();
|
|
15
|
+
// NOTE: Do NOT use useExitOnCtrlC here - SSH handles Ctrl+C itself
|
|
16
|
+
// Using useInput would conflict with the subprocess's terminal control
|
|
17
|
+
// Extract SSH config from params
|
|
18
|
+
const keyPath = params.keyPath;
|
|
19
|
+
const proxyCommand = params.proxyCommand;
|
|
20
|
+
const sshUser = params.sshUser;
|
|
21
|
+
const url = params.url;
|
|
22
|
+
const devboxName = params.devboxName || params.devboxId || "devbox";
|
|
23
|
+
const returnScreen = params.returnScreen || "devbox-list";
|
|
24
|
+
const returnParams = params.returnParams || {};
|
|
25
|
+
// Validate required params
|
|
26
|
+
if (!keyPath || !proxyCommand || !sshUser || !url) {
|
|
27
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "SSH Session", active: true }] }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Text, { color: colors.error, children: [figures.cross, " Missing SSH configuration. Returning..."] }) })] }));
|
|
28
|
+
}
|
|
29
|
+
// Build SSH command args
|
|
30
|
+
// The proxy command needs to be passed as a single value in the -o option
|
|
31
|
+
const sshArgs = React.useMemo(() => [
|
|
32
|
+
"-t", // Force pseudo-terminal allocation for proper input handling
|
|
33
|
+
"-i",
|
|
34
|
+
keyPath,
|
|
35
|
+
"-o",
|
|
36
|
+
`ProxyCommand=${proxyCommand}`,
|
|
37
|
+
"-o",
|
|
38
|
+
"StrictHostKeyChecking=no",
|
|
39
|
+
"-o",
|
|
40
|
+
"UserKnownHostsFile=/dev/null",
|
|
41
|
+
`${sshUser}@${url}`,
|
|
42
|
+
], [keyPath, proxyCommand, sshUser, url]);
|
|
43
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "SSH Session", active: true }] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Connecting to ", devboxName, "..."] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Ctrl+C or type exit to disconnect" })] }), _jsx(InteractiveSpawn, { command: "ssh", args: sshArgs, onExit: (_code) => {
|
|
44
|
+
// Replace current screen (don't add SSH to history stack)
|
|
45
|
+
// Using replace() instead of navigate() prevents "escape goes back to SSH" bug
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
replace(returnScreen, returnParams || {});
|
|
48
|
+
}, 100);
|
|
49
|
+
}, onError: (_error) => {
|
|
50
|
+
// On error, replace current screen as well
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
replace(returnScreen, returnParams || {});
|
|
53
|
+
}, 100);
|
|
54
|
+
} })] }));
|
|
55
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { ListSnapshotsUI } from "../commands/snapshot/list.js";
|
|
4
|
+
export function SnapshotListScreen() {
|
|
5
|
+
const { goBack } = useNavigation();
|
|
6
|
+
return _jsx(ListSnapshotsUI, { onBack: goBack, onExit: goBack });
|
|
7
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint Service - Handles all blueprint API calls
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../utils/client.js";
|
|
5
|
+
/**
|
|
6
|
+
* List blueprints with pagination
|
|
7
|
+
*/
|
|
8
|
+
export async function listBlueprints(options) {
|
|
9
|
+
const client = getClient();
|
|
10
|
+
const queryParams = {
|
|
11
|
+
limit: options.limit,
|
|
12
|
+
};
|
|
13
|
+
if (options.startingAfter) {
|
|
14
|
+
queryParams.starting_after = options.startingAfter;
|
|
15
|
+
}
|
|
16
|
+
if (options.search) {
|
|
17
|
+
queryParams.name = options.search;
|
|
18
|
+
}
|
|
19
|
+
const pagePromise = client.blueprints.list(queryParams);
|
|
20
|
+
const page = (await pagePromise);
|
|
21
|
+
const blueprints = [];
|
|
22
|
+
if (page.blueprints && Array.isArray(page.blueprints)) {
|
|
23
|
+
page.blueprints.forEach((b) => {
|
|
24
|
+
// CRITICAL: Truncate all strings to prevent Yoga crashes
|
|
25
|
+
const MAX_ID_LENGTH = 100;
|
|
26
|
+
const MAX_NAME_LENGTH = 200;
|
|
27
|
+
const MAX_STATUS_LENGTH = 50;
|
|
28
|
+
const MAX_ARCH_LENGTH = 50;
|
|
29
|
+
const MAX_RESOURCES_LENGTH = 100;
|
|
30
|
+
// Extract architecture and resources from launch_parameters
|
|
31
|
+
const architecture = b.parameters?.launch_parameters?.architecture;
|
|
32
|
+
const resources = b.parameters?.launch_parameters?.resource_size_request;
|
|
33
|
+
blueprints.push({
|
|
34
|
+
id: String(b.id || "").substring(0, MAX_ID_LENGTH),
|
|
35
|
+
name: String(b.name || "").substring(0, MAX_NAME_LENGTH),
|
|
36
|
+
status: String(b.status || "").substring(0, MAX_STATUS_LENGTH),
|
|
37
|
+
create_time_ms: b.create_time_ms,
|
|
38
|
+
build_status: b.status
|
|
39
|
+
? String(b.status).substring(0, MAX_STATUS_LENGTH)
|
|
40
|
+
: undefined,
|
|
41
|
+
architecture: architecture
|
|
42
|
+
? String(architecture).substring(0, MAX_ARCH_LENGTH)
|
|
43
|
+
: undefined,
|
|
44
|
+
resources: resources
|
|
45
|
+
? String(resources).substring(0, MAX_RESOURCES_LENGTH)
|
|
46
|
+
: undefined,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const result = {
|
|
51
|
+
blueprints,
|
|
52
|
+
totalCount: page.total_count || blueprints.length,
|
|
53
|
+
hasMore: page.has_more || false,
|
|
54
|
+
};
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get a single blueprint by ID
|
|
59
|
+
*/
|
|
60
|
+
export async function getBlueprint(id) {
|
|
61
|
+
const client = getClient();
|
|
62
|
+
const blueprint = await client.blueprints.retrieve(id);
|
|
63
|
+
return {
|
|
64
|
+
id: blueprint.id,
|
|
65
|
+
name: blueprint.name,
|
|
66
|
+
status: blueprint.status,
|
|
67
|
+
create_time_ms: blueprint.create_time_ms,
|
|
68
|
+
build_status: blueprint.build_status,
|
|
69
|
+
architecture: blueprint.architecture,
|
|
70
|
+
resources: blueprint.resources,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get blueprint logs
|
|
75
|
+
* Returns the raw logs array from the API response
|
|
76
|
+
* Similar to getDevboxLogs - formatting is handled by logFormatter
|
|
77
|
+
*/
|
|
78
|
+
export async function getBlueprintLogs(id) {
|
|
79
|
+
const client = getClient();
|
|
80
|
+
const response = await client.blueprints.logs(id);
|
|
81
|
+
// Return the logs array directly - formatting is handled by logFormatter
|
|
82
|
+
// Ensure timestamp_ms is present (API may return timestamp or timestamp_ms)
|
|
83
|
+
if (response.logs && Array.isArray(response.logs)) {
|
|
84
|
+
return response.logs.map((log) => {
|
|
85
|
+
// Normalize timestamp field to timestamp_ms if needed
|
|
86
|
+
// Create a new object to avoid mutating the original
|
|
87
|
+
const normalizedLog = { ...log };
|
|
88
|
+
if (normalizedLog.timestamp && !normalizedLog.timestamp_ms) {
|
|
89
|
+
// If timestamp is a number, use it directly; if it's a string, parse it
|
|
90
|
+
if (typeof normalizedLog.timestamp === "number") {
|
|
91
|
+
normalizedLog.timestamp_ms = normalizedLog.timestamp;
|
|
92
|
+
}
|
|
93
|
+
else if (typeof normalizedLog.timestamp === "string") {
|
|
94
|
+
normalizedLog.timestamp_ms = new Date(normalizedLog.timestamp).getTime();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return normalizedLog;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devbox Service - Handles all devbox API calls
|
|
3
|
+
* Returns plain data objects with no SDK reference retention
|
|
4
|
+
*/
|
|
5
|
+
import { getClient } from "../utils/client.js";
|
|
6
|
+
/**
|
|
7
|
+
* Recursively truncate all strings in an object to prevent Yoga crashes
|
|
8
|
+
* CRITICAL: Must be applied to ALL data from API before storing/rendering
|
|
9
|
+
*/
|
|
10
|
+
function truncateStrings(obj, maxLength = 200) {
|
|
11
|
+
if (obj === null || obj === undefined)
|
|
12
|
+
return obj;
|
|
13
|
+
if (typeof obj === "string") {
|
|
14
|
+
return (obj.length > maxLength ? obj.substring(0, maxLength) : obj);
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(obj)) {
|
|
17
|
+
return obj.map((item) => truncateStrings(item, maxLength));
|
|
18
|
+
}
|
|
19
|
+
if (typeof obj === "object") {
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const key in obj) {
|
|
22
|
+
if (Object.hasOwn(obj, key)) {
|
|
23
|
+
result[key] = truncateStrings(obj[key], maxLength);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
return obj;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List devboxes with pagination
|
|
32
|
+
* CRITICAL: Creates defensive copies to break SDK reference chains
|
|
33
|
+
*/
|
|
34
|
+
export async function listDevboxes(options) {
|
|
35
|
+
// Check if aborted before making request
|
|
36
|
+
if (options.signal?.aborted) {
|
|
37
|
+
throw new DOMException("Aborted", "AbortError");
|
|
38
|
+
}
|
|
39
|
+
const client = getClient();
|
|
40
|
+
const queryParams = {
|
|
41
|
+
limit: options.limit,
|
|
42
|
+
};
|
|
43
|
+
if (options.startingAfter) {
|
|
44
|
+
queryParams.starting_after = options.startingAfter;
|
|
45
|
+
}
|
|
46
|
+
if (options.status) {
|
|
47
|
+
queryParams.status = options.status;
|
|
48
|
+
}
|
|
49
|
+
if (options.search) {
|
|
50
|
+
queryParams.name = options.search;
|
|
51
|
+
}
|
|
52
|
+
// Fetch ONE page only - never iterate
|
|
53
|
+
const pagePromise = client.devboxes.list(queryParams);
|
|
54
|
+
// Wrap in Promise.race to support abort
|
|
55
|
+
let page;
|
|
56
|
+
if (options.signal) {
|
|
57
|
+
const abortPromise = new Promise((_, reject) => {
|
|
58
|
+
options.signal.addEventListener("abort", () => {
|
|
59
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
page = (await Promise.race([
|
|
64
|
+
pagePromise,
|
|
65
|
+
abortPromise,
|
|
66
|
+
]));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Re-throw abort errors, convert others
|
|
70
|
+
if (err?.name === "AbortError") {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
page = (await pagePromise);
|
|
78
|
+
}
|
|
79
|
+
// Check again after await (in case abort happened during request)
|
|
80
|
+
if (options.signal?.aborted) {
|
|
81
|
+
throw new DOMException("Aborted", "AbortError");
|
|
82
|
+
}
|
|
83
|
+
// Extract data and create defensive copies immediately
|
|
84
|
+
const devboxes = [];
|
|
85
|
+
if (page.devboxes && Array.isArray(page.devboxes)) {
|
|
86
|
+
page.devboxes.forEach((d) => {
|
|
87
|
+
// CRITICAL: Recursively truncate ALL strings in the object to prevent Yoga crashes
|
|
88
|
+
// This catches nested fields like launch_parameters.user_parameters.username
|
|
89
|
+
const truncated = truncateStrings(d, 200);
|
|
90
|
+
devboxes.push(truncated);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const result = {
|
|
94
|
+
devboxes,
|
|
95
|
+
totalCount: page.total_count || devboxes.length,
|
|
96
|
+
hasMore: page.has_more || false,
|
|
97
|
+
};
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get a single devbox by ID
|
|
102
|
+
*/
|
|
103
|
+
export async function getDevbox(id) {
|
|
104
|
+
const client = getClient();
|
|
105
|
+
const devbox = await client.devboxes.retrieve(id);
|
|
106
|
+
// CRITICAL: Recursively truncate ALL strings in the object to prevent Yoga crashes
|
|
107
|
+
return truncateStrings(devbox, 200);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Delete a devbox (actually shuts it down)
|
|
111
|
+
*/
|
|
112
|
+
export async function deleteDevbox(id) {
|
|
113
|
+
const client = getClient();
|
|
114
|
+
await client.devboxes.shutdown(id);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Shutdown a devbox
|
|
118
|
+
*/
|
|
119
|
+
export async function shutdownDevbox(id) {
|
|
120
|
+
const client = getClient();
|
|
121
|
+
await client.devboxes.shutdown(id);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Suspend a devbox
|
|
125
|
+
*/
|
|
126
|
+
export async function suspendDevbox(id) {
|
|
127
|
+
const client = getClient();
|
|
128
|
+
await client.devboxes.suspend(id);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resume a devbox
|
|
132
|
+
*/
|
|
133
|
+
export async function resumeDevbox(id) {
|
|
134
|
+
const client = getClient();
|
|
135
|
+
await client.devboxes.resume(id);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Upload file to devbox
|
|
139
|
+
*/
|
|
140
|
+
export async function uploadFile(id, filepath, remotePath) {
|
|
141
|
+
const client = getClient();
|
|
142
|
+
const fs = await import("fs");
|
|
143
|
+
const fileStream = fs.createReadStream(filepath);
|
|
144
|
+
await client.devboxes.uploadFile(id, {
|
|
145
|
+
file: fileStream,
|
|
146
|
+
path: remotePath,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Create snapshot of devbox
|
|
151
|
+
*/
|
|
152
|
+
export async function createSnapshot(id, name) {
|
|
153
|
+
const client = getClient();
|
|
154
|
+
const snapshot = await client.devboxes.snapshotDisk(id, { name });
|
|
155
|
+
return {
|
|
156
|
+
id: String(snapshot.id || "").substring(0, 100),
|
|
157
|
+
name: snapshot.name ? String(snapshot.name).substring(0, 200) : undefined,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create SSH key for devbox
|
|
162
|
+
*/
|
|
163
|
+
export async function createSSHKey(id) {
|
|
164
|
+
const client = getClient();
|
|
165
|
+
const result = await client.devboxes.createSSHKey(id);
|
|
166
|
+
// Truncate keys if they're unexpectedly long (shouldn't happen, but safety)
|
|
167
|
+
return {
|
|
168
|
+
ssh_private_key: String(result.ssh_private_key || "").substring(0, 10000),
|
|
169
|
+
url: String(result.url || "").substring(0, 500),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Create tunnel to devbox
|
|
174
|
+
*/
|
|
175
|
+
export async function createTunnel(id, port) {
|
|
176
|
+
const client = getClient();
|
|
177
|
+
const tunnel = await client.devboxes.createTunnel(id, { port });
|
|
178
|
+
return {
|
|
179
|
+
url: String(tunnel.url || "").substring(0, 500),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Execute command in devbox
|
|
184
|
+
*/
|
|
185
|
+
export async function execCommand(id, command) {
|
|
186
|
+
const client = getClient();
|
|
187
|
+
const result = await client.devboxes.executeSync(id, { command });
|
|
188
|
+
// CRITICAL: Truncate output to prevent Yoga crashes
|
|
189
|
+
const MAX_OUTPUT_LENGTH = 10000; // Allow more for command output
|
|
190
|
+
let stdout = String(result.stdout || "");
|
|
191
|
+
let stderr = String(result.stderr || "");
|
|
192
|
+
if (stdout.length > MAX_OUTPUT_LENGTH) {
|
|
193
|
+
stdout =
|
|
194
|
+
stdout.substring(0, MAX_OUTPUT_LENGTH) + "\n... (output truncated)";
|
|
195
|
+
}
|
|
196
|
+
if (stderr.length > MAX_OUTPUT_LENGTH) {
|
|
197
|
+
stderr =
|
|
198
|
+
stderr.substring(0, MAX_OUTPUT_LENGTH) + "\n... (output truncated)";
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
stdout,
|
|
202
|
+
stderr,
|
|
203
|
+
exit_code: result.exit_code || 0,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get devbox logs
|
|
208
|
+
* Returns the raw logs array from the API response
|
|
209
|
+
*/
|
|
210
|
+
export async function getDevboxLogs(id) {
|
|
211
|
+
const client = getClient();
|
|
212
|
+
const response = await client.devboxes.logs.list(id);
|
|
213
|
+
// Return the logs array directly - formatting is handled by logFormatter
|
|
214
|
+
return response.logs || [];
|
|
215
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot Service - Handles all snapshot API calls
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../utils/client.js";
|
|
5
|
+
/**
|
|
6
|
+
* List snapshots with pagination
|
|
7
|
+
*/
|
|
8
|
+
export async function listSnapshots(options) {
|
|
9
|
+
const client = getClient();
|
|
10
|
+
const queryParams = {
|
|
11
|
+
limit: options.limit,
|
|
12
|
+
};
|
|
13
|
+
if (options.startingAfter) {
|
|
14
|
+
queryParams.starting_after = options.startingAfter;
|
|
15
|
+
}
|
|
16
|
+
if (options.devboxId) {
|
|
17
|
+
queryParams.devbox_id = options.devboxId;
|
|
18
|
+
}
|
|
19
|
+
const pagePromise = client.devboxes.listDiskSnapshots(queryParams);
|
|
20
|
+
const page = (await pagePromise);
|
|
21
|
+
const snapshots = [];
|
|
22
|
+
if (page.snapshots && Array.isArray(page.snapshots)) {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
page.snapshots.forEach((s) => {
|
|
25
|
+
// CRITICAL: Truncate all strings to prevent Yoga crashes
|
|
26
|
+
const MAX_ID_LENGTH = 100;
|
|
27
|
+
const MAX_NAME_LENGTH = 200;
|
|
28
|
+
const MAX_STATUS_LENGTH = 50;
|
|
29
|
+
// Status is constructed/available in API response but not in type definition
|
|
30
|
+
const snapshotView = s;
|
|
31
|
+
snapshots.push({
|
|
32
|
+
id: String(snapshotView.id || "").substring(0, MAX_ID_LENGTH),
|
|
33
|
+
name: snapshotView.name
|
|
34
|
+
? String(snapshotView.name).substring(0, MAX_NAME_LENGTH)
|
|
35
|
+
: undefined,
|
|
36
|
+
devbox_id: String(snapshotView.source_devbox_id || "").substring(0, MAX_ID_LENGTH),
|
|
37
|
+
status: snapshotView.status
|
|
38
|
+
? String(snapshotView.status).substring(0, MAX_STATUS_LENGTH)
|
|
39
|
+
: "",
|
|
40
|
+
create_time_ms: snapshotView.create_time_ms,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const result = {
|
|
45
|
+
snapshots,
|
|
46
|
+
totalCount: page.total_count || snapshots.length,
|
|
47
|
+
hasMore: page.has_more || false,
|
|
48
|
+
};
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get snapshot status by ID
|
|
53
|
+
*/
|
|
54
|
+
export async function getSnapshotStatus(id) {
|
|
55
|
+
const client = getClient();
|
|
56
|
+
const status = await client.devboxes.diskSnapshots.queryStatus(id);
|
|
57
|
+
return status;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a snapshot
|
|
61
|
+
*/
|
|
62
|
+
export async function createSnapshot(devboxId, name) {
|
|
63
|
+
const client = getClient();
|
|
64
|
+
const snapshot = await client.devboxes.snapshotDisk(devboxId, {
|
|
65
|
+
name,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
id: snapshot.id,
|
|
69
|
+
name: snapshot.name || undefined,
|
|
70
|
+
devbox_id: snapshot.devbox_id || devboxId,
|
|
71
|
+
status: snapshot.status || "pending",
|
|
72
|
+
create_time_ms: snapshot.create_time_ms,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Delete a snapshot
|
|
77
|
+
*/
|
|
78
|
+
export async function deleteSnapshot(id) {
|
|
79
|
+
const client = getClient();
|
|
80
|
+
await client.devboxes.diskSnapshots.delete(id);
|
|
81
|
+
}
|