@runloop/rl-cli 0.0.3 → 0.1.1

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 (73) hide show
  1. package/README.md +64 -29
  2. package/dist/cli.js +401 -92
  3. package/dist/commands/auth.js +12 -11
  4. package/dist/commands/blueprint/create.js +108 -0
  5. package/dist/commands/blueprint/get.js +37 -0
  6. package/dist/commands/blueprint/list.js +293 -225
  7. package/dist/commands/blueprint/logs.js +40 -0
  8. package/dist/commands/blueprint/preview.js +45 -0
  9. package/dist/commands/devbox/create.js +10 -9
  10. package/dist/commands/devbox/delete.js +8 -8
  11. package/dist/commands/devbox/download.js +49 -0
  12. package/dist/commands/devbox/exec.js +23 -13
  13. package/dist/commands/devbox/execAsync.js +43 -0
  14. package/dist/commands/devbox/get.js +37 -0
  15. package/dist/commands/devbox/getAsync.js +37 -0
  16. package/dist/commands/devbox/list.js +328 -190
  17. package/dist/commands/devbox/logs.js +40 -0
  18. package/dist/commands/devbox/read.js +49 -0
  19. package/dist/commands/devbox/resume.js +37 -0
  20. package/dist/commands/devbox/rsync.js +118 -0
  21. package/dist/commands/devbox/scp.js +122 -0
  22. package/dist/commands/devbox/shutdown.js +37 -0
  23. package/dist/commands/devbox/ssh.js +104 -0
  24. package/dist/commands/devbox/suspend.js +37 -0
  25. package/dist/commands/devbox/tunnel.js +120 -0
  26. package/dist/commands/devbox/upload.js +10 -10
  27. package/dist/commands/devbox/write.js +51 -0
  28. package/dist/commands/mcp-http.js +37 -0
  29. package/dist/commands/mcp-install.js +120 -0
  30. package/dist/commands/mcp.js +30 -0
  31. package/dist/commands/menu.js +20 -20
  32. package/dist/commands/object/delete.js +37 -0
  33. package/dist/commands/object/download.js +88 -0
  34. package/dist/commands/object/get.js +37 -0
  35. package/dist/commands/object/list.js +112 -0
  36. package/dist/commands/object/upload.js +130 -0
  37. package/dist/commands/snapshot/create.js +12 -11
  38. package/dist/commands/snapshot/delete.js +8 -8
  39. package/dist/commands/snapshot/list.js +56 -97
  40. package/dist/commands/snapshot/status.js +37 -0
  41. package/dist/components/ActionsPopup.js +16 -13
  42. package/dist/components/Banner.js +4 -4
  43. package/dist/components/Breadcrumb.js +55 -5
  44. package/dist/components/DetailView.js +7 -4
  45. package/dist/components/DevboxActionsMenu.js +315 -178
  46. package/dist/components/DevboxCard.js +15 -14
  47. package/dist/components/DevboxCreatePage.js +147 -113
  48. package/dist/components/DevboxDetailPage.js +180 -102
  49. package/dist/components/ErrorMessage.js +5 -4
  50. package/dist/components/Header.js +4 -3
  51. package/dist/components/MainMenu.js +34 -33
  52. package/dist/components/MetadataDisplay.js +17 -9
  53. package/dist/components/OperationsMenu.js +6 -5
  54. package/dist/components/ResourceActionsMenu.js +117 -0
  55. package/dist/components/ResourceListView.js +213 -0
  56. package/dist/components/Spinner.js +5 -4
  57. package/dist/components/StatusBadge.js +81 -31
  58. package/dist/components/SuccessMessage.js +4 -3
  59. package/dist/components/Table.example.js +53 -23
  60. package/dist/components/Table.js +19 -11
  61. package/dist/hooks/useCursorPagination.js +125 -0
  62. package/dist/mcp/server-http.js +416 -0
  63. package/dist/mcp/server.js +397 -0
  64. package/dist/utils/CommandExecutor.js +16 -12
  65. package/dist/utils/client.js +7 -7
  66. package/dist/utils/config.js +130 -4
  67. package/dist/utils/interactiveCommand.js +2 -2
  68. package/dist/utils/output.js +17 -17
  69. package/dist/utils/ssh.js +160 -0
  70. package/dist/utils/sshSession.js +16 -12
  71. package/dist/utils/theme.js +22 -0
  72. package/dist/utils/url.js +4 -4
  73. package/package.json +29 -4
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { getClient } from "../../utils/client.js";
4
+ import { Banner } from "../../components/Banner.js";
5
+ import { SpinnerComponent } from "../../components/Spinner.js";
6
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
7
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
8
+ import { createExecutor } from "../../utils/CommandExecutor.js";
9
+ import { getSSHKey, getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
10
+ import { exec } from "child_process";
11
+ import { promisify } from "util";
12
+ const execAsync = promisify(exec);
13
+ const TunnelUI = ({ devboxId, ports }) => {
14
+ const [loading, setLoading] = React.useState(true);
15
+ const [result, setResult] = React.useState(null);
16
+ const [error, setError] = React.useState(null);
17
+ React.useEffect(() => {
18
+ const createTunnel = async () => {
19
+ try {
20
+ // Check if SSH tools are available
21
+ const sshToolsAvailable = await checkSSHTools();
22
+ if (!sshToolsAvailable) {
23
+ throw new Error("SSH tools (ssh, openssl) are not available on this system");
24
+ }
25
+ if (!ports.includes(":")) {
26
+ throw new Error("Ports must be specified as 'local:remote'");
27
+ }
28
+ const [localPort, remotePort] = ports.split(":");
29
+ const client = getClient();
30
+ // Get devbox details to determine user
31
+ const devbox = await client.devboxes.retrieve(devboxId);
32
+ const user = devbox.launch_parameters?.user_parameters?.username || "user";
33
+ // Get SSH key
34
+ const sshInfo = await getSSHKey(devboxId);
35
+ if (!sshInfo) {
36
+ throw new Error("Failed to create SSH key");
37
+ }
38
+ const proxyCommand = getProxyCommand();
39
+ const tunnelCommand = [
40
+ "/usr/bin/ssh",
41
+ "-i",
42
+ sshInfo.keyfilePath,
43
+ "-o",
44
+ `ProxyCommand=${proxyCommand}`,
45
+ "-o",
46
+ "StrictHostKeyChecking=no",
47
+ "-N", // Do not execute a remote command
48
+ "-L",
49
+ `${localPort}:localhost:${remotePort}`,
50
+ `${user}@${sshInfo.url}`,
51
+ ];
52
+ console.log(`Starting tunnel: local port ${localPort} -> remote port ${remotePort}`);
53
+ console.log("Press Ctrl+C to stop the tunnel.");
54
+ // Set up signal handler for graceful shutdown
55
+ const signalHandler = () => {
56
+ console.log("\nStopping tunnel...");
57
+ process.exit(0);
58
+ };
59
+ process.on("SIGINT", signalHandler);
60
+ const { stdout, stderr } = await execAsync(tunnelCommand.join(" "));
61
+ setResult({ localPort, remotePort, stdout, stderr });
62
+ }
63
+ catch (err) {
64
+ setError(err);
65
+ }
66
+ finally {
67
+ setLoading(false);
68
+ }
69
+ };
70
+ createTunnel();
71
+ }, [devboxId, ports]);
72
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Creating SSH tunnel..." }), result && (_jsx(SuccessMessage, { message: "SSH tunnel created", details: `Local port: ${result.localPort}\nRemote port: ${result.remotePort}` })), error && (_jsx(ErrorMessage, { message: "Failed to create SSH tunnel", error: error }))] }));
73
+ };
74
+ export async function createTunnel(devboxId, options) {
75
+ const executor = createExecutor({ output: options.outputFormat });
76
+ await executor.executeAction(async () => {
77
+ // Check if SSH tools are available
78
+ const sshToolsAvailable = await checkSSHTools();
79
+ if (!sshToolsAvailable) {
80
+ throw new Error("SSH tools (ssh, openssl) are not available on this system");
81
+ }
82
+ if (!options.ports.includes(":")) {
83
+ throw new Error("Ports must be specified as 'local:remote'");
84
+ }
85
+ const [localPort, remotePort] = options.ports.split(":");
86
+ const client = executor.getClient();
87
+ // Get devbox details to determine user
88
+ const devbox = await client.devboxes.retrieve(devboxId);
89
+ const user = devbox.launch_parameters?.user_parameters?.username || "user";
90
+ // Get SSH key
91
+ const sshInfo = await getSSHKey(devboxId);
92
+ if (!sshInfo) {
93
+ throw new Error("Failed to create SSH key");
94
+ }
95
+ const proxyCommand = getProxyCommand();
96
+ const tunnelCommand = [
97
+ "/usr/bin/ssh",
98
+ "-i",
99
+ sshInfo.keyfilePath,
100
+ "-o",
101
+ `ProxyCommand=${proxyCommand}`,
102
+ "-o",
103
+ "StrictHostKeyChecking=no",
104
+ "-N", // Do not execute a remote command
105
+ "-L",
106
+ `${localPort}:localhost:${remotePort}`,
107
+ `${user}@${sshInfo.url}`,
108
+ ];
109
+ console.log(`Starting tunnel: local port ${localPort} -> remote port ${remotePort}`);
110
+ console.log("Press Ctrl+C to stop the tunnel.");
111
+ // Set up signal handler for graceful shutdown
112
+ const signalHandler = () => {
113
+ console.log("\nStopping tunnel...");
114
+ process.exit(0);
115
+ };
116
+ process.on("SIGINT", signalHandler);
117
+ const { stdout, stderr } = await execAsync(tunnelCommand.join(" "));
118
+ return { localPort, remotePort, stdout, stderr };
119
+ }, () => _jsx(TunnelUI, { devboxId: devboxId, ports: options.ports }));
120
+ }
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { render } from 'ink';
4
- import { createReadStream } from 'fs';
5
- import { getClient } from '../../utils/client.js';
6
- import { Header } from '../../components/Header.js';
7
- import { SpinnerComponent } from '../../components/Spinner.js';
8
- import { SuccessMessage } from '../../components/SuccessMessage.js';
9
- import { ErrorMessage } from '../../components/ErrorMessage.js';
2
+ import React from "react";
3
+ import { render } from "ink";
4
+ import { createReadStream } from "fs";
5
+ import { getClient } from "../../utils/client.js";
6
+ import { Header } from "../../components/Header.js";
7
+ import { SpinnerComponent } from "../../components/Spinner.js";
8
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
9
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
10
10
  const UploadFileUI = ({ id, file, targetPath }) => {
11
11
  const [loading, setLoading] = React.useState(true);
12
12
  const [success, setSuccess] = React.useState(false);
@@ -16,7 +16,7 @@ const UploadFileUI = ({ id, file, targetPath }) => {
16
16
  try {
17
17
  const client = getClient();
18
18
  const fileStream = createReadStream(file);
19
- const filename = file.split('/').pop() || 'uploaded-file';
19
+ const filename = file.split("/").pop() || "uploaded-file";
20
20
  await client.devboxes.uploadFile(id, {
21
21
  path: targetPath || filename,
22
22
  file: fileStream,
@@ -32,7 +32,7 @@ const UploadFileUI = ({ id, file, targetPath }) => {
32
32
  };
33
33
  upload();
34
34
  }, []);
35
- return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Upload File", subtitle: `Uploading to devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Uploading file..." }), success && (_jsx(SuccessMessage, { message: "File uploaded successfully!", details: `File: ${file}${targetPath ? `\nTarget: ${targetPath}` : ''}` })), error && _jsx(ErrorMessage, { message: "Failed to upload file", error: error })] }));
35
+ return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Upload File", subtitle: `Uploading to devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Uploading file..." }), success && (_jsx(SuccessMessage, { message: "File uploaded successfully!", details: `File: ${file}${targetPath ? `\nTarget: ${targetPath}` : ""}` })), error && _jsx(ErrorMessage, { message: "Failed to upload file", error: error })] }));
36
36
  };
37
37
  export async function uploadFile(id, file, options) {
38
38
  const { waitUntilExit } = render(_jsx(UploadFileUI, { id: id, file: file, targetPath: options.path }));
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { getClient } from "../../utils/client.js";
4
+ import { Banner } from "../../components/Banner.js";
5
+ import { SpinnerComponent } from "../../components/Spinner.js";
6
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
7
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
8
+ import { createExecutor } from "../../utils/CommandExecutor.js";
9
+ import { readFile } from "fs/promises";
10
+ const WriteFileUI = ({ devboxId, inputPath, remotePath }) => {
11
+ const [loading, setLoading] = React.useState(true);
12
+ const [result, setResult] = React.useState(null);
13
+ const [error, setError] = React.useState(null);
14
+ React.useEffect(() => {
15
+ const writeFile = async () => {
16
+ try {
17
+ const client = getClient();
18
+ const contents = await readFile(inputPath, "utf-8");
19
+ await client.devboxes.writeFileContents(devboxId, {
20
+ file_path: remotePath,
21
+ contents,
22
+ });
23
+ setResult({ inputPath, remotePath, size: contents.length });
24
+ }
25
+ catch (err) {
26
+ setError(err);
27
+ }
28
+ finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+ writeFile();
33
+ }, [devboxId, inputPath, remotePath]);
34
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Writing file to devbox..." }), result && (_jsx(SuccessMessage, { message: "File written successfully", details: `Local: ${result.inputPath}\nRemote: ${result.remotePath}\nSize: ${result.size} bytes` })), error && _jsx(ErrorMessage, { message: "Failed to write file", error: error })] }));
35
+ };
36
+ export async function writeFile(devboxId, options) {
37
+ const executor = createExecutor({ output: options.output });
38
+ await executor.executeAction(async () => {
39
+ const client = executor.getClient();
40
+ const contents = await readFile(options.input, "utf-8");
41
+ await client.devboxes.writeFileContents(devboxId, {
42
+ file_path: options.remote,
43
+ contents,
44
+ });
45
+ return {
46
+ inputPath: options.input,
47
+ remotePath: options.remote,
48
+ size: contents.length,
49
+ };
50
+ }, () => (_jsx(WriteFileUI, { devboxId: devboxId, inputPath: options.input, remotePath: options.remote })));
51
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "child_process";
3
+ import { fileURLToPath } from "url";
4
+ import { dirname, join } from "path";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ export async function startMcpHttpServer(port) {
8
+ // Get the path to the compiled MCP HTTP server
9
+ const serverPath = join(__dirname, "../mcp/server-http.js");
10
+ const env = { ...process.env };
11
+ if (port) {
12
+ env.PORT = port.toString();
13
+ }
14
+ console.log(`Starting Runloop MCP HTTP server on port ${port || 3000}...`);
15
+ console.log("Press Ctrl+C to stop\n");
16
+ // Start the MCP HTTP server as a child process
17
+ const serverProcess = spawn("node", [serverPath], {
18
+ stdio: "inherit",
19
+ env,
20
+ });
21
+ serverProcess.on("error", (error) => {
22
+ console.error("Failed to start MCP HTTP server:", error);
23
+ process.exit(1);
24
+ });
25
+ serverProcess.on("exit", (code) => {
26
+ if (code !== 0) {
27
+ console.error(`MCP HTTP server exited with code ${code}`);
28
+ process.exit(code || 1);
29
+ }
30
+ });
31
+ // Handle Ctrl+C
32
+ process.on("SIGINT", () => {
33
+ console.log("\nShutting down MCP HTTP server...");
34
+ serverProcess.kill("SIGINT");
35
+ process.exit(0);
36
+ });
37
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
+ import { homedir, platform } from "os";
4
+ import { join } from "path";
5
+ import { execSync } from "child_process";
6
+ function getClaudeConfigPath() {
7
+ const plat = platform();
8
+ if (plat === "darwin") {
9
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
10
+ }
11
+ else if (plat === "win32") {
12
+ const appData = process.env.APPDATA;
13
+ if (!appData) {
14
+ throw new Error("APPDATA environment variable not found");
15
+ }
16
+ return join(appData, "Claude", "claude_desktop_config.json");
17
+ }
18
+ else {
19
+ // Linux
20
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
21
+ }
22
+ }
23
+ function getRliPath() {
24
+ try {
25
+ const cmd = platform() === "win32" ? "where rli" : "which rli";
26
+ const path = execSync(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
27
+ return path;
28
+ }
29
+ catch (error) {
30
+ // If rli not found in PATH, just use 'rli' and hope it works
31
+ return "rli";
32
+ }
33
+ }
34
+ export async function installMcpConfig() {
35
+ try {
36
+ const configPath = getClaudeConfigPath();
37
+ const rliPath = getRliPath();
38
+ console.log(`\n📍 Claude Desktop config location: ${configPath}`);
39
+ console.log(`📍 rli command location: ${rliPath}\n`);
40
+ // Read or create config
41
+ let config = { mcpServers: {} };
42
+ if (existsSync(configPath)) {
43
+ console.log("✓ Found existing Claude Desktop config");
44
+ const content = readFileSync(configPath, "utf-8");
45
+ try {
46
+ config = JSON.parse(content);
47
+ if (!config.mcpServers) {
48
+ config.mcpServers = {};
49
+ }
50
+ }
51
+ catch (error) {
52
+ console.error("❌ Error: Claude config file exists but is not valid JSON");
53
+ console.error("Please fix the file manually or delete it to create a new one");
54
+ process.exit(1);
55
+ }
56
+ }
57
+ else {
58
+ console.log("✓ No existing config found, will create new one");
59
+ // Create directory if it doesn't exist
60
+ const configDir = join(configPath, "..");
61
+ if (!existsSync(configDir)) {
62
+ mkdirSync(configDir, { recursive: true });
63
+ console.log(`✓ Created directory: ${configDir}`);
64
+ }
65
+ }
66
+ // Check if runloop is already configured
67
+ if (config.mcpServers.runloop) {
68
+ console.log("\n⚠️ Runloop MCP server is already configured in Claude Desktop");
69
+ console.log("\nCurrent configuration:");
70
+ console.log(JSON.stringify(config.mcpServers.runloop, null, 2));
71
+ // Ask if they want to overwrite
72
+ console.log("\n❓ Do you want to overwrite it? (y/N): ");
73
+ // For non-interactive mode, just exit
74
+ if (process.stdin.isTTY) {
75
+ const response = await new Promise((resolve) => {
76
+ process.stdin.once("data", (data) => {
77
+ resolve(data.toString().trim().toLowerCase());
78
+ });
79
+ });
80
+ if (response !== "y" && response !== "yes") {
81
+ console.log("\n✓ Keeping existing configuration");
82
+ process.exit(0);
83
+ }
84
+ }
85
+ else {
86
+ console.log("\n✓ Keeping existing configuration (non-interactive mode)");
87
+ process.exit(0);
88
+ }
89
+ }
90
+ // Add runloop MCP server config
91
+ config.mcpServers.runloop = {
92
+ command: rliPath,
93
+ args: ["mcp", "start"],
94
+ };
95
+ // Write config back
96
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
97
+ console.log("\n✅ Successfully installed Runloop MCP server configuration!");
98
+ console.log("\nConfiguration added:");
99
+ console.log(JSON.stringify({ mcpServers: { runloop: config.mcpServers.runloop } }, null, 2));
100
+ console.log("\n📝 Next steps:");
101
+ console.log("1. Restart Claude Desktop completely (quit and reopen)");
102
+ console.log('2. Ask Claude: "List my devboxes" or "What Runloop tools do you have?"');
103
+ console.log('\n💡 Tip: Make sure you\'ve run "rli auth" to configure your API key first!');
104
+ }
105
+ catch (error) {
106
+ console.error("\n❌ Error installing MCP configuration:", error.message);
107
+ console.error("\n💡 You can manually add this configuration to your Claude Desktop config:");
108
+ console.error(`\nFile location: ${getClaudeConfigPath()}`);
109
+ console.error("\nConfiguration to add:");
110
+ console.error(JSON.stringify({
111
+ mcpServers: {
112
+ runloop: {
113
+ command: "rli",
114
+ args: ["mcp", "start"],
115
+ },
116
+ },
117
+ }, null, 2));
118
+ process.exit(1);
119
+ }
120
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "child_process";
3
+ import { fileURLToPath } from "url";
4
+ import { dirname, join } from "path";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ export async function startMcpServer() {
8
+ // Get the path to the compiled MCP server
9
+ const serverPath = join(__dirname, "../mcp/server.js");
10
+ // Start the MCP server as a child process
11
+ // The server uses stdio transport, so it communicates via stdin/stdout
12
+ const serverProcess = spawn("node", [serverPath], {
13
+ stdio: "inherit", // Pass through stdin/stdout/stderr
14
+ });
15
+ serverProcess.on("error", (error) => {
16
+ console.error("Failed to start MCP server:", error);
17
+ process.exit(1);
18
+ });
19
+ serverProcess.on("exit", (code) => {
20
+ if (code !== 0) {
21
+ console.error(`MCP server exited with code ${code}`);
22
+ process.exit(code || 1);
23
+ }
24
+ });
25
+ // Handle Ctrl+C
26
+ process.on("SIGINT", () => {
27
+ serverProcess.kill("SIGINT");
28
+ process.exit(0);
29
+ });
30
+ }
@@ -1,32 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { render, useApp } from 'ink';
4
- import { MainMenu } from '../components/MainMenu.js';
5
- import { runSSHSession } from '../utils/sshSession.js';
2
+ import React from "react";
3
+ import { render, useApp } from "ink";
4
+ import { MainMenu } from "../components/MainMenu.js";
5
+ import { runSSHSession } from "../utils/sshSession.js";
6
6
  // Import the UI components directly
7
- import { ListDevboxesUI } from './devbox/list.js';
8
- import { ListBlueprintsUI } from './blueprint/list.js';
9
- import { ListSnapshotsUI } from './snapshot/list.js';
10
- import { Box } from 'ink';
11
- const App = ({ onSSHRequest, initialScreen = 'menu', focusDevboxId }) => {
7
+ import { ListDevboxesUI } from "./devbox/list.js";
8
+ import { ListBlueprintsUI } from "./blueprint/list.js";
9
+ import { ListSnapshotsUI } from "./snapshot/list.js";
10
+ import { Box } from "ink";
11
+ const App = ({ onSSHRequest, initialScreen = "menu", focusDevboxId, }) => {
12
12
  const { exit } = useApp();
13
13
  const [currentScreen, setCurrentScreen] = React.useState(initialScreen);
14
- const [, forceUpdate] = React.useReducer(x => x + 1, 0);
14
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
15
15
  const handleMenuSelect = (key) => {
16
16
  setCurrentScreen(key);
17
17
  };
18
18
  const handleBack = () => {
19
- setCurrentScreen('menu');
19
+ setCurrentScreen("menu");
20
20
  };
21
21
  const handleExit = () => {
22
22
  exit();
23
23
  };
24
24
  // Wrap everything in a full-height container
25
- return (_jsxs(Box, { flexDirection: "column", minHeight: process.stdout.rows || 24, children: [currentScreen === 'menu' && _jsx(MainMenu, { onSelect: handleMenuSelect }), currentScreen === 'devboxes' && (_jsx(ListDevboxesUI, { onBack: handleBack, onExit: handleExit, onSSHRequest: onSSHRequest, focusDevboxId: focusDevboxId })), currentScreen === 'blueprints' && _jsx(ListBlueprintsUI, { onBack: handleBack, onExit: handleExit }), currentScreen === 'snapshots' && _jsx(ListSnapshotsUI, { onBack: handleBack, onExit: handleExit })] }));
25
+ return (_jsxs(Box, { flexDirection: "column", minHeight: process.stdout.rows || 24, children: [currentScreen === "menu" && _jsx(MainMenu, { onSelect: handleMenuSelect }), currentScreen === "devboxes" && (_jsx(ListDevboxesUI, { onBack: handleBack, onExit: handleExit, onSSHRequest: onSSHRequest, focusDevboxId: focusDevboxId })), currentScreen === "blueprints" && (_jsx(ListBlueprintsUI, { onBack: handleBack, onExit: handleExit })), currentScreen === "snapshots" && (_jsx(ListSnapshotsUI, { onBack: handleBack, onExit: handleExit }))] }));
26
26
  };
27
- export async function runMainMenu(initialScreen = 'menu', focusDevboxId) {
27
+ export async function runMainMenu(initialScreen = "menu", focusDevboxId) {
28
28
  // Enter alternate screen buffer once at the start
29
- process.stdout.write('\x1b[?1049h');
29
+ process.stdout.write("\x1b[?1049h");
30
30
  let sshSessionConfig = null;
31
31
  let shouldContinue = true;
32
32
  let currentInitialScreen = initialScreen;
@@ -41,21 +41,21 @@ export async function runMainMenu(initialScreen = 'menu', focusDevboxId) {
41
41
  shouldContinue = false;
42
42
  }
43
43
  catch (error) {
44
- console.error('Error in menu:', error);
44
+ console.error("Error in menu:", error);
45
45
  shouldContinue = false;
46
46
  }
47
47
  // If SSH was requested, handle it now after Ink has exited
48
48
  if (sshSessionConfig) {
49
49
  // Exit alternate screen buffer for SSH
50
- process.stdout.write('\x1b[?1049l');
50
+ process.stdout.write("\x1b[?1049l");
51
51
  const result = await runSSHSession(sshSessionConfig);
52
52
  if (result.shouldRestart) {
53
53
  console.clear();
54
54
  console.log(`\nSSH session ended. Returning to menu...\n`);
55
- await new Promise(resolve => setTimeout(resolve, 500));
55
+ await new Promise((resolve) => setTimeout(resolve, 500));
56
56
  // Re-enter alternate screen buffer and return to devboxes list
57
- process.stdout.write('\x1b[?1049h');
58
- currentInitialScreen = 'devboxes';
57
+ process.stdout.write("\x1b[?1049h");
58
+ currentInitialScreen = "devboxes";
59
59
  currentFocusDevboxId = result.returnToDevboxId;
60
60
  shouldContinue = true;
61
61
  }
@@ -65,6 +65,6 @@ export async function runMainMenu(initialScreen = 'menu', focusDevboxId) {
65
65
  }
66
66
  }
67
67
  // Exit alternate screen buffer once at the end
68
- process.stdout.write('\x1b[?1049l');
68
+ process.stdout.write("\x1b[?1049l");
69
69
  process.exit(0);
70
70
  }
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { getClient } from "../../utils/client.js";
4
+ import { Banner } from "../../components/Banner.js";
5
+ import { SpinnerComponent } from "../../components/Spinner.js";
6
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
7
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
8
+ import { createExecutor } from "../../utils/CommandExecutor.js";
9
+ const DeleteObjectUI = ({ objectId }) => {
10
+ const [loading, setLoading] = React.useState(true);
11
+ const [result, setResult] = React.useState(null);
12
+ const [error, setError] = React.useState(null);
13
+ React.useEffect(() => {
14
+ const deleteObject = async () => {
15
+ try {
16
+ const client = getClient();
17
+ const deletedObject = await client.objects.delete(objectId);
18
+ setResult(deletedObject);
19
+ }
20
+ catch (err) {
21
+ setError(err);
22
+ }
23
+ finally {
24
+ setLoading(false);
25
+ }
26
+ };
27
+ deleteObject();
28
+ }, [objectId]);
29
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Deleting object..." }), result && (_jsx(SuccessMessage, { message: "Object deleted successfully", details: `ID: ${result.id}\nName: ${result.name}\nThis action is irreversible` })), error && (_jsx(ErrorMessage, { message: "Failed to delete object", error: error }))] }));
30
+ };
31
+ export async function deleteObject(options) {
32
+ const executor = createExecutor({ output: options.outputFormat });
33
+ await executor.executeAction(async () => {
34
+ const client = executor.getClient();
35
+ return client.objects.delete(options.id);
36
+ }, () => _jsx(DeleteObjectUI, { objectId: options.id }));
37
+ }
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { getClient } from "../../utils/client.js";
4
+ import { Banner } from "../../components/Banner.js";
5
+ import { SpinnerComponent } from "../../components/Spinner.js";
6
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
7
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
8
+ import { createExecutor } from "../../utils/CommandExecutor.js";
9
+ const DownloadObjectUI = ({ objectId, path, extract, durationSeconds }) => {
10
+ const [loading, setLoading] = React.useState(true);
11
+ const [result, setResult] = React.useState(null);
12
+ const [error, setError] = React.useState(null);
13
+ React.useEffect(() => {
14
+ const downloadObject = async () => {
15
+ try {
16
+ const client = getClient();
17
+ // Get the object metadata first
18
+ const object = await client.objects.retrieve(objectId);
19
+ // Get the download URL
20
+ const downloadUrlResponse = await client.objects.download(objectId, {
21
+ duration_seconds: durationSeconds || 3600,
22
+ });
23
+ // Download the file
24
+ const response = await fetch(downloadUrlResponse.download_url);
25
+ if (!response.ok) {
26
+ throw new Error(`Download failed: HTTP ${response.status}`);
27
+ }
28
+ // Handle extraction if requested
29
+ if (extract) {
30
+ // For now, just save to the specified path
31
+ // In a full implementation, you'd handle archive extraction here
32
+ const arrayBuffer = await response.arrayBuffer();
33
+ const buffer = Buffer.from(arrayBuffer);
34
+ await import("fs/promises").then((fs) => fs.writeFile(path, buffer));
35
+ }
36
+ else {
37
+ const arrayBuffer = await response.arrayBuffer();
38
+ const buffer = Buffer.from(arrayBuffer);
39
+ await import("fs/promises").then((fs) => fs.writeFile(path, buffer));
40
+ }
41
+ setResult({ objectId, path, extract });
42
+ }
43
+ catch (err) {
44
+ setError(err);
45
+ }
46
+ finally {
47
+ setLoading(false);
48
+ }
49
+ };
50
+ downloadObject();
51
+ }, [objectId, path, extract, durationSeconds]);
52
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Downloading object..." }), result && (_jsx(SuccessMessage, { message: "Object downloaded successfully", details: `Object ID: ${result.objectId}\nPath: ${result.path}\nExtracted: ${result.extract ? "Yes" : "No"}` })), error && (_jsx(ErrorMessage, { message: "Failed to download object", error: error }))] }));
53
+ };
54
+ export async function downloadObject(options) {
55
+ const executor = createExecutor({ output: options.outputFormat });
56
+ await executor.executeAction(async () => {
57
+ const client = executor.getClient();
58
+ // Get the object metadata first
59
+ const object = await client.objects.retrieve(options.id);
60
+ // Get the download URL
61
+ const downloadUrlResponse = await client.objects.download(options.id, {
62
+ duration_seconds: options.durationSeconds || 3600,
63
+ });
64
+ // Download the file
65
+ const response = await fetch(downloadUrlResponse.download_url);
66
+ if (!response.ok) {
67
+ throw new Error(`Download failed: HTTP ${response.status}`);
68
+ }
69
+ // Handle extraction if requested
70
+ if (options.extract) {
71
+ // For now, just save to the specified path
72
+ // In a full implementation, you'd handle archive extraction here
73
+ const arrayBuffer = await response.arrayBuffer();
74
+ const buffer = Buffer.from(arrayBuffer);
75
+ await import("fs/promises").then((fs) => fs.writeFile(options.path, buffer));
76
+ }
77
+ else {
78
+ const arrayBuffer = await response.arrayBuffer();
79
+ const buffer = Buffer.from(arrayBuffer);
80
+ await import("fs/promises").then((fs) => fs.writeFile(options.path, buffer));
81
+ }
82
+ return {
83
+ objectId: options.id,
84
+ path: options.path,
85
+ extract: options.extract,
86
+ };
87
+ }, () => (_jsx(DownloadObjectUI, { objectId: options.id, path: options.path, extract: options.extract, durationSeconds: options.durationSeconds })));
88
+ }
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { getClient } from "../../utils/client.js";
4
+ import { Banner } from "../../components/Banner.js";
5
+ import { SpinnerComponent } from "../../components/Spinner.js";
6
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
7
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
8
+ import { createExecutor } from "../../utils/CommandExecutor.js";
9
+ const GetObjectUI = ({ objectId }) => {
10
+ const [loading, setLoading] = React.useState(true);
11
+ const [result, setResult] = React.useState(null);
12
+ const [error, setError] = React.useState(null);
13
+ React.useEffect(() => {
14
+ const getObject = async () => {
15
+ try {
16
+ const client = getClient();
17
+ const object = await client.objects.retrieve(objectId);
18
+ setResult(object);
19
+ }
20
+ catch (err) {
21
+ setError(err);
22
+ }
23
+ finally {
24
+ setLoading(false);
25
+ }
26
+ };
27
+ getObject();
28
+ }, [objectId]);
29
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Fetching object details..." }), result && (_jsx(SuccessMessage, { message: "Object details retrieved", details: `ID: ${result.id}\nName: ${result.name}\nType: ${result.contentType}\nState: ${result.state}\nSize: ${result.sizeBytes ? `${result.sizeBytes} bytes` : "Unknown"}` })), error && _jsx(ErrorMessage, { message: "Failed to get object", error: error })] }));
30
+ };
31
+ export async function getObject(options) {
32
+ const executor = createExecutor({ output: options.outputFormat });
33
+ await executor.executeAction(async () => {
34
+ const client = executor.getClient();
35
+ return client.objects.retrieve(options.id);
36
+ }, () => _jsx(GetObjectUI, { objectId: options.id }));
37
+ }