@runloop/rl-cli 0.0.2 → 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.
Files changed (73) hide show
  1. package/README.md +64 -29
  2. package/dist/cli.js +420 -76
  3. package/dist/commands/auth.js +12 -10
  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 +303 -224
  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 +390 -205
  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 +70 -0
  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 +59 -91
  40. package/dist/commands/snapshot/status.js +37 -0
  41. package/dist/components/ActionsPopup.js +16 -13
  42. package/dist/components/Banner.js +5 -8
  43. package/dist/components/Breadcrumb.js +6 -6
  44. package/dist/components/DetailView.js +7 -4
  45. package/dist/components/DevboxActionsMenu.js +347 -189
  46. package/dist/components/DevboxCard.js +15 -14
  47. package/dist/components/DevboxCreatePage.js +147 -113
  48. package/dist/components/DevboxDetailPage.js +182 -103
  49. package/dist/components/ErrorMessage.js +5 -4
  50. package/dist/components/Header.js +4 -3
  51. package/dist/components/MainMenu.js +72 -0
  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 +22 -6
  65. package/dist/utils/client.js +20 -3
  66. package/dist/utils/config.js +40 -4
  67. package/dist/utils/interactiveCommand.js +14 -0
  68. package/dist/utils/output.js +17 -17
  69. package/dist/utils/ssh.js +160 -0
  70. package/dist/utils/sshSession.js +29 -0
  71. package/dist/utils/theme.js +22 -0
  72. package/dist/utils/url.js +39 -0
  73. 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
+ }
@@ -0,0 +1,29 @@
1
+ import { spawnSync } from "child_process";
2
+ export async function runSSHSession(config) {
3
+ // Reset terminal to fix input visibility issues
4
+ // This ensures the terminal is in a proper state after exiting Ink
5
+ spawnSync("reset", [], { stdio: "inherit" });
6
+ console.clear();
7
+ console.log(`\nConnecting to devbox ${config.devboxName}...\n`);
8
+ // Spawn SSH in foreground with proper terminal settings
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}`,
20
+ ], {
21
+ stdio: "inherit",
22
+ shell: false,
23
+ });
24
+ return {
25
+ exitCode: result.status || 0,
26
+ shouldRestart: true,
27
+ returnToDevboxId: config.devboxId,
28
+ };
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
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Utility functions for generating URLs
3
+ */
4
+ /**
5
+ * Get the base URL for the Runloop platform based on environment
6
+ * - dev: https://platform.runloop.pro
7
+ * - prod or unset: https://platform.runloop.ai (default)
8
+ */
9
+ export function getBaseUrl() {
10
+ const env = process.env.RUNLOOP_ENV?.toLowerCase();
11
+ switch (env) {
12
+ case "dev":
13
+ return "https://platform.runloop.pro";
14
+ case "prod":
15
+ default:
16
+ return "https://platform.runloop.ai";
17
+ }
18
+ }
19
+ /**
20
+ * Generate a devbox URL for the given devbox ID
21
+ */
22
+ export function getDevboxUrl(devboxId) {
23
+ const baseUrl = getBaseUrl();
24
+ return `${baseUrl}/devboxes/${devboxId}`;
25
+ }
26
+ /**
27
+ * Generate a blueprint URL for the given blueprint ID
28
+ */
29
+ export function getBlueprintUrl(blueprintId) {
30
+ const baseUrl = getBaseUrl();
31
+ return `${baseUrl}/blueprints/${blueprintId}`;
32
+ }
33
+ /**
34
+ * Generate a settings URL
35
+ */
36
+ export function getSettingsUrl() {
37
+ const baseUrl = getBaseUrl();
38
+ return `${baseUrl}/settings`;
39
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@runloop/rl-cli",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Beautiful CLI for Runloop devbox management",
5
5
  "type": "module",
6
6
  "bin": {
7
- "rln": "./dist/cli.js"
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
- "@runloop/api-client": "^0.55.0",
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
  }