@runloop/rl-cli 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -29
- package/dist/cli.js +389 -91
- 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 +5 -4
- 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 +40 -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,160 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { writeFile, mkdir, chmod } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { getClient } from "./client.js";
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
export function constructSSHConfig(options) {
|
|
9
|
+
return `Host ${options.hostname}
|
|
10
|
+
User ${options.username}
|
|
11
|
+
Hostname ${options.hostname}
|
|
12
|
+
Port ${options.port}
|
|
13
|
+
IdentityFile ${options.keyPath}
|
|
14
|
+
StrictHostKeyChecking no
|
|
15
|
+
UserKnownHostsFile /dev/null
|
|
16
|
+
ProxyCommand openssl s_client -connect ${options.hostname}:${options.port} -quiet
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get or create SSH key for a devbox
|
|
21
|
+
*/
|
|
22
|
+
export async function getSSHKey(devboxId) {
|
|
23
|
+
try {
|
|
24
|
+
const client = getClient();
|
|
25
|
+
const result = await client.devboxes.createSSHKey(devboxId);
|
|
26
|
+
if (!result || !result.ssh_private_key || !result.url) {
|
|
27
|
+
throw new Error("Failed to create SSH key");
|
|
28
|
+
}
|
|
29
|
+
// Create SSH keys directory
|
|
30
|
+
const sshDir = join(homedir(), ".runloop", "ssh_keys");
|
|
31
|
+
await mkdir(sshDir, { recursive: true });
|
|
32
|
+
// Save private key to file
|
|
33
|
+
const keyfilePath = join(sshDir, `${devboxId}.pem`);
|
|
34
|
+
await writeFile(keyfilePath, result.ssh_private_key, { mode: 0o600 });
|
|
35
|
+
await chmod(keyfilePath, 0o600);
|
|
36
|
+
return {
|
|
37
|
+
keyfilePath,
|
|
38
|
+
privateKey: result.ssh_private_key,
|
|
39
|
+
url: result.url,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error("Failed to create SSH key:", error);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wait for a devbox to be ready
|
|
49
|
+
*/
|
|
50
|
+
export async function waitForReady(devboxId, timeoutSeconds = 180, pollIntervalSeconds = 3) {
|
|
51
|
+
const startTime = Date.now();
|
|
52
|
+
const client = getClient();
|
|
53
|
+
while (true) {
|
|
54
|
+
try {
|
|
55
|
+
const devbox = await client.devboxes.retrieve(devboxId);
|
|
56
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
57
|
+
const remaining = timeoutSeconds - elapsed;
|
|
58
|
+
if (devbox.status === "running") {
|
|
59
|
+
console.log(`Devbox ${devboxId} is ready!`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
else if (devbox.status === "failure") {
|
|
63
|
+
console.log(`Devbox ${devboxId} failed to start (status: ${devbox.status})`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
else if (["shutdown", "suspended"].includes(devbox.status)) {
|
|
67
|
+
console.log(`Devbox ${devboxId} is not running (status: ${devbox.status})`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log(`Devbox ${devboxId} is still ${devbox.status}... (elapsed: ${elapsed.toFixed(0)}s, remaining: ${remaining.toFixed(0)}s)`);
|
|
72
|
+
if (elapsed >= timeoutSeconds) {
|
|
73
|
+
console.log(`Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalSeconds * 1000));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
81
|
+
if (elapsed >= timeoutSeconds) {
|
|
82
|
+
console.log(`Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds (error: ${error})`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
console.log(`Error checking devbox status: ${error}, retrying in ${pollIntervalSeconds} seconds...`);
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalSeconds * 1000));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get SSH URL based on environment
|
|
92
|
+
*/
|
|
93
|
+
export function getSSHUrl() {
|
|
94
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
95
|
+
return env === "dev" ? "ssh.runloop.pro:443" : "ssh.runloop.ai:443";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get proxy command for SSH over HTTPS
|
|
99
|
+
*/
|
|
100
|
+
export function getProxyCommand() {
|
|
101
|
+
const sshUrl = getSSHUrl();
|
|
102
|
+
return `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshUrl} 2>/dev/null`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Execute SSH command
|
|
106
|
+
*/
|
|
107
|
+
export async function executeSSH(devboxId, user, keyfilePath, url, additionalArgs = []) {
|
|
108
|
+
const proxyCommand = getProxyCommand();
|
|
109
|
+
const command = [
|
|
110
|
+
"/usr/bin/ssh",
|
|
111
|
+
"-i",
|
|
112
|
+
keyfilePath,
|
|
113
|
+
"-o",
|
|
114
|
+
`ProxyCommand=${proxyCommand}`,
|
|
115
|
+
"-o",
|
|
116
|
+
"StrictHostKeyChecking=no",
|
|
117
|
+
...additionalArgs,
|
|
118
|
+
`${user}@${url}`,
|
|
119
|
+
];
|
|
120
|
+
try {
|
|
121
|
+
const { stdout, stderr } = await execAsync(command.join(" "));
|
|
122
|
+
if (stdout)
|
|
123
|
+
console.log(stdout);
|
|
124
|
+
if (stderr)
|
|
125
|
+
console.error(stderr);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error("SSH command failed:", error);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generate SSH config for a devbox
|
|
134
|
+
*/
|
|
135
|
+
export function generateSSHConfig(devboxId, user, keyfilePath, url) {
|
|
136
|
+
const proxyCommand = getProxyCommand();
|
|
137
|
+
return `
|
|
138
|
+
Host ${devboxId}
|
|
139
|
+
Hostname ${url}
|
|
140
|
+
User ${user}
|
|
141
|
+
IdentityFile ${keyfilePath}
|
|
142
|
+
StrictHostKeyChecking no
|
|
143
|
+
ProxyCommand ${proxyCommand}
|
|
144
|
+
`.trim();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if SSH tools are available
|
|
148
|
+
*/
|
|
149
|
+
export async function checkSSHTools() {
|
|
150
|
+
try {
|
|
151
|
+
await execAsync("which ssh");
|
|
152
|
+
await execAsync("which scp");
|
|
153
|
+
await execAsync("which rsync");
|
|
154
|
+
await execAsync("which openssl");
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
package/dist/utils/sshSession.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import { spawnSync } from
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
2
|
export async function runSSHSession(config) {
|
|
3
3
|
// Reset terminal to fix input visibility issues
|
|
4
4
|
// This ensures the terminal is in a proper state after exiting Ink
|
|
5
|
-
spawnSync(
|
|
5
|
+
spawnSync("reset", [], { stdio: "inherit" });
|
|
6
6
|
console.clear();
|
|
7
7
|
console.log(`\nConnecting to devbox ${config.devboxName}...\n`);
|
|
8
8
|
// Spawn SSH in foreground with proper terminal settings
|
|
9
|
-
const result = spawnSync(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const result = spawnSync("ssh", [
|
|
10
|
+
"-t", // Force pseudo-terminal allocation for proper input handling
|
|
11
|
+
"-i",
|
|
12
|
+
config.keyPath,
|
|
13
|
+
"-o",
|
|
14
|
+
`ProxyCommand=${config.proxyCommand}`,
|
|
15
|
+
"-o",
|
|
16
|
+
"StrictHostKeyChecking=no",
|
|
17
|
+
"-o",
|
|
18
|
+
"UserKnownHostsFile=/dev/null",
|
|
19
|
+
`${config.sshUser}@${config.url}`,
|
|
16
20
|
], {
|
|
17
|
-
stdio:
|
|
18
|
-
shell: false
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
shell: false,
|
|
19
23
|
});
|
|
20
24
|
return {
|
|
21
25
|
exitCode: result.status || 0,
|
|
22
26
|
shouldRestart: true,
|
|
23
|
-
returnToDevboxId: config.devboxId
|
|
27
|
+
returnToDevboxId: config.devboxId,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color theme constants for the CLI application
|
|
3
|
+
* Centralized color definitions for easy theme customization
|
|
4
|
+
*/
|
|
5
|
+
export const colors = {
|
|
6
|
+
// Primary brand colors
|
|
7
|
+
primary: "cyan",
|
|
8
|
+
secondary: "magenta",
|
|
9
|
+
// Status colors
|
|
10
|
+
success: "green",
|
|
11
|
+
warning: "yellow",
|
|
12
|
+
error: "red",
|
|
13
|
+
info: "blue",
|
|
14
|
+
// UI colors
|
|
15
|
+
text: "white",
|
|
16
|
+
textDim: "gray",
|
|
17
|
+
border: "gray",
|
|
18
|
+
// Accent colors for menu items and highlights
|
|
19
|
+
accent1: "cyan",
|
|
20
|
+
accent2: "magenta",
|
|
21
|
+
accent3: "green",
|
|
22
|
+
};
|
package/dist/utils/url.js
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
export function getBaseUrl() {
|
|
10
10
|
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
11
11
|
switch (env) {
|
|
12
|
-
case
|
|
13
|
-
return
|
|
14
|
-
case
|
|
12
|
+
case "dev":
|
|
13
|
+
return "https://platform.runloop.pro";
|
|
14
|
+
case "prod":
|
|
15
15
|
default:
|
|
16
|
-
return
|
|
16
|
+
return "https://platform.runloop.ai";
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
/**
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runloop/rl-cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Beautiful CLI for Runloop devbox management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"rli": "./dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
@@ -14,7 +14,17 @@
|
|
|
14
14
|
"version:patch": "npm version patch",
|
|
15
15
|
"version:minor": "npm version minor",
|
|
16
16
|
"version:major": "npm version major",
|
|
17
|
-
"release": "npm run build && npm publish"
|
|
17
|
+
"release": "npm run build && npm publish",
|
|
18
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
19
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
20
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
21
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"test:unit": "jest tests/__tests__/unit",
|
|
24
|
+
"test:integration": "jest tests/__tests__/integration",
|
|
25
|
+
"test:watch": "jest --watch",
|
|
26
|
+
"test:coverage": "jest --coverage",
|
|
27
|
+
"test:e2e": "RUN_E2E=1 jest tests/__tests__/integration"
|
|
18
28
|
},
|
|
19
29
|
"keywords": [
|
|
20
30
|
"runloop",
|
|
@@ -46,11 +56,15 @@
|
|
|
46
56
|
},
|
|
47
57
|
"dependencies": {
|
|
48
58
|
"@inkjs/ui": "^2.0.0",
|
|
49
|
-
"@
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
60
|
+
"@runloop/api-client": "^0.58.0",
|
|
50
61
|
"@runloop/rl-cli": "^0.0.1",
|
|
62
|
+
"@types/express": "^5.0.3",
|
|
51
63
|
"chalk": "^5.3.0",
|
|
52
64
|
"commander": "^12.1.0",
|
|
53
65
|
"conf": "^13.0.1",
|
|
66
|
+
"dotenv": "^16.4.5",
|
|
67
|
+
"express": "^5.1.0",
|
|
54
68
|
"figures": "^6.1.0",
|
|
55
69
|
"gradient-string": "^2.0.2",
|
|
56
70
|
"ink": "^5.0.1",
|
|
@@ -63,8 +77,19 @@
|
|
|
63
77
|
"yaml": "^2.8.1"
|
|
64
78
|
},
|
|
65
79
|
"devDependencies": {
|
|
80
|
+
"@types/jest": "^29.5.0",
|
|
66
81
|
"@types/node": "^22.7.9",
|
|
67
82
|
"@types/react": "^18.3.11",
|
|
83
|
+
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
|
84
|
+
"@typescript-eslint/parser": "^8.46.0",
|
|
85
|
+
"eslint": "^9.37.0",
|
|
86
|
+
"eslint-plugin-react": "^7.37.5",
|
|
87
|
+
"eslint-plugin-react-hooks": "^6.1.1",
|
|
88
|
+
"globals": "^16.4.0",
|
|
89
|
+
"jest": "^29.7.0",
|
|
90
|
+
"prettier": "^3.6.2",
|
|
91
|
+
"ts-jest": "^29.1.0",
|
|
92
|
+
"ts-node": "^10.9.0",
|
|
68
93
|
"typescript": "^5.6.3"
|
|
69
94
|
}
|
|
70
95
|
}
|