@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.
- package/README.md +64 -29
- package/dist/cli.js +401 -92
- package/dist/commands/auth.js +12 -11
- package/dist/commands/blueprint/create.js +108 -0
- package/dist/commands/blueprint/get.js +37 -0
- package/dist/commands/blueprint/list.js +293 -225
- package/dist/commands/blueprint/logs.js +40 -0
- package/dist/commands/blueprint/preview.js +45 -0
- package/dist/commands/devbox/create.js +10 -9
- package/dist/commands/devbox/delete.js +8 -8
- package/dist/commands/devbox/download.js +49 -0
- package/dist/commands/devbox/exec.js +23 -13
- package/dist/commands/devbox/execAsync.js +43 -0
- package/dist/commands/devbox/get.js +37 -0
- package/dist/commands/devbox/getAsync.js +37 -0
- package/dist/commands/devbox/list.js +328 -190
- package/dist/commands/devbox/logs.js +40 -0
- package/dist/commands/devbox/read.js +49 -0
- package/dist/commands/devbox/resume.js +37 -0
- package/dist/commands/devbox/rsync.js +118 -0
- package/dist/commands/devbox/scp.js +122 -0
- package/dist/commands/devbox/shutdown.js +37 -0
- package/dist/commands/devbox/ssh.js +104 -0
- package/dist/commands/devbox/suspend.js +37 -0
- package/dist/commands/devbox/tunnel.js +120 -0
- package/dist/commands/devbox/upload.js +10 -10
- package/dist/commands/devbox/write.js +51 -0
- package/dist/commands/mcp-http.js +37 -0
- package/dist/commands/mcp-install.js +120 -0
- package/dist/commands/mcp.js +30 -0
- package/dist/commands/menu.js +20 -20
- package/dist/commands/object/delete.js +37 -0
- package/dist/commands/object/download.js +88 -0
- package/dist/commands/object/get.js +37 -0
- package/dist/commands/object/list.js +112 -0
- package/dist/commands/object/upload.js +130 -0
- package/dist/commands/snapshot/create.js +12 -11
- package/dist/commands/snapshot/delete.js +8 -8
- package/dist/commands/snapshot/list.js +56 -97
- package/dist/commands/snapshot/status.js +37 -0
- package/dist/components/ActionsPopup.js +16 -13
- package/dist/components/Banner.js +4 -4
- package/dist/components/Breadcrumb.js +55 -5
- package/dist/components/DetailView.js +7 -4
- package/dist/components/DevboxActionsMenu.js +315 -178
- package/dist/components/DevboxCard.js +15 -14
- package/dist/components/DevboxCreatePage.js +147 -113
- package/dist/components/DevboxDetailPage.js +180 -102
- package/dist/components/ErrorMessage.js +5 -4
- package/dist/components/Header.js +4 -3
- package/dist/components/MainMenu.js +34 -33
- package/dist/components/MetadataDisplay.js +17 -9
- package/dist/components/OperationsMenu.js +6 -5
- package/dist/components/ResourceActionsMenu.js +117 -0
- package/dist/components/ResourceListView.js +213 -0
- package/dist/components/Spinner.js +5 -4
- package/dist/components/StatusBadge.js +81 -31
- package/dist/components/SuccessMessage.js +4 -3
- package/dist/components/Table.example.js +53 -23
- package/dist/components/Table.js +19 -11
- package/dist/hooks/useCursorPagination.js +125 -0
- package/dist/mcp/server-http.js +416 -0
- package/dist/mcp/server.js +397 -0
- package/dist/utils/CommandExecutor.js +16 -12
- package/dist/utils/client.js +7 -7
- package/dist/utils/config.js +130 -4
- package/dist/utils/interactiveCommand.js +2 -2
- package/dist/utils/output.js +17 -17
- package/dist/utils/ssh.js +160 -0
- package/dist/utils/sshSession.js +16 -12
- package/dist/utils/theme.js +22 -0
- package/dist/utils/url.js +4 -4
- 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
|
|
3
|
-
import { render } from
|
|
4
|
-
import { createReadStream } from
|
|
5
|
-
import { getClient } from
|
|
6
|
-
import { Header } from
|
|
7
|
-
import { SpinnerComponent } from
|
|
8
|
-
import { SuccessMessage } from
|
|
9
|
-
import { ErrorMessage } from
|
|
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(
|
|
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}` :
|
|
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
|
+
}
|
package/dist/commands/menu.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from
|
|
3
|
-
import { render, useApp } from
|
|
4
|
-
import { MainMenu } from
|
|
5
|
-
import { runSSHSession } from
|
|
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
|
|
8
|
-
import { ListBlueprintsUI } from
|
|
9
|
-
import { ListSnapshotsUI } from
|
|
10
|
-
import { Box } from
|
|
11
|
-
const App = ({ onSSHRequest, initialScreen =
|
|
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(
|
|
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 ===
|
|
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 =
|
|
27
|
+
export async function runMainMenu(initialScreen = "menu", focusDevboxId) {
|
|
28
28
|
// Enter alternate screen buffer once at the start
|
|
29
|
-
process.stdout.write(
|
|
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(
|
|
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(
|
|
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(
|
|
58
|
-
currentInitialScreen =
|
|
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(
|
|
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
|
+
}
|