@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.
Files changed (105) hide show
  1. package/README.md +54 -10
  2. package/dist/cli.js +79 -72
  3. package/dist/commands/auth.js +2 -2
  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 +278 -230
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/config.js +118 -0
  9. package/dist/commands/devbox/create.js +120 -40
  10. package/dist/commands/devbox/delete.js +17 -33
  11. package/dist/commands/devbox/download.js +29 -43
  12. package/dist/commands/devbox/exec.js +22 -39
  13. package/dist/commands/devbox/execAsync.js +20 -37
  14. package/dist/commands/devbox/get.js +13 -35
  15. package/dist/commands/devbox/getAsync.js +12 -34
  16. package/dist/commands/devbox/list.js +241 -402
  17. package/dist/commands/devbox/logs.js +20 -38
  18. package/dist/commands/devbox/read.js +29 -43
  19. package/dist/commands/devbox/resume.js +13 -35
  20. package/dist/commands/devbox/rsync.js +26 -78
  21. package/dist/commands/devbox/scp.js +25 -79
  22. package/dist/commands/devbox/sendStdin.js +41 -0
  23. package/dist/commands/devbox/shutdown.js +13 -35
  24. package/dist/commands/devbox/ssh.js +46 -78
  25. package/dist/commands/devbox/suspend.js +13 -35
  26. package/dist/commands/devbox/tunnel.js +37 -88
  27. package/dist/commands/devbox/upload.js +28 -36
  28. package/dist/commands/devbox/write.js +29 -44
  29. package/dist/commands/mcp-http.js +6 -5
  30. package/dist/commands/mcp-install.js +12 -10
  31. package/dist/commands/mcp.js +5 -4
  32. package/dist/commands/menu.js +26 -67
  33. package/dist/commands/object/delete.js +12 -34
  34. package/dist/commands/object/download.js +26 -74
  35. package/dist/commands/object/get.js +12 -34
  36. package/dist/commands/object/list.js +15 -93
  37. package/dist/commands/object/upload.js +35 -96
  38. package/dist/commands/snapshot/create.js +23 -39
  39. package/dist/commands/snapshot/delete.js +17 -33
  40. package/dist/commands/snapshot/get.js +16 -0
  41. package/dist/commands/snapshot/list.js +309 -80
  42. package/dist/commands/snapshot/status.js +12 -34
  43. package/dist/components/ActionsPopup.js +64 -39
  44. package/dist/components/Banner.js +7 -1
  45. package/dist/components/Breadcrumb.js +11 -48
  46. package/dist/components/DevboxActionsMenu.js +117 -207
  47. package/dist/components/DevboxCreatePage.js +12 -7
  48. package/dist/components/DevboxDetailPage.js +76 -28
  49. package/dist/components/ErrorBoundary.js +29 -0
  50. package/dist/components/ErrorMessage.js +10 -2
  51. package/dist/components/Header.js +12 -4
  52. package/dist/components/InteractiveSpawn.js +104 -0
  53. package/dist/components/LogsViewer.js +169 -0
  54. package/dist/components/MainMenu.js +37 -33
  55. package/dist/components/MetadataDisplay.js +4 -4
  56. package/dist/components/OperationsMenu.js +1 -1
  57. package/dist/components/ResourceActionsMenu.js +4 -4
  58. package/dist/components/ResourceListView.js +46 -34
  59. package/dist/components/Spinner.js +7 -2
  60. package/dist/components/StatusBadge.js +1 -1
  61. package/dist/components/SuccessMessage.js +12 -2
  62. package/dist/components/Table.js +16 -6
  63. package/dist/components/UpdateNotification.js +56 -0
  64. package/dist/hooks/useCursorPagination.js +125 -85
  65. package/dist/hooks/useExitOnCtrlC.js +15 -0
  66. package/dist/hooks/useViewportHeight.js +47 -0
  67. package/dist/mcp/server-http.js +2 -1
  68. package/dist/mcp/server.js +71 -7
  69. package/dist/router/Router.js +70 -0
  70. package/dist/router/types.js +1 -0
  71. package/dist/screens/BlueprintListScreen.js +7 -0
  72. package/dist/screens/BlueprintLogsScreen.js +74 -0
  73. package/dist/screens/DevboxActionsScreen.js +25 -0
  74. package/dist/screens/DevboxCreateScreen.js +11 -0
  75. package/dist/screens/DevboxDetailScreen.js +60 -0
  76. package/dist/screens/DevboxListScreen.js +23 -0
  77. package/dist/screens/LogsSessionScreen.js +49 -0
  78. package/dist/screens/MenuScreen.js +23 -0
  79. package/dist/screens/SSHSessionScreen.js +55 -0
  80. package/dist/screens/SnapshotListScreen.js +7 -0
  81. package/dist/services/blueprintService.js +101 -0
  82. package/dist/services/devboxService.js +215 -0
  83. package/dist/services/snapshotService.js +81 -0
  84. package/dist/store/blueprintStore.js +89 -0
  85. package/dist/store/devboxStore.js +105 -0
  86. package/dist/store/index.js +7 -0
  87. package/dist/store/navigationStore.js +101 -0
  88. package/dist/store/snapshotStore.js +87 -0
  89. package/dist/utils/client.js +4 -2
  90. package/dist/utils/config.js +22 -111
  91. package/dist/utils/interactiveCommand.js +3 -2
  92. package/dist/utils/logFormatter.js +208 -0
  93. package/dist/utils/memoryMonitor.js +85 -0
  94. package/dist/utils/output.js +153 -61
  95. package/dist/utils/process.js +106 -0
  96. package/dist/utils/processUtils.js +135 -0
  97. package/dist/utils/screen.js +61 -0
  98. package/dist/utils/ssh.js +6 -3
  99. package/dist/utils/sshSession.js +5 -29
  100. package/dist/utils/terminalDetection.js +185 -0
  101. package/dist/utils/terminalSync.js +39 -0
  102. package/dist/utils/theme.js +162 -13
  103. package/dist/utils/versionCheck.js +53 -0
  104. package/dist/version.js +12 -0
  105. package/package.json +19 -17
@@ -2,44 +2,69 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import figures from "figures";
4
4
  import chalk from "chalk";
5
- export const ActionsPopup = ({ devbox, operations, selectedOperation, onClose, }) => {
6
- // Calculate the maximum width needed
7
- const maxLabelLength = Math.max(...operations.map((op) => op.label.length));
8
- const contentWidth = maxLabelLength + 12; // Content + icon + pointer + shortcuts
9
- // Strip ANSI codes to get real length, then pad
10
- const stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, "");
11
- const bgLine = (content) => {
12
- const cleanLength = stripAnsi(content).length;
13
- const padding = Math.max(0, contentWidth - cleanLength);
14
- return chalk.bgBlack(content + " ".repeat(padding));
5
+ import { getChalkTextColor, getChalkColor } from "../utils/theme.js";
6
+ export const ActionsPopup = ({ devbox: _devbox, operations, selectedOperation, onClose: _onClose, }) => {
7
+ // Calculate max width needed for content (visible characters only)
8
+ // CRITICAL: Ensure all values are valid numbers to prevent Yoga crashes
9
+ const maxContentWidth = Math.max(...operations.map((op) => {
10
+ const lineText = `${figures.pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
11
+ const len = lineText.length;
12
+ return Number.isFinite(len) && len > 0 ? len : 0;
13
+ }), `${figures.play} Quick Actions`.length, `${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`.length, 40);
14
+ // Add horizontal padding to width (2 spaces on each side = 4 total)
15
+ // Plus 2 for border characters = 6 total extra
16
+ // CRITICAL: Validate all computed widths are positive integers
17
+ const contentWidth = Math.max(10, maxContentWidth + 4);
18
+ // Get background color chalk function - use theme colors to match the theme mode
19
+ // In light mode, use light background; in dark mode, use dark background
20
+ const popupBgHex = getChalkColor("background");
21
+ const popupTextHex = getChalkColor("text");
22
+ const bgColorFn = chalk.bgHex(popupBgHex);
23
+ const textColorFn = chalk.hex(popupTextHex);
24
+ // Helper to create background lines with proper padding including left/right margins
25
+ const createBgLine = (styledContent, plainContent) => {
26
+ const visibleLength = plainContent.length;
27
+ // CRITICAL: Validate repeat count is non-negative integer
28
+ const repeatCount = Math.max(0, Math.floor(maxContentWidth - visibleLength));
29
+ const rightPadding = " ".repeat(repeatCount);
30
+ // Apply background to left padding + content + right padding
31
+ return bgColorFn(" " + styledContent + rightPadding + " ");
15
32
  };
16
- // Render all lines with background
17
- const lines = [
18
- bgLine(chalk.cyan.bold(` ${figures.play} Quick Actions`)),
19
- chalk.bgBlack(" ".repeat(contentWidth)),
20
- ...operations.map((op, index) => {
21
- const isSelected = index === selectedOperation;
22
- const pointer = isSelected ? figures.pointer : " ";
23
- const content = ` ${pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
24
- let styled;
25
- if (isSelected) {
26
- const colorFn = chalk[op.color];
27
- styled =
28
- typeof colorFn === "function"
29
- ? colorFn.bold(content)
30
- : chalk.white.bold(content);
31
- }
32
- else {
33
- styled = chalk.gray(content);
34
- }
35
- return bgLine(styled);
36
- }),
37
- chalk.bgBlack(" ".repeat(contentWidth)),
38
- bgLine(chalk.gray.dim(` ${figures.arrowUp}${figures.arrowDown} Nav • [Enter]`)),
39
- bgLine(chalk.gray.dim(` [Esc] Close`)),
40
- ];
41
- // Draw custom border with background to fill gaps
42
- const borderTop = chalk.cyan("╭" + "─".repeat(contentWidth) + "╮");
43
- const borderBottom = chalk.cyan("╰" + "─".repeat(contentWidth) + "╯");
44
- return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), lines.map((line, i) => (_jsxs(Text, { children: [chalk.cyan("│"), line, chalk.cyan("│")] }, i))), _jsx(Text, { children: borderBottom })] }) }));
33
+ // Create empty line with full background
34
+ // CRITICAL: Validate repeat count is positive integer
35
+ const emptyLine = bgColorFn(" ".repeat(Math.max(1, Math.floor(contentWidth))));
36
+ // Create border lines with background and integrated title
37
+ const title = `${figures.play} Quick Actions`;
38
+ // The content between and ╮ should be exactly contentWidth
39
+ // Format: " title ─────"
40
+ const titleWithSpaces = ` ${title} `;
41
+ const titleTotalLength = titleWithSpaces.length + 1; // +1 for leading dash
42
+ // CRITICAL: Validate repeat counts are non-negative integers
43
+ const remainingDashes = Math.max(0, Math.floor(contentWidth - titleTotalLength));
44
+ // Use theme primary color for borders to match theme
45
+ const borderColorFn = getChalkTextColor("primary");
46
+ const borderTop = bgColorFn(borderColorFn("╭─" + titleWithSpaces + "─".repeat(remainingDashes) + "╮"));
47
+ // CRITICAL: Validate contentWidth is a positive integer
48
+ const borderBottom = bgColorFn(borderColorFn("╰" + "─".repeat(Math.max(1, Math.floor(contentWidth))) + "╯"));
49
+ const borderSide = (content) => {
50
+ return bgColorFn(borderColorFn("│") + content + borderColorFn("│"));
51
+ };
52
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), _jsx(Text, { children: borderSide(emptyLine) }), operations.map((op, index) => {
53
+ const isSelected = index === selectedOperation;
54
+ const pointer = isSelected ? figures.pointer : " ";
55
+ const lineText = `${pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
56
+ let styledLine;
57
+ if (isSelected) {
58
+ // Selected: use operation-specific color for icon and label
59
+ const opColor = op.color;
60
+ const colorFn = chalk[opColor] || textColorFn;
61
+ styledLine = `${textColorFn(pointer)} ${colorFn(op.icon)} ${colorFn.bold(op.label)} ${textColorFn(`[${op.shortcut}]`)}`;
62
+ }
63
+ else {
64
+ // Unselected: use theme's textDim color for dimmed text
65
+ const dimFn = getChalkTextColor("textDim");
66
+ styledLine = `${dimFn(pointer)} ${dimFn(op.icon)} ${dimFn(op.label)} ${dimFn(`[${op.shortcut}]`)}`;
67
+ }
68
+ return (_jsx(Text, { children: borderSide(createBgLine(styledLine, lineText)) }, op.key));
69
+ }), _jsx(Text, { children: borderSide(emptyLine) }), _jsx(Text, { children: borderSide(createBgLine(textColorFn(`${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`), `${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`)) }), _jsx(Text, { children: borderSide(emptyLine) }), _jsx(Text, { children: borderBottom })] }) }));
45
70
  };
@@ -3,6 +3,12 @@ import React from "react";
3
3
  import { Box } from "ink";
4
4
  import BigText from "ink-big-text";
5
5
  import Gradient from "ink-gradient";
6
+ import { isLightMode } from "../utils/theme.js";
6
7
  export const Banner = React.memo(() => {
7
- return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", children: _jsx(Gradient, { name: "vice", children: _jsx(BigText, { text: "RUNLOOP.ai", font: "simple3d" }) }) }));
8
+ // Use theme-aware gradient colors
9
+ // In light mode, use darker/deeper colors for better contrast on light backgrounds
10
+ // "teen" has darker colors (blue/purple) that work well on light backgrounds
11
+ // In dark mode, use the vibrant "vice" gradient (pink/cyan) that works well on dark backgrounds
12
+ const gradientName = isLightMode() ? "teen" : "vice";
13
+ return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", paddingX: 1, children: _jsx(Gradient, { name: gradientName, children: _jsx(BigText, { text: "RUNLOOP.ai", font: "simple3d" }) }) }));
8
14
  });
@@ -2,53 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Box, Text } from "ink";
4
4
  import { colors } from "../utils/theme.js";
5
- import { VERSION } from "../cli.js";
6
- // Version check component
7
- const VersionCheck = () => {
8
- const [updateAvailable, setUpdateAvailable] = React.useState(null);
9
- const [isChecking, setIsChecking] = React.useState(true);
10
- React.useEffect(() => {
11
- const checkForUpdates = async () => {
12
- try {
13
- // Import the utility functions from config
14
- const { checkForUpdates: checkForUpdatesUtil } = await import("../utils/config.js");
15
- // Use the same logic as the non-interactive version
16
- // We'll call the utility function and capture its output
17
- const originalConsoleError = console.error;
18
- let updateMessage = "";
19
- // Capture the console.error output
20
- console.error = (...args) => {
21
- updateMessage = args.join(' ');
22
- originalConsoleError(...args);
23
- };
24
- // Call the update check utility
25
- await checkForUpdatesUtil(false);
26
- // Restore original console.error
27
- console.error = originalConsoleError;
28
- // Parse the update message to extract the latest version
29
- if (updateMessage.includes("Update available:")) {
30
- const match = updateMessage.match(/Update available: .+ → (.+)/);
31
- if (match && match[1]) {
32
- setUpdateAvailable(match[1]);
33
- }
34
- }
35
- }
36
- catch (error) {
37
- // Silently fail
38
- }
39
- finally {
40
- setIsChecking(false);
41
- }
42
- };
43
- checkForUpdates();
44
- }, []);
45
- if (isChecking || !updateAvailable) {
46
- return null;
47
- }
48
- return (_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: "\u2728" }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "Update available:", " "] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: VERSION }), _jsxs(Text, { color: colors.primary, bold: true, children: [" ", "\u2192", " "] }), _jsx(Text, { color: colors.success, bold: true, children: updateAvailable }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Run:", " "] }), _jsx(Text, { color: colors.primary, bold: true, children: "npm install -g @runloop/rl-cli@latest" })] }));
49
- };
50
- export const Breadcrumb = React.memo(({ items, showVersionCheck = false }) => {
5
+ import { UpdateNotification } from "./UpdateNotification.js";
6
+ export const Breadcrumb = ({ items, showVersionCheck = false, }) => {
51
7
  const env = process.env.RUNLOOP_ENV?.toLowerCase();
52
8
  const isDevEnvironment = env === "dev";
53
- return (_jsxs(Box, { marginBottom: 1, paddingX: 1, paddingY: 0, flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: "redBright", bold: true, children: [" ", "(dev)"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u203A", " "] }), items.map((item, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? colors.text : colors.textDim, bold: item.active, dimColor: !item.active, children: item.label }), index < items.length - 1 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u203A", " "] }))] }, index)))] }), showVersionCheck && (_jsx(Box, { paddingX: 2, marginTop: 0, children: _jsx(VersionCheck, {}) }))] }));
54
- });
9
+ return (_jsxs(Box, { marginBottom: 1, paddingX: 0, paddingY: 0, children: [_jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: colors.error, bold: true, children: [" ", "(dev)"] })), _jsx(Text, { color: colors.textDim, children: " \u203A " }), items.map((item, index) => {
10
+ // Limit label length to prevent Yoga layout engine errors
11
+ const MAX_LABEL_LENGTH = 80;
12
+ const truncatedLabel = item.label.length > MAX_LABEL_LENGTH
13
+ ? item.label.substring(0, MAX_LABEL_LENGTH) + "..."
14
+ : item.label;
15
+ return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? colors.primary : colors.textDim, children: truncatedLabel }), index < items.length - 1 && (_jsx(Text, { color: colors.textDim, children: " \u203A " }))] }, index));
16
+ })] }), showVersionCheck && (_jsx(Box, { paddingX: 2, marginTop: 0, children: _jsx(UpdateNotification, {}) }))] }));
17
+ };