@runloop/rl-cli 0.1.1 → 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.
Files changed (96) hide show
  1. package/README.md +54 -0
  2. package/dist/cli.js +73 -60
  3. package/dist/commands/auth.js +0 -1
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +215 -213
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/blueprint/preview.js +42 -38
  9. package/dist/commands/config.js +117 -0
  10. package/dist/commands/devbox/create.js +120 -40
  11. package/dist/commands/devbox/delete.js +17 -33
  12. package/dist/commands/devbox/download.js +29 -43
  13. package/dist/commands/devbox/exec.js +22 -39
  14. package/dist/commands/devbox/execAsync.js +20 -37
  15. package/dist/commands/devbox/get.js +13 -35
  16. package/dist/commands/devbox/getAsync.js +12 -34
  17. package/dist/commands/devbox/list.js +241 -402
  18. package/dist/commands/devbox/logs.js +20 -38
  19. package/dist/commands/devbox/read.js +29 -43
  20. package/dist/commands/devbox/resume.js +13 -35
  21. package/dist/commands/devbox/rsync.js +26 -78
  22. package/dist/commands/devbox/scp.js +25 -79
  23. package/dist/commands/devbox/sendStdin.js +41 -0
  24. package/dist/commands/devbox/shutdown.js +13 -35
  25. package/dist/commands/devbox/ssh.js +45 -78
  26. package/dist/commands/devbox/suspend.js +13 -35
  27. package/dist/commands/devbox/tunnel.js +36 -88
  28. package/dist/commands/devbox/upload.js +28 -36
  29. package/dist/commands/devbox/write.js +29 -44
  30. package/dist/commands/mcp-install.js +4 -3
  31. package/dist/commands/menu.js +24 -66
  32. package/dist/commands/object/delete.js +12 -34
  33. package/dist/commands/object/download.js +26 -74
  34. package/dist/commands/object/get.js +12 -34
  35. package/dist/commands/object/list.js +15 -93
  36. package/dist/commands/object/upload.js +35 -96
  37. package/dist/commands/snapshot/create.js +23 -39
  38. package/dist/commands/snapshot/delete.js +17 -33
  39. package/dist/commands/snapshot/get.js +16 -0
  40. package/dist/commands/snapshot/list.js +309 -80
  41. package/dist/commands/snapshot/status.js +12 -34
  42. package/dist/components/ActionsPopup.js +63 -39
  43. package/dist/components/Breadcrumb.js +10 -52
  44. package/dist/components/DevboxActionsMenu.js +182 -110
  45. package/dist/components/DevboxCreatePage.js +12 -7
  46. package/dist/components/DevboxDetailPage.js +76 -28
  47. package/dist/components/ErrorBoundary.js +29 -0
  48. package/dist/components/ErrorMessage.js +10 -2
  49. package/dist/components/Header.js +12 -4
  50. package/dist/components/InteractiveSpawn.js +94 -0
  51. package/dist/components/MainMenu.js +36 -32
  52. package/dist/components/MetadataDisplay.js +4 -4
  53. package/dist/components/OperationsMenu.js +1 -1
  54. package/dist/components/ResourceActionsMenu.js +4 -4
  55. package/dist/components/ResourceListView.js +46 -34
  56. package/dist/components/Spinner.js +7 -2
  57. package/dist/components/StatusBadge.js +1 -1
  58. package/dist/components/SuccessMessage.js +12 -2
  59. package/dist/components/Table.js +16 -6
  60. package/dist/hooks/useCursorPagination.js +125 -85
  61. package/dist/hooks/useExitOnCtrlC.js +14 -0
  62. package/dist/hooks/useViewportHeight.js +47 -0
  63. package/dist/mcp/server.js +65 -6
  64. package/dist/router/Router.js +68 -0
  65. package/dist/router/types.js +1 -0
  66. package/dist/screens/BlueprintListScreen.js +7 -0
  67. package/dist/screens/DevboxActionsScreen.js +25 -0
  68. package/dist/screens/DevboxCreateScreen.js +11 -0
  69. package/dist/screens/DevboxDetailScreen.js +60 -0
  70. package/dist/screens/DevboxListScreen.js +23 -0
  71. package/dist/screens/LogsSessionScreen.js +49 -0
  72. package/dist/screens/MenuScreen.js +23 -0
  73. package/dist/screens/SSHSessionScreen.js +55 -0
  74. package/dist/screens/SnapshotListScreen.js +7 -0
  75. package/dist/services/blueprintService.js +105 -0
  76. package/dist/services/devboxService.js +215 -0
  77. package/dist/services/snapshotService.js +81 -0
  78. package/dist/store/blueprintStore.js +89 -0
  79. package/dist/store/devboxStore.js +105 -0
  80. package/dist/store/index.js +7 -0
  81. package/dist/store/navigationStore.js +101 -0
  82. package/dist/store/snapshotStore.js +87 -0
  83. package/dist/utils/CommandExecutor.js +53 -24
  84. package/dist/utils/client.js +0 -2
  85. package/dist/utils/config.js +20 -90
  86. package/dist/utils/interactiveCommand.js +3 -2
  87. package/dist/utils/logFormatter.js +162 -0
  88. package/dist/utils/memoryMonitor.js +85 -0
  89. package/dist/utils/output.js +150 -59
  90. package/dist/utils/screen.js +23 -0
  91. package/dist/utils/ssh.js +3 -1
  92. package/dist/utils/sshSession.js +5 -29
  93. package/dist/utils/terminalDetection.js +97 -0
  94. package/dist/utils/terminalSync.js +39 -0
  95. package/dist/utils/theme.js +147 -13
  96. package/package.json +16 -13
@@ -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 { getClient } from "../utils/client.js";
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
- const transport = new StdioServerTransport();
390
- await server.connect(transport);
391
- // Log to stderr so it doesn't interfere with MCP protocol on stdout
392
- console.error("Runloop MCP server running on stdio");
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
+ }