@runloop/rl-cli 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -10
- package/dist/cli.js +79 -72
- package/dist/commands/auth.js +2 -2
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +278 -230
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/config.js +118 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +46 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +37 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +12 -10
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +26 -67
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +64 -39
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +11 -48
- package/dist/components/DevboxActionsMenu.js +117 -207
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +104 -0
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +37 -33
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +15 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +71 -7
- package/dist/router/Router.js +70 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +101 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/client.js +4 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +208 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +153 -61
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +61 -0
- package/dist/utils/ssh.js +6 -3
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +185 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +162 -13
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +19 -17
|
@@ -1,125 +1,165 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Hook for cursor-based pagination with polling.
|
|
4
|
+
*
|
|
5
|
+
* Design:
|
|
6
|
+
* - No caching: always fetches fresh data on navigation or poll
|
|
7
|
+
* - Cursor history: tracks the last item ID of each visited page
|
|
8
|
+
* - cursorHistory[N] = last item ID of page N (used as startingAt for page N+1)
|
|
9
|
+
* - Polling: refreshes current page every pollInterval ms
|
|
10
|
+
*
|
|
11
|
+
* Navigation:
|
|
12
|
+
* - Page 0: startingAt = undefined
|
|
13
|
+
* - Page N: startingAt = cursorHistory[N-1]
|
|
14
|
+
* - Going back uses known cursor from history
|
|
15
|
+
*/
|
|
2
16
|
export function useCursorPagination(config) {
|
|
17
|
+
const { fetchPage, pageSize, getItemId, pollInterval = 2000, deps = [], pollingEnabled = true, } = config;
|
|
18
|
+
// State
|
|
3
19
|
const [items, setItems] = React.useState([]);
|
|
4
20
|
const [loading, setLoading] = React.useState(true);
|
|
21
|
+
const [navigating, setNavigating] = React.useState(false);
|
|
5
22
|
const [error, setError] = React.useState(null);
|
|
6
23
|
const [currentPage, setCurrentPage] = React.useState(0);
|
|
7
|
-
const [totalCount, setTotalCount] = React.useState(0);
|
|
8
24
|
const [hasMore, setHasMore] = React.useState(false);
|
|
9
|
-
const [
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
25
|
+
const [totalCount, setTotalCount] = React.useState(0);
|
|
26
|
+
// Cursor history: cursorHistory[N] = last item ID of page N
|
|
27
|
+
// Used to determine startingAt for page N+1
|
|
28
|
+
const cursorHistoryRef = React.useRef([]);
|
|
29
|
+
// Track if component is mounted
|
|
30
|
+
const isMountedRef = React.useRef(true);
|
|
31
|
+
// Track if we're currently fetching (to prevent concurrent fetches)
|
|
32
|
+
const isFetchingRef = React.useRef(false);
|
|
33
|
+
// Store stable references to config
|
|
34
|
+
const fetchPageRef = React.useRef(fetchPage);
|
|
35
|
+
const getItemIdRef = React.useRef(getItemId);
|
|
36
|
+
const pageSizeRef = React.useRef(pageSize);
|
|
37
|
+
// Keep refs in sync
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
fetchPageRef.current = fetchPage;
|
|
40
|
+
}, [fetchPage]);
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
getItemIdRef.current = getItemId;
|
|
43
|
+
}, [getItemId]);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
pageSizeRef.current = pageSize;
|
|
46
|
+
}, [pageSize]);
|
|
47
|
+
// Cleanup on unmount
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
isMountedRef.current = true;
|
|
50
|
+
return () => {
|
|
51
|
+
isMountedRef.current = false;
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
/**
|
|
55
|
+
* Fetch a specific page
|
|
56
|
+
* @param page - Page number to fetch (0-indexed)
|
|
57
|
+
* @param isInitialLoad - Whether this is the initial load (shows loading state)
|
|
58
|
+
* @param isNavigation - Whether this is a page navigation (shows navigating state)
|
|
59
|
+
*/
|
|
60
|
+
const fetchPageData = React.useCallback(async (page, isInitialLoad = false, isNavigation = false) => {
|
|
61
|
+
if (!isMountedRef.current)
|
|
62
|
+
return;
|
|
63
|
+
if (isFetchingRef.current)
|
|
64
|
+
return;
|
|
65
|
+
isFetchingRef.current = true;
|
|
14
66
|
try {
|
|
15
67
|
if (isInitialLoad) {
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
setLoading(true);
|
|
19
|
-
// Check cache first (skip on refresh)
|
|
20
|
-
if (!isInitialLoad && pageCache.current.has(currentPage)) {
|
|
21
|
-
setItems(pageCache.current.get(currentPage) || []);
|
|
22
|
-
setLoading(false);
|
|
23
|
-
return;
|
|
68
|
+
setLoading(true);
|
|
24
69
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const startingAt = currentPage > 0
|
|
28
|
-
? lastIdCache.current.get(currentPage - 1)
|
|
29
|
-
: undefined;
|
|
30
|
-
// Build query params
|
|
31
|
-
const queryParams = {
|
|
32
|
-
limit: config.pageSize,
|
|
33
|
-
...config.queryParams,
|
|
34
|
-
};
|
|
35
|
-
if (startingAt) {
|
|
36
|
-
queryParams.starting_at = startingAt;
|
|
70
|
+
if (isNavigation) {
|
|
71
|
+
setNavigating(true);
|
|
37
72
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
setError(null);
|
|
74
|
+
// Determine startingAt cursor:
|
|
75
|
+
// - Page 0: undefined
|
|
76
|
+
// - Page N: cursorHistory[N-1] (last item ID from previous page)
|
|
77
|
+
const startingAt = page > 0 ? cursorHistoryRef.current[page - 1] : undefined;
|
|
78
|
+
const result = await fetchPageRef.current({
|
|
79
|
+
limit: pageSizeRef.current,
|
|
80
|
+
startingAt,
|
|
81
|
+
});
|
|
82
|
+
if (!isMountedRef.current)
|
|
83
|
+
return;
|
|
84
|
+
// Update items
|
|
85
|
+
setItems(result.items);
|
|
86
|
+
// Update cursor history for this page
|
|
87
|
+
if (result.items.length > 0) {
|
|
88
|
+
const lastItemId = getItemIdRef.current(result.items[result.items.length - 1]);
|
|
89
|
+
cursorHistoryRef.current[page] = lastItemId;
|
|
51
90
|
}
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
// Update pagination state
|
|
92
|
+
setHasMore(result.hasMore);
|
|
93
|
+
if (result.totalCount !== undefined) {
|
|
94
|
+
setTotalCount(result.totalCount);
|
|
56
95
|
}
|
|
57
|
-
// Update items for current page
|
|
58
|
-
setItems(pageItems);
|
|
59
96
|
}
|
|
60
97
|
catch (err) {
|
|
98
|
+
if (!isMountedRef.current)
|
|
99
|
+
return;
|
|
61
100
|
setError(err);
|
|
62
101
|
}
|
|
63
102
|
finally {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
if (isMountedRef.current) {
|
|
104
|
+
setLoading(false);
|
|
105
|
+
setNavigating(false);
|
|
67
106
|
}
|
|
107
|
+
isFetchingRef.current = false;
|
|
68
108
|
}
|
|
69
|
-
}, [
|
|
70
|
-
//
|
|
109
|
+
}, []);
|
|
110
|
+
// Reset when deps change (e.g., filters, search)
|
|
111
|
+
const depsKey = JSON.stringify(deps);
|
|
71
112
|
React.useEffect(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
// Clear cursor history when deps change
|
|
114
|
+
cursorHistoryRef.current = [];
|
|
115
|
+
setCurrentPage(0);
|
|
116
|
+
setItems([]);
|
|
117
|
+
setHasMore(false);
|
|
118
|
+
setTotalCount(0);
|
|
119
|
+
// Fetch page 0
|
|
120
|
+
fetchPageData(0, true);
|
|
121
|
+
}, [depsKey, fetchPageData]);
|
|
122
|
+
// Polling effect
|
|
75
123
|
React.useEffect(() => {
|
|
76
|
-
if (!
|
|
124
|
+
if (!pollInterval || pollInterval <= 0 || !pollingEnabled) {
|
|
77
125
|
return;
|
|
78
126
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
const timer = setInterval(() => {
|
|
128
|
+
if (isMountedRef.current && !isFetchingRef.current) {
|
|
129
|
+
fetchPageData(currentPage, false);
|
|
130
|
+
}
|
|
131
|
+
}, pollInterval);
|
|
132
|
+
return () => clearInterval(timer);
|
|
133
|
+
}, [pollInterval, pollingEnabled, currentPage, fetchPageData]);
|
|
134
|
+
// Navigation functions
|
|
87
135
|
const nextPage = React.useCallback(() => {
|
|
88
|
-
if (!loading && hasMore) {
|
|
89
|
-
|
|
136
|
+
if (!loading && !navigating && hasMore) {
|
|
137
|
+
const newPage = currentPage + 1;
|
|
138
|
+
setCurrentPage(newPage);
|
|
139
|
+
fetchPageData(newPage, false, true);
|
|
90
140
|
}
|
|
91
|
-
}, [loading, hasMore]);
|
|
141
|
+
}, [loading, navigating, hasMore, currentPage, fetchPageData]);
|
|
92
142
|
const prevPage = React.useCallback(() => {
|
|
93
|
-
if (!loading && currentPage > 0) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const goToPage = React.useCallback((page) => {
|
|
98
|
-
if (!loading && page >= 0) {
|
|
99
|
-
setCurrentPage(page);
|
|
143
|
+
if (!loading && !navigating && currentPage > 0) {
|
|
144
|
+
const newPage = currentPage - 1;
|
|
145
|
+
setCurrentPage(newPage);
|
|
146
|
+
fetchPageData(newPage, false, true);
|
|
100
147
|
}
|
|
101
|
-
}, [loading]);
|
|
148
|
+
}, [loading, navigating, currentPage, fetchPageData]);
|
|
102
149
|
const refresh = React.useCallback(() => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
fetchData(true);
|
|
106
|
-
}, [fetchData]);
|
|
107
|
-
const clearCache = React.useCallback(() => {
|
|
108
|
-
pageCache.current.clear();
|
|
109
|
-
lastIdCache.current.clear();
|
|
110
|
-
}, []);
|
|
150
|
+
fetchPageData(currentPage, false);
|
|
151
|
+
}, [currentPage, fetchPageData]);
|
|
111
152
|
return {
|
|
112
153
|
items,
|
|
113
154
|
loading,
|
|
155
|
+
navigating,
|
|
114
156
|
error,
|
|
115
157
|
currentPage,
|
|
116
|
-
totalCount,
|
|
117
158
|
hasMore,
|
|
118
|
-
|
|
159
|
+
hasPrev: currentPage > 0,
|
|
160
|
+
totalCount,
|
|
119
161
|
nextPage,
|
|
120
162
|
prevPage,
|
|
121
|
-
goToPage,
|
|
122
163
|
refresh,
|
|
123
|
-
clearCache,
|
|
124
164
|
};
|
|
125
165
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to handle Ctrl+C (SIGINT) consistently across all screens
|
|
3
|
+
* Exits the program with proper cleanup of alternate screen buffer
|
|
4
|
+
*/
|
|
5
|
+
import { useInput } from "ink";
|
|
6
|
+
import { exitAlternateScreenBuffer } from "../utils/screen.js";
|
|
7
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
8
|
+
export function useExitOnCtrlC() {
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (key.ctrl && input === "c") {
|
|
11
|
+
exitAlternateScreenBuffer();
|
|
12
|
+
processUtils.exit(130); // Standard exit code for SIGINT
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useStdout } from "ink";
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook to calculate available viewport height for content rendering.
|
|
5
|
+
* Ensures consistent layout calculations across all CLI screens and prevents overflow.
|
|
6
|
+
*
|
|
7
|
+
* @param options Configuration for viewport calculation
|
|
8
|
+
* @returns Viewport dimensions including available height for content
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { viewportHeight } = useViewportHeight({ overhead: 10 });
|
|
13
|
+
* const pageSize = viewportHeight; // Use for dynamic page sizing
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function useViewportHeight(options = {}) {
|
|
17
|
+
const { overhead = 0, minHeight = 5, maxHeight = 100 } = options;
|
|
18
|
+
const { stdout } = useStdout();
|
|
19
|
+
// Sample terminal dimensions ONCE and use fixed values - no reactive dependencies
|
|
20
|
+
// This prevents re-renders and Yoga WASM crashes from dynamic resizing
|
|
21
|
+
// CRITICAL: Initialize with safe fallback values to prevent null/undefined
|
|
22
|
+
const dimensions = React.useRef({
|
|
23
|
+
width: 120,
|
|
24
|
+
height: 30,
|
|
25
|
+
});
|
|
26
|
+
// Only sample on first call when still at default values
|
|
27
|
+
if (dimensions.current.width === 120 && dimensions.current.height === 30) {
|
|
28
|
+
// Only sample if stdout has valid dimensions
|
|
29
|
+
const sampledWidth = stdout?.columns && stdout.columns > 0 ? stdout.columns : 120;
|
|
30
|
+
const sampledHeight = stdout?.rows && stdout.rows > 0 ? stdout.rows : 30;
|
|
31
|
+
// Always enforce safe bounds to prevent Yoga crashes
|
|
32
|
+
dimensions.current = {
|
|
33
|
+
width: Math.max(80, Math.min(200, sampledWidth)),
|
|
34
|
+
height: Math.max(20, Math.min(100, sampledHeight)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const terminalHeight = dimensions.current.height;
|
|
38
|
+
const terminalWidth = dimensions.current.width;
|
|
39
|
+
// Calculate viewport height with bounds
|
|
40
|
+
const viewportHeight = Math.max(minHeight, Math.min(maxHeight, terminalHeight - overhead));
|
|
41
|
+
// Removed console.logs to prevent rendering interference
|
|
42
|
+
return {
|
|
43
|
+
viewportHeight,
|
|
44
|
+
terminalHeight,
|
|
45
|
+
terminalWidth,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/mcp/server-http.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { getClient } from "../utils/client.js";
|
|
6
6
|
import express from "express";
|
|
7
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
7
8
|
// Define available tools for the MCP server
|
|
8
9
|
const TOOLS = [
|
|
9
10
|
{
|
|
@@ -412,5 +413,5 @@ async function main() {
|
|
|
412
413
|
}
|
|
413
414
|
main().catch((error) => {
|
|
414
415
|
console.error("Fatal error in main():", error);
|
|
415
|
-
|
|
416
|
+
processUtils.exit(1);
|
|
416
417
|
});
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,7 +2,62 @@
|
|
|
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 { VERSION } from "@runloop/api-client/version.js";
|
|
7
|
+
import Conf from "conf";
|
|
8
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
9
|
+
let configInstance = null;
|
|
10
|
+
function getConfigInstance() {
|
|
11
|
+
if (!configInstance) {
|
|
12
|
+
configInstance = new Conf({
|
|
13
|
+
projectName: "runloop-cli",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return configInstance;
|
|
17
|
+
}
|
|
18
|
+
function getConfig() {
|
|
19
|
+
// Check environment variable first
|
|
20
|
+
const envApiKey = process.env.RUNLOOP_API_KEY;
|
|
21
|
+
if (envApiKey) {
|
|
22
|
+
return { apiKey: envApiKey };
|
|
23
|
+
}
|
|
24
|
+
// Fall back to stored config
|
|
25
|
+
try {
|
|
26
|
+
const config = getConfigInstance();
|
|
27
|
+
const apiKey = config.get("apiKey");
|
|
28
|
+
return { apiKey };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error("Warning: Failed to load config:", error);
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function getBaseUrl() {
|
|
36
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
37
|
+
switch (env) {
|
|
38
|
+
case "dev":
|
|
39
|
+
return "https://api.runloop.pro";
|
|
40
|
+
case "prod":
|
|
41
|
+
default:
|
|
42
|
+
return "https://api.runloop.ai";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getClient() {
|
|
46
|
+
const config = getConfig();
|
|
47
|
+
if (!config.apiKey) {
|
|
48
|
+
throw new Error("API key not configured. Please set RUNLOOP_API_KEY environment variable or run: rli auth");
|
|
49
|
+
}
|
|
50
|
+
const baseURL = getBaseUrl();
|
|
51
|
+
return new Runloop({
|
|
52
|
+
bearerToken: config.apiKey,
|
|
53
|
+
baseURL,
|
|
54
|
+
timeout: 10000, // 10 seconds instead of default 30 seconds
|
|
55
|
+
maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
|
|
56
|
+
defaultHeaders: {
|
|
57
|
+
"User-Agent": `Runloop/JS ${VERSION} - CLI MCP`,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
6
61
|
// Define available tools for the MCP server
|
|
7
62
|
const TOOLS = [
|
|
8
63
|
{
|
|
@@ -386,12 +441,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
386
441
|
});
|
|
387
442
|
// Start the server
|
|
388
443
|
async function main() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
444
|
+
try {
|
|
445
|
+
console.error("[MCP] Starting Runloop MCP server...");
|
|
446
|
+
const transport = new StdioServerTransport();
|
|
447
|
+
console.error("[MCP] Created stdio transport");
|
|
448
|
+
await server.connect(transport);
|
|
449
|
+
// Log to stderr so it doesn't interfere with MCP protocol on stdout
|
|
450
|
+
console.error("[MCP] Server initialization complete, waiting for requests...");
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
console.error("[MCP] Error in main():", error);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
393
456
|
}
|
|
394
457
|
main().catch((error) => {
|
|
395
|
-
console.error("Fatal error in main():", error);
|
|
396
|
-
|
|
458
|
+
console.error("[MCP] Fatal error in main():", error);
|
|
459
|
+
console.error("[MCP] Stack trace:", error instanceof Error ? error.stack : "N/A");
|
|
460
|
+
processUtils.exit(1);
|
|
397
461
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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 { BlueprintLogsScreen } from "../screens/BlueprintLogsScreen.js";
|
|
20
|
+
import { SnapshotListScreen } from "../screens/SnapshotListScreen.js";
|
|
21
|
+
import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
|
|
22
|
+
/**
|
|
23
|
+
* Router component that renders the current screen
|
|
24
|
+
* Implements memory cleanup on route changes
|
|
25
|
+
*
|
|
26
|
+
* Uses React key prop to force complete unmount/remount on screen changes,
|
|
27
|
+
* which prevents Yoga WASM errors during transitions.
|
|
28
|
+
*/
|
|
29
|
+
export function Router() {
|
|
30
|
+
const { currentScreen, params } = useNavigation();
|
|
31
|
+
const prevScreenRef = React.useRef(null);
|
|
32
|
+
// Memory cleanup on route changes
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const prevScreen = prevScreenRef.current;
|
|
35
|
+
if (prevScreen && prevScreen !== currentScreen) {
|
|
36
|
+
// Immediate cleanup without delay - React's key-based remount handles timing
|
|
37
|
+
switch (prevScreen) {
|
|
38
|
+
case "devbox-list":
|
|
39
|
+
case "devbox-detail":
|
|
40
|
+
case "devbox-actions":
|
|
41
|
+
case "devbox-create":
|
|
42
|
+
// Clear devbox data when leaving devbox screens
|
|
43
|
+
// Keep cache if we're still in devbox context
|
|
44
|
+
if (!currentScreen.startsWith("devbox")) {
|
|
45
|
+
useDevboxStore.getState().clearAll();
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "blueprint-list":
|
|
49
|
+
case "blueprint-detail":
|
|
50
|
+
case "blueprint-logs":
|
|
51
|
+
if (!currentScreen.startsWith("blueprint")) {
|
|
52
|
+
useBlueprintStore.getState().clearAll();
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
case "snapshot-list":
|
|
56
|
+
case "snapshot-detail":
|
|
57
|
+
if (!currentScreen.startsWith("snapshot")) {
|
|
58
|
+
useSnapshotStore.getState().clearAll();
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
prevScreenRef.current = currentScreen;
|
|
64
|
+
}, [currentScreen]);
|
|
65
|
+
// CRITICAL: Use key prop to force React to completely unmount old component
|
|
66
|
+
// and mount new component, preventing race conditions during screen transitions.
|
|
67
|
+
// The key ensures React treats this as a completely new component tree.
|
|
68
|
+
// Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
|
|
69
|
+
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 === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...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}`));
|
|
70
|
+
}
|
|
@@ -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,74 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BlueprintLogsScreen - Screen for viewing blueprint build logs
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
8
|
+
import { LogsViewer } from "../components/LogsViewer.js";
|
|
9
|
+
import { Header } from "../components/Header.js";
|
|
10
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
11
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
12
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
13
|
+
import { getBlueprintLogs } from "../services/blueprintService.js";
|
|
14
|
+
import { colors } from "../utils/theme.js";
|
|
15
|
+
export function BlueprintLogsScreen({ blueprintId }) {
|
|
16
|
+
const { goBack, params } = useNavigation();
|
|
17
|
+
const [logs, setLogs] = React.useState([]);
|
|
18
|
+
const [loading, setLoading] = React.useState(true);
|
|
19
|
+
const [error, setError] = React.useState(null);
|
|
20
|
+
// Use blueprintId from props or params
|
|
21
|
+
const id = blueprintId || params.blueprintId;
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (!id) {
|
|
24
|
+
goBack();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
const fetchLogs = async () => {
|
|
29
|
+
try {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
const blueprintLogs = await getBlueprintLogs(id);
|
|
33
|
+
if (!cancelled) {
|
|
34
|
+
setLogs(Array.isArray(blueprintLogs) ? blueprintLogs : []);
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (!cancelled) {
|
|
40
|
+
setError(err);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
fetchLogs();
|
|
46
|
+
return () => {
|
|
47
|
+
cancelled = true;
|
|
48
|
+
};
|
|
49
|
+
}, [id, goBack]);
|
|
50
|
+
if (!id) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Get blueprint name from params if available (for breadcrumb)
|
|
54
|
+
const blueprintName = params.blueprintName || id;
|
|
55
|
+
if (loading) {
|
|
56
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
57
|
+
{ label: "Blueprints" },
|
|
58
|
+
{ label: blueprintName },
|
|
59
|
+
{ label: "Logs", active: true },
|
|
60
|
+
] }), _jsx(Header, { title: "Loading Logs" }), _jsx(SpinnerComponent, { message: "Fetching blueprint logs..." })] }));
|
|
61
|
+
}
|
|
62
|
+
if (error) {
|
|
63
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
64
|
+
{ label: "Blueprints" },
|
|
65
|
+
{ label: blueprintName },
|
|
66
|
+
{ label: "Logs", active: true },
|
|
67
|
+
] }), _jsx(Header, { title: "Error" }), _jsx(ErrorMessage, { message: "Failed to load blueprint logs", error: error }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [q] or [esc] to go back" }) })] }));
|
|
68
|
+
}
|
|
69
|
+
return (_jsx(LogsViewer, { logs: logs, breadcrumbItems: [
|
|
70
|
+
{ label: "Blueprints" },
|
|
71
|
+
{ label: blueprintName },
|
|
72
|
+
{ label: "Logs", active: true },
|
|
73
|
+
], onBack: goBack, title: "Blueprint Build Logs" }));
|
|
74
|
+
}
|
|
@@ -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
|
+
}
|