@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
@@ -1,31 +1,31 @@
1
1
  /**
2
2
  * Utility for handling different output formats across CLI commands
3
3
  */
4
- import YAML from 'yaml';
4
+ import YAML from "yaml";
5
5
  /**
6
6
  * Check if the command should use non-interactive output
7
7
  */
8
8
  export function shouldUseNonInteractiveOutput(options) {
9
- return !!options.output;
9
+ return !!options.output && options.output !== "interactive";
10
10
  }
11
11
  /**
12
12
  * Output data in the specified format
13
13
  */
14
- export function outputData(data, format = 'json') {
15
- if (format === 'json') {
14
+ export function outputData(data, format = "json") {
15
+ if (format === "json") {
16
16
  console.log(JSON.stringify(data, null, 2));
17
17
  return;
18
18
  }
19
- if (format === 'yaml') {
19
+ if (format === "yaml") {
20
20
  console.log(YAML.stringify(data));
21
21
  return;
22
22
  }
23
- if (format === 'text') {
23
+ if (format === "text") {
24
24
  // Simple text output
25
25
  if (Array.isArray(data)) {
26
26
  // For lists of complex objects, just output IDs
27
27
  data.forEach((item) => {
28
- if (typeof item === 'object' && item !== null && 'id' in item) {
28
+ if (typeof item === "object" && item !== null && "id" in item) {
29
29
  console.log(item.id);
30
30
  }
31
31
  else {
@@ -45,7 +45,7 @@ export function outputData(data, format = 'json') {
45
45
  * Format a single item as text output
46
46
  */
47
47
  function formatTextOutput(item) {
48
- if (typeof item === 'string') {
48
+ if (typeof item === "string") {
49
49
  return item;
50
50
  }
51
51
  // For objects, create a simple key: value format
@@ -55,7 +55,7 @@ function formatTextOutput(item) {
55
55
  lines.push(`${key}: ${value}`);
56
56
  }
57
57
  }
58
- return lines.join('\n');
58
+ return lines.join("\n");
59
59
  }
60
60
  /**
61
61
  * Output a single result (for create, delete, etc)
@@ -83,10 +83,10 @@ export function outputList(items, options) {
83
83
  */
84
84
  export function outputError(error, options) {
85
85
  if (shouldUseNonInteractiveOutput(options)) {
86
- if (options.output === 'json') {
86
+ if (options.output === "json") {
87
87
  console.error(JSON.stringify({ error: error.message }, null, 2));
88
88
  }
89
- else if (options.output === 'yaml') {
89
+ else if (options.output === "yaml") {
90
90
  console.error(YAML.stringify({ error: error.message }));
91
91
  }
92
92
  else {
@@ -101,14 +101,14 @@ export function outputError(error, options) {
101
101
  * Validate output format option
102
102
  */
103
103
  export function validateOutputFormat(format) {
104
- if (!format || format === 'text') {
105
- return 'text';
104
+ if (!format || format === "text") {
105
+ return "text";
106
106
  }
107
- if (format === 'json') {
108
- return 'json';
107
+ if (format === "json") {
108
+ return "json";
109
109
  }
110
- if (format === 'yaml') {
111
- return 'yaml';
110
+ if (format === "yaml") {
111
+ return "yaml";
112
112
  }
113
113
  console.error(`Unknown output format: ${format}. Valid options: text, json, yaml`);
114
114
  process.exit(1);
@@ -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
+ }
@@ -1,25 +1,29 @@
1
- import { spawnSync } from 'child_process';
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('reset', [], { stdio: 'inherit' });
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('ssh', [
10
- '-t', // Force pseudo-terminal allocation for proper input handling
11
- '-i', config.keyPath,
12
- '-o', `ProxyCommand=${config.proxyCommand}`,
13
- '-o', 'StrictHostKeyChecking=no',
14
- '-o', 'UserKnownHostsFile=/dev/null',
15
- `${config.sshUser}@${config.url}`
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: 'inherit',
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 'dev':
13
- return 'https://platform.runloop.pro';
14
- case 'prod':
12
+ case "dev":
13
+ return "https://platform.runloop.pro";
14
+ case "prod":
15
15
  default:
16
- return 'https://platform.runloop.ai';
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",
3
+ "version": "0.1.1",
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
  }