@runloop/rl-cli 0.1.2 → 0.2.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 -0
- package/dist/cli.js +73 -60
- package/dist/commands/auth.js +0 -1
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +215 -213
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/blueprint/preview.js +42 -38
- package/dist/commands/config.js +117 -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 +45 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +36 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-install.js +4 -3
- package/dist/commands/menu.js +24 -66
- 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 +63 -39
- package/dist/components/Breadcrumb.js +10 -48
- package/dist/components/DevboxActionsMenu.js +182 -110
- 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 +94 -0
- package/dist/components/MainMenu.js +36 -32
- 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/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +14 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server.js +65 -6
- package/dist/router/Router.js +68 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -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 +105 -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/CommandExecutor.js +53 -24
- package/dist/utils/client.js +0 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +162 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +150 -59
- package/dist/utils/screen.js +23 -0
- package/dist/utils/ssh.js +3 -1
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +97 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +147 -13
- package/package.json +16 -13
package/dist/mcp/server.js
CHANGED
|
@@ -2,7 +2,57 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import
|
|
5
|
+
import Runloop from "@runloop/api-client";
|
|
6
|
+
import Conf from "conf";
|
|
7
|
+
let configInstance = null;
|
|
8
|
+
function getConfigInstance() {
|
|
9
|
+
if (!configInstance) {
|
|
10
|
+
configInstance = new Conf({
|
|
11
|
+
projectName: "runloop-cli",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return configInstance;
|
|
15
|
+
}
|
|
16
|
+
function getConfig() {
|
|
17
|
+
// Check environment variable first
|
|
18
|
+
const envApiKey = process.env.RUNLOOP_API_KEY;
|
|
19
|
+
if (envApiKey) {
|
|
20
|
+
return { apiKey: envApiKey };
|
|
21
|
+
}
|
|
22
|
+
// Fall back to stored config
|
|
23
|
+
try {
|
|
24
|
+
const config = getConfigInstance();
|
|
25
|
+
const apiKey = config.get("apiKey");
|
|
26
|
+
return { apiKey };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error("Warning: Failed to load config:", error);
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getBaseUrl() {
|
|
34
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
35
|
+
switch (env) {
|
|
36
|
+
case "dev":
|
|
37
|
+
return "https://api.runloop.pro";
|
|
38
|
+
case "prod":
|
|
39
|
+
default:
|
|
40
|
+
return "https://api.runloop.ai";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getClient() {
|
|
44
|
+
const config = getConfig();
|
|
45
|
+
if (!config.apiKey) {
|
|
46
|
+
throw new Error("API key not configured. Please set RUNLOOP_API_KEY environment variable or run: rli auth");
|
|
47
|
+
}
|
|
48
|
+
const baseURL = getBaseUrl();
|
|
49
|
+
return new Runloop({
|
|
50
|
+
bearerToken: config.apiKey,
|
|
51
|
+
baseURL,
|
|
52
|
+
timeout: 10000, // 10 seconds instead of default 30 seconds
|
|
53
|
+
maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
6
56
|
// Define available tools for the MCP server
|
|
7
57
|
const TOOLS = [
|
|
8
58
|
{
|
|
@@ -386,12 +436,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
386
436
|
});
|
|
387
437
|
// Start the server
|
|
388
438
|
async function main() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
439
|
+
try {
|
|
440
|
+
console.error("[MCP] Starting Runloop MCP server...");
|
|
441
|
+
const transport = new StdioServerTransport();
|
|
442
|
+
console.error("[MCP] Created stdio transport");
|
|
443
|
+
await server.connect(transport);
|
|
444
|
+
// Log to stderr so it doesn't interfere with MCP protocol on stdout
|
|
445
|
+
console.error("[MCP] Server initialization complete, waiting for requests...");
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
console.error("[MCP] Error in main():", error);
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
393
451
|
}
|
|
394
452
|
main().catch((error) => {
|
|
395
|
-
console.error("Fatal error in main():", error);
|
|
453
|
+
console.error("[MCP] Fatal error in main():", error);
|
|
454
|
+
console.error("[MCP] Stack trace:", error instanceof Error ? error.stack : "N/A");
|
|
396
455
|
process.exit(1);
|
|
397
456
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Router - Manages screen navigation with clean mount/unmount lifecycle
|
|
4
|
+
* Replaces conditional rendering pattern from menu.tsx
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
8
|
+
import { useDevboxStore } from "../store/devboxStore.js";
|
|
9
|
+
import { useBlueprintStore } from "../store/blueprintStore.js";
|
|
10
|
+
import { useSnapshotStore } from "../store/snapshotStore.js";
|
|
11
|
+
import { ErrorBoundary } from "../components/ErrorBoundary.js";
|
|
12
|
+
// Import screen components
|
|
13
|
+
import { MenuScreen } from "../screens/MenuScreen.js";
|
|
14
|
+
import { DevboxListScreen } from "../screens/DevboxListScreen.js";
|
|
15
|
+
import { DevboxDetailScreen } from "../screens/DevboxDetailScreen.js";
|
|
16
|
+
import { DevboxActionsScreen } from "../screens/DevboxActionsScreen.js";
|
|
17
|
+
import { DevboxCreateScreen } from "../screens/DevboxCreateScreen.js";
|
|
18
|
+
import { BlueprintListScreen } from "../screens/BlueprintListScreen.js";
|
|
19
|
+
import { SnapshotListScreen } from "../screens/SnapshotListScreen.js";
|
|
20
|
+
import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
|
|
21
|
+
/**
|
|
22
|
+
* Router component that renders the current screen
|
|
23
|
+
* Implements memory cleanup on route changes
|
|
24
|
+
*
|
|
25
|
+
* Uses React key prop to force complete unmount/remount on screen changes,
|
|
26
|
+
* which prevents Yoga WASM errors during transitions.
|
|
27
|
+
*/
|
|
28
|
+
export function Router() {
|
|
29
|
+
const { currentScreen, params } = useNavigation();
|
|
30
|
+
const prevScreenRef = React.useRef(null);
|
|
31
|
+
// Memory cleanup on route changes
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
const prevScreen = prevScreenRef.current;
|
|
34
|
+
if (prevScreen && prevScreen !== currentScreen) {
|
|
35
|
+
// Immediate cleanup without delay - React's key-based remount handles timing
|
|
36
|
+
switch (prevScreen) {
|
|
37
|
+
case "devbox-list":
|
|
38
|
+
case "devbox-detail":
|
|
39
|
+
case "devbox-actions":
|
|
40
|
+
case "devbox-create":
|
|
41
|
+
// Clear devbox data when leaving devbox screens
|
|
42
|
+
// Keep cache if we're still in devbox context
|
|
43
|
+
if (!currentScreen.startsWith("devbox")) {
|
|
44
|
+
useDevboxStore.getState().clearAll();
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case "blueprint-list":
|
|
48
|
+
case "blueprint-detail":
|
|
49
|
+
if (!currentScreen.startsWith("blueprint")) {
|
|
50
|
+
useBlueprintStore.getState().clearAll();
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case "snapshot-list":
|
|
54
|
+
case "snapshot-detail":
|
|
55
|
+
if (!currentScreen.startsWith("snapshot")) {
|
|
56
|
+
useSnapshotStore.getState().clearAll();
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
prevScreenRef.current = currentScreen;
|
|
62
|
+
}, [currentScreen]);
|
|
63
|
+
// CRITICAL: Use key prop to force React to completely unmount old component
|
|
64
|
+
// and mount new component, preventing race conditions during screen transitions.
|
|
65
|
+
// The key ensures React treats this as a completely new component tree.
|
|
66
|
+
// Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
|
|
67
|
+
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { ListBlueprintsUI } from "../commands/blueprint/list.js";
|
|
4
|
+
export function BlueprintListScreen() {
|
|
5
|
+
const { goBack } = useNavigation();
|
|
6
|
+
return _jsx(ListBlueprintsUI, { onBack: goBack, onExit: goBack });
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* DevboxActionsScreen - Pure UI component for devbox actions
|
|
4
|
+
* Refactored from components/DevboxActionsMenu.tsx
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
8
|
+
import { useDevboxStore } from "../store/devboxStore.js";
|
|
9
|
+
import { DevboxActionsMenu } from "../components/DevboxActionsMenu.js";
|
|
10
|
+
export function DevboxActionsScreen({ devboxId, operation, }) {
|
|
11
|
+
const { goBack } = useNavigation();
|
|
12
|
+
const devboxes = useDevboxStore((state) => state.devboxes);
|
|
13
|
+
// Find devbox in store
|
|
14
|
+
const devbox = devboxes.find((d) => d.id === devboxId);
|
|
15
|
+
// Navigate back if devbox not found - must be in useEffect, not during render
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
if (!devbox) {
|
|
18
|
+
goBack();
|
|
19
|
+
}
|
|
20
|
+
}, [devbox, goBack]);
|
|
21
|
+
if (!devbox) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return (_jsx(DevboxActionsMenu, { devbox: devbox, onBack: goBack, initialOperation: operation }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { DevboxCreatePage } from "../components/DevboxCreatePage.js";
|
|
4
|
+
export function DevboxCreateScreen() {
|
|
5
|
+
const { goBack } = useNavigation();
|
|
6
|
+
const handleCreate = () => {
|
|
7
|
+
// After creation, go back to list (which will refresh)
|
|
8
|
+
goBack();
|
|
9
|
+
};
|
|
10
|
+
return _jsx(DevboxCreatePage, { onBack: goBack, onCreate: handleCreate });
|
|
11
|
+
}
|
|
@@ -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,105 @@
|
|
|
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
|
+
*/
|
|
76
|
+
export async function getBlueprintLogs(id) {
|
|
77
|
+
const client = getClient();
|
|
78
|
+
const response = await client.blueprints.logs(id);
|
|
79
|
+
// CRITICAL: Truncate all strings to prevent Yoga crashes
|
|
80
|
+
const MAX_MESSAGE_LENGTH = 1000;
|
|
81
|
+
const MAX_LEVEL_LENGTH = 20;
|
|
82
|
+
const logs = [];
|
|
83
|
+
if (response.logs && Array.isArray(response.logs)) {
|
|
84
|
+
response.logs.forEach((log) => {
|
|
85
|
+
// Truncate message and escape newlines
|
|
86
|
+
let message = String(log.message || "");
|
|
87
|
+
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
88
|
+
message = message.substring(0, MAX_MESSAGE_LENGTH) + "...";
|
|
89
|
+
}
|
|
90
|
+
message = message
|
|
91
|
+
.replace(/\r\n/g, "\\n")
|
|
92
|
+
.replace(/\n/g, "\\n")
|
|
93
|
+
.replace(/\r/g, "\\r")
|
|
94
|
+
.replace(/\t/g, "\\t");
|
|
95
|
+
logs.push({
|
|
96
|
+
timestamp: log.timestamp,
|
|
97
|
+
message,
|
|
98
|
+
level: log.level
|
|
99
|
+
? String(log.level).substring(0, MAX_LEVEL_LENGTH)
|
|
100
|
+
: undefined,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return logs;
|
|
105
|
+
}
|