@runloop/rl-cli 0.1.2 → 0.3.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 (105) hide show
  1. package/README.md +54 -10
  2. package/dist/cli.js +79 -72
  3. package/dist/commands/auth.js +2 -2
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +278 -230
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/config.js +118 -0
  9. package/dist/commands/devbox/create.js +120 -40
  10. package/dist/commands/devbox/delete.js +17 -33
  11. package/dist/commands/devbox/download.js +29 -43
  12. package/dist/commands/devbox/exec.js +22 -39
  13. package/dist/commands/devbox/execAsync.js +20 -37
  14. package/dist/commands/devbox/get.js +13 -35
  15. package/dist/commands/devbox/getAsync.js +12 -34
  16. package/dist/commands/devbox/list.js +241 -402
  17. package/dist/commands/devbox/logs.js +20 -38
  18. package/dist/commands/devbox/read.js +29 -43
  19. package/dist/commands/devbox/resume.js +13 -35
  20. package/dist/commands/devbox/rsync.js +26 -78
  21. package/dist/commands/devbox/scp.js +25 -79
  22. package/dist/commands/devbox/sendStdin.js +41 -0
  23. package/dist/commands/devbox/shutdown.js +13 -35
  24. package/dist/commands/devbox/ssh.js +46 -78
  25. package/dist/commands/devbox/suspend.js +13 -35
  26. package/dist/commands/devbox/tunnel.js +37 -88
  27. package/dist/commands/devbox/upload.js +28 -36
  28. package/dist/commands/devbox/write.js +29 -44
  29. package/dist/commands/mcp-http.js +6 -5
  30. package/dist/commands/mcp-install.js +12 -10
  31. package/dist/commands/mcp.js +5 -4
  32. package/dist/commands/menu.js +26 -67
  33. package/dist/commands/object/delete.js +12 -34
  34. package/dist/commands/object/download.js +26 -74
  35. package/dist/commands/object/get.js +12 -34
  36. package/dist/commands/object/list.js +15 -93
  37. package/dist/commands/object/upload.js +35 -96
  38. package/dist/commands/snapshot/create.js +23 -39
  39. package/dist/commands/snapshot/delete.js +17 -33
  40. package/dist/commands/snapshot/get.js +16 -0
  41. package/dist/commands/snapshot/list.js +309 -80
  42. package/dist/commands/snapshot/status.js +12 -34
  43. package/dist/components/ActionsPopup.js +64 -39
  44. package/dist/components/Banner.js +7 -1
  45. package/dist/components/Breadcrumb.js +11 -48
  46. package/dist/components/DevboxActionsMenu.js +117 -207
  47. package/dist/components/DevboxCreatePage.js +12 -7
  48. package/dist/components/DevboxDetailPage.js +76 -28
  49. package/dist/components/ErrorBoundary.js +29 -0
  50. package/dist/components/ErrorMessage.js +10 -2
  51. package/dist/components/Header.js +12 -4
  52. package/dist/components/InteractiveSpawn.js +104 -0
  53. package/dist/components/LogsViewer.js +169 -0
  54. package/dist/components/MainMenu.js +37 -33
  55. package/dist/components/MetadataDisplay.js +4 -4
  56. package/dist/components/OperationsMenu.js +1 -1
  57. package/dist/components/ResourceActionsMenu.js +4 -4
  58. package/dist/components/ResourceListView.js +46 -34
  59. package/dist/components/Spinner.js +7 -2
  60. package/dist/components/StatusBadge.js +1 -1
  61. package/dist/components/SuccessMessage.js +12 -2
  62. package/dist/components/Table.js +16 -6
  63. package/dist/components/UpdateNotification.js +56 -0
  64. package/dist/hooks/useCursorPagination.js +125 -85
  65. package/dist/hooks/useExitOnCtrlC.js +15 -0
  66. package/dist/hooks/useViewportHeight.js +47 -0
  67. package/dist/mcp/server-http.js +2 -1
  68. package/dist/mcp/server.js +71 -7
  69. package/dist/router/Router.js +70 -0
  70. package/dist/router/types.js +1 -0
  71. package/dist/screens/BlueprintListScreen.js +7 -0
  72. package/dist/screens/BlueprintLogsScreen.js +74 -0
  73. package/dist/screens/DevboxActionsScreen.js +25 -0
  74. package/dist/screens/DevboxCreateScreen.js +11 -0
  75. package/dist/screens/DevboxDetailScreen.js +60 -0
  76. package/dist/screens/DevboxListScreen.js +23 -0
  77. package/dist/screens/LogsSessionScreen.js +49 -0
  78. package/dist/screens/MenuScreen.js +23 -0
  79. package/dist/screens/SSHSessionScreen.js +55 -0
  80. package/dist/screens/SnapshotListScreen.js +7 -0
  81. package/dist/services/blueprintService.js +101 -0
  82. package/dist/services/devboxService.js +215 -0
  83. package/dist/services/snapshotService.js +81 -0
  84. package/dist/store/blueprintStore.js +89 -0
  85. package/dist/store/devboxStore.js +105 -0
  86. package/dist/store/index.js +7 -0
  87. package/dist/store/navigationStore.js +101 -0
  88. package/dist/store/snapshotStore.js +87 -0
  89. package/dist/utils/client.js +4 -2
  90. package/dist/utils/config.js +22 -111
  91. package/dist/utils/interactiveCommand.js +3 -2
  92. package/dist/utils/logFormatter.js +208 -0
  93. package/dist/utils/memoryMonitor.js +85 -0
  94. package/dist/utils/output.js +153 -61
  95. package/dist/utils/process.js +106 -0
  96. package/dist/utils/processUtils.js +135 -0
  97. package/dist/utils/screen.js +61 -0
  98. package/dist/utils/ssh.js +6 -3
  99. package/dist/utils/sshSession.js +5 -29
  100. package/dist/utils/terminalDetection.js +185 -0
  101. package/dist/utils/terminalSync.js +39 -0
  102. package/dist/utils/theme.js +162 -13
  103. package/dist/utils/versionCheck.js +53 -0
  104. package/dist/version.js +12 -0
  105. package/package.json +19 -17
@@ -1,82 +1,25 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from "react";
3
- import { Box, Text } from "ink";
1
+ /**
2
+ * SSH into devbox command
3
+ */
4
+ import { spawn } from "child_process";
4
5
  import { getClient } from "../../utils/client.js";
5
- import { Banner } from "../../components/Banner.js";
6
- import { SpinnerComponent } from "../../components/Spinner.js";
7
- import { SuccessMessage } from "../../components/SuccessMessage.js";
8
- import { ErrorMessage } from "../../components/ErrorMessage.js";
9
- import { createExecutor } from "../../utils/CommandExecutor.js";
10
- import { colors } from "../../utils/theme.js";
11
- import { getSSHKey, waitForReady, generateSSHConfig, checkSSHTools, } from "../../utils/ssh.js";
12
- const SSHDevboxUI = ({ devboxId, options }) => {
13
- const [loading, setLoading] = React.useState(true);
14
- const [result, setResult] = React.useState(null);
15
- const [error, setError] = React.useState(null);
16
- React.useEffect(() => {
17
- const connectSSH = async () => {
18
- try {
19
- // Check if SSH tools are available
20
- const sshToolsAvailable = await checkSSHTools();
21
- if (!sshToolsAvailable) {
22
- throw new Error("SSH tools (ssh, scp, rsync, openssl) are not available on this system");
23
- }
24
- const client = getClient();
25
- // Wait for devbox to be ready unless --no-wait is specified
26
- if (!options.noWait) {
27
- console.log(`Waiting for devbox ${devboxId} to be ready...`);
28
- const isReady = await waitForReady(devboxId, options.timeout || 180, options.pollInterval || 3);
29
- if (!isReady) {
30
- throw new Error(`Devbox ${devboxId} is not ready. Please try again later.`);
31
- }
32
- }
33
- // Get devbox details to determine user
34
- const devbox = await client.devboxes.retrieve(devboxId);
35
- const user = devbox.launch_parameters?.user_parameters?.username || "user";
36
- // Get SSH key
37
- const sshInfo = await getSSHKey(devboxId);
38
- if (!sshInfo) {
39
- throw new Error("Failed to create SSH key");
40
- }
41
- if (options.configOnly) {
42
- const config = generateSSHConfig(devboxId, user, sshInfo.keyfilePath, sshInfo.url);
43
- setResult({ config });
44
- }
45
- else {
46
- setResult({
47
- devboxId,
48
- user,
49
- keyfilePath: sshInfo.keyfilePath,
50
- url: sshInfo.url,
51
- });
52
- }
53
- }
54
- catch (err) {
55
- setError(err);
56
- }
57
- finally {
58
- setLoading(false);
59
- }
60
- };
61
- connectSSH();
62
- }, [devboxId, options]);
63
- return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Setting up SSH connection..." }), result && result.config && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.primary, children: "SSH Config:" }), _jsx(Text, { children: result.config })] })), result && !result.config && (_jsx(SuccessMessage, { message: "SSH connection ready", details: `Devbox: ${result.devboxId}\nUser: ${result.user}\nKey: ${result.keyfilePath}` })), error && (_jsx(ErrorMessage, { message: "Failed to setup SSH connection", error: error }))] }));
64
- };
65
- export async function sshDevbox(devboxId, options) {
66
- const executor = createExecutor(options);
67
- await executor.executeAction(async () => {
6
+ import { output, outputError } from "../../utils/output.js";
7
+ import { processUtils } from "../../utils/processUtils.js";
8
+ import { getSSHKey, waitForReady, generateSSHConfig, checkSSHTools, getProxyCommand, } from "../../utils/ssh.js";
9
+ export async function sshDevbox(devboxId, options = {}) {
10
+ try {
68
11
  // Check if SSH tools are available
69
12
  const sshToolsAvailable = await checkSSHTools();
70
13
  if (!sshToolsAvailable) {
71
- throw new Error("SSH tools (ssh, scp, rsync, openssl) are not available on this system");
14
+ outputError("SSH tools (ssh, scp, rsync, openssl) are not available on this system");
72
15
  }
73
- const client = executor.getClient();
16
+ const client = getClient();
74
17
  // Wait for devbox to be ready unless --no-wait is specified
75
18
  if (!options.noWait) {
76
- console.log(`Waiting for devbox ${devboxId} to be ready...`);
19
+ console.error(`Waiting for devbox ${devboxId} to be ready...`);
77
20
  const isReady = await waitForReady(devboxId, options.timeout || 180, options.pollInterval || 3);
78
21
  if (!isReady) {
79
- throw new Error(`Devbox ${devboxId} is not ready. Please try again later.`);
22
+ outputError(`Devbox ${devboxId} is not ready. Please try again later.`);
80
23
  }
81
24
  }
82
25
  // Get devbox details to determine user
@@ -85,20 +28,45 @@ export async function sshDevbox(devboxId, options) {
85
28
  // Get SSH key
86
29
  const sshInfo = await getSSHKey(devboxId);
87
30
  if (!sshInfo) {
88
- throw new Error("Failed to create SSH key");
31
+ outputError("Failed to create SSH key");
89
32
  }
90
33
  if (options.configOnly) {
91
- return {
92
- config: generateSSHConfig(devboxId, user, sshInfo.keyfilePath, sshInfo.url),
93
- };
34
+ const config = generateSSHConfig(devboxId, user, sshInfo.keyfilePath, sshInfo.url);
35
+ output({ config }, { format: options.output, defaultFormat: "text" });
36
+ return;
94
37
  }
95
- else {
96
- return {
38
+ // If output format is specified, just return the connection info
39
+ if (options.output && options.output !== "text") {
40
+ output({
97
41
  devboxId,
98
42
  user,
99
43
  keyfilePath: sshInfo.keyfilePath,
100
44
  url: sshInfo.url,
101
- };
45
+ }, { format: options.output, defaultFormat: "json" });
46
+ return;
102
47
  }
103
- }, () => _jsx(SSHDevboxUI, { devboxId: devboxId, options: options }));
48
+ // Actually start SSH session
49
+ const proxyCommand = getProxyCommand();
50
+ const sshArgs = [
51
+ "-i",
52
+ sshInfo.keyfilePath,
53
+ "-o",
54
+ `ProxyCommand=${proxyCommand}`,
55
+ "-o",
56
+ "StrictHostKeyChecking=no",
57
+ `${user}@${sshInfo.url}`,
58
+ ];
59
+ const sshProcess = spawn("/usr/bin/ssh", sshArgs, {
60
+ stdio: "inherit",
61
+ });
62
+ sshProcess.on("close", (code) => {
63
+ processUtils.exit(code || 0);
64
+ });
65
+ sshProcess.on("error", (err) => {
66
+ outputError("SSH connection failed", err);
67
+ });
68
+ }
69
+ catch (error) {
70
+ outputError("Failed to setup SSH connection", error);
71
+ }
104
72
  }
@@ -1,37 +1,15 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
1
+ /**
2
+ * Suspend devbox command
3
+ */
3
4
  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 SuspendDevboxUI = ({ devboxId }) => {
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 suspendDevbox = async () => {
15
- try {
16
- const client = getClient();
17
- const devbox = await client.devboxes.suspend(devboxId);
18
- setResult(devbox);
19
- }
20
- catch (err) {
21
- setError(err);
22
- }
23
- finally {
24
- setLoading(false);
25
- }
26
- };
27
- suspendDevbox();
28
- }, [devboxId]);
29
- return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Suspending devbox..." }), result && (_jsx(SuccessMessage, { message: "Devbox suspended", details: `ID: ${result.id}\nStatus: ${result.status}` })), error && (_jsx(ErrorMessage, { message: "Failed to suspend devbox", error: error }))] }));
30
- };
31
- export async function suspendDevbox(devboxId, options) {
32
- const executor = createExecutor(options);
33
- await executor.executeAction(async () => {
34
- const client = executor.getClient();
35
- return client.devboxes.suspend(devboxId);
36
- }, () => _jsx(SuspendDevboxUI, { devboxId: devboxId }));
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function suspendDevbox(devboxId, options = {}) {
7
+ try {
8
+ const client = getClient();
9
+ const devbox = await client.devboxes.suspend(devboxId);
10
+ output(devbox, { format: options.output, defaultFormat: "json" });
11
+ }
12
+ catch (error) {
13
+ outputError("Failed to suspend devbox", error);
14
+ }
37
15
  }
@@ -1,100 +1,44 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
1
+ /**
2
+ * Create SSH tunnel to devbox command
3
+ */
4
+ import { spawn } from "child_process";
3
5
  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";
6
+ import { output, outputError } from "../../utils/output.js";
7
+ import { processUtils } from "../../utils/processUtils.js";
9
8
  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
9
  export async function createTunnel(devboxId, options) {
75
- const executor = createExecutor({ output: options.outputFormat });
76
- await executor.executeAction(async () => {
10
+ try {
77
11
  // Check if SSH tools are available
78
12
  const sshToolsAvailable = await checkSSHTools();
79
13
  if (!sshToolsAvailable) {
80
- throw new Error("SSH tools (ssh, openssl) are not available on this system");
14
+ outputError("SSH tools (ssh, openssl) are not available on this system");
81
15
  }
82
16
  if (!options.ports.includes(":")) {
83
- throw new Error("Ports must be specified as 'local:remote'");
17
+ outputError("Ports must be specified as 'local:remote'");
84
18
  }
85
19
  const [localPort, remotePort] = options.ports.split(":");
86
- const client = executor.getClient();
20
+ const client = getClient();
87
21
  // Get devbox details to determine user
88
22
  const devbox = await client.devboxes.retrieve(devboxId);
89
23
  const user = devbox.launch_parameters?.user_parameters?.username || "user";
90
24
  // Get SSH key
91
25
  const sshInfo = await getSSHKey(devboxId);
92
26
  if (!sshInfo) {
93
- throw new Error("Failed to create SSH key");
27
+ outputError("Failed to create SSH key");
28
+ }
29
+ // If output format is specified, just return the tunnel info
30
+ if (options.output && options.output !== "text") {
31
+ output({
32
+ devboxId,
33
+ localPort,
34
+ remotePort,
35
+ user,
36
+ keyfilePath: sshInfo.keyfilePath,
37
+ }, { format: options.output, defaultFormat: "json" });
38
+ return;
94
39
  }
95
40
  const proxyCommand = getProxyCommand();
96
- const tunnelCommand = [
97
- "/usr/bin/ssh",
41
+ const tunnelArgs = [
98
42
  "-i",
99
43
  sshInfo.keyfilePath,
100
44
  "-o",
@@ -108,13 +52,18 @@ export async function createTunnel(devboxId, options) {
108
52
  ];
109
53
  console.log(`Starting tunnel: local port ${localPort} -> remote port ${remotePort}`);
110
54
  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 }));
55
+ const tunnelProcess = spawn("/usr/bin/ssh", tunnelArgs, {
56
+ stdio: "inherit",
57
+ });
58
+ tunnelProcess.on("close", (code) => {
59
+ console.log("\nTunnel closed.");
60
+ processUtils.exit(code || 0);
61
+ });
62
+ tunnelProcess.on("error", (err) => {
63
+ outputError("Tunnel creation failed", err);
64
+ });
65
+ }
66
+ catch (error) {
67
+ outputError("Failed to create SSH tunnel", error);
68
+ }
120
69
  }
@@ -1,40 +1,32 @@
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";
1
+ /**
2
+ * Upload file to devbox command
3
+ */
4
4
  import { createReadStream } from "fs";
5
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
- const UploadFileUI = ({ id, file, targetPath }) => {
11
- const [loading, setLoading] = React.useState(true);
12
- const [success, setSuccess] = React.useState(false);
13
- const [error, setError] = React.useState(null);
14
- React.useEffect(() => {
15
- const upload = async () => {
16
- try {
17
- const client = getClient();
18
- const fileStream = createReadStream(file);
19
- const filename = file.split("/").pop() || "uploaded-file";
20
- await client.devboxes.uploadFile(id, {
21
- path: targetPath || filename,
22
- file: fileStream,
23
- });
24
- setSuccess(true);
25
- }
26
- catch (err) {
27
- setError(err);
28
- }
29
- finally {
30
- setLoading(false);
31
- }
6
+ import { output, outputError } from "../../utils/output.js";
7
+ export async function uploadFile(id, file, options = {}) {
8
+ try {
9
+ const client = getClient();
10
+ const fileStream = createReadStream(file);
11
+ const filename = file.split("/").pop() || "uploaded-file";
12
+ await client.devboxes.uploadFile(id, {
13
+ path: options.path || filename,
14
+ file: fileStream,
15
+ });
16
+ const result = {
17
+ file,
18
+ target: options.path || filename,
19
+ devboxId: id,
32
20
  };
33
- upload();
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 })] }));
36
- };
37
- export async function uploadFile(id, file, options) {
38
- const { waitUntilExit } = render(_jsx(UploadFileUI, { id: id, file: file, targetPath: options.path }));
39
- await waitUntilExit();
21
+ // Default: just output the target path for easy scripting
22
+ if (!options.output || options.output === "text") {
23
+ console.log(options.path || filename);
24
+ }
25
+ else {
26
+ output(result, { format: options.output, defaultFormat: "json" });
27
+ }
28
+ }
29
+ catch (error) {
30
+ outputError("Failed to upload file", error);
31
+ }
40
32
  }
@@ -1,51 +1,36 @@
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";
1
+ /**
2
+ * Write file to devbox command (using API)
3
+ */
9
4
  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();
5
+ import { getClient } from "../../utils/client.js";
6
+ import { output, outputError } from "../../utils/output.js";
7
+ export async function writeFile(devboxId, options = {}) {
8
+ if (!options.input) {
9
+ outputError("--input is required");
10
+ }
11
+ if (!options.remote) {
12
+ outputError("--remote is required");
13
+ }
14
+ try {
15
+ const client = getClient();
40
16
  const contents = await readFile(options.input, "utf-8");
41
17
  await client.devboxes.writeFileContents(devboxId, {
42
18
  file_path: options.remote,
43
19
  contents,
44
20
  });
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 })));
21
+ // Default: just output the remote path for easy scripting
22
+ if (!options.output || options.output === "text") {
23
+ console.log(options.remote);
24
+ }
25
+ else {
26
+ output({
27
+ local: options.input,
28
+ remote: options.remote,
29
+ size: contents.length,
30
+ }, { format: options.output, defaultFormat: "json" });
31
+ }
32
+ }
33
+ catch (error) {
34
+ outputError("Failed to write file", error);
35
+ }
51
36
  }
@@ -2,12 +2,13 @@
2
2
  import { spawn } from "child_process";
3
3
  import { fileURLToPath } from "url";
4
4
  import { dirname, join } from "path";
5
+ import { processUtils } from "../utils/processUtils.js";
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
7
8
  export async function startMcpHttpServer(port) {
8
9
  // Get the path to the compiled MCP HTTP server
9
10
  const serverPath = join(__dirname, "../mcp/server-http.js");
10
- const env = { ...process.env };
11
+ const env = { ...processUtils.env };
11
12
  if (port) {
12
13
  env.PORT = port.toString();
13
14
  }
@@ -20,18 +21,18 @@ export async function startMcpHttpServer(port) {
20
21
  });
21
22
  serverProcess.on("error", (error) => {
22
23
  console.error("Failed to start MCP HTTP server:", error);
23
- process.exit(1);
24
+ processUtils.exit(1);
24
25
  });
25
26
  serverProcess.on("exit", (code) => {
26
27
  if (code !== 0) {
27
28
  console.error(`MCP HTTP server exited with code ${code}`);
28
- process.exit(code || 1);
29
+ processUtils.exit(code || 1);
29
30
  }
30
31
  });
31
32
  // Handle Ctrl+C
32
- process.on("SIGINT", () => {
33
+ processUtils.on("SIGINT", () => {
33
34
  console.log("\nShutting down MCP HTTP server...");
34
35
  serverProcess.kill("SIGINT");
35
- process.exit(0);
36
+ processUtils.exit(0);
36
37
  });
37
38
  }
@@ -3,13 +3,14 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
3
  import { homedir, platform } from "os";
4
4
  import { join } from "path";
5
5
  import { execSync } from "child_process";
6
+ import { processUtils } from "../utils/processUtils.js";
6
7
  function getClaudeConfigPath() {
7
8
  const plat = platform();
8
9
  if (plat === "darwin") {
9
10
  return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
10
11
  }
11
12
  else if (plat === "win32") {
12
- const appData = process.env.APPDATA;
13
+ const appData = processUtils.env.APPDATA;
13
14
  if (!appData) {
14
15
  throw new Error("APPDATA environment variable not found");
15
16
  }
@@ -26,7 +27,7 @@ function getRliPath() {
26
27
  const path = execSync(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
27
28
  return path;
28
29
  }
29
- catch (error) {
30
+ catch {
30
31
  // If rli not found in PATH, just use 'rli' and hope it works
31
32
  return "rli";
32
33
  }
@@ -48,10 +49,10 @@ export async function installMcpConfig() {
48
49
  config.mcpServers = {};
49
50
  }
50
51
  }
51
- catch (error) {
52
+ catch {
52
53
  console.error("❌ Error: Claude config file exists but is not valid JSON");
53
54
  console.error("Please fix the file manually or delete it to create a new one");
54
- process.exit(1);
55
+ processUtils.exit(1);
55
56
  }
56
57
  }
57
58
  else {
@@ -71,20 +72,20 @@ export async function installMcpConfig() {
71
72
  // Ask if they want to overwrite
72
73
  console.log("\n❓ Do you want to overwrite it? (y/N): ");
73
74
  // For non-interactive mode, just exit
74
- if (process.stdin.isTTY) {
75
+ if (processUtils.stdin.isTTY) {
75
76
  const response = await new Promise((resolve) => {
76
- process.stdin.once("data", (data) => {
77
+ processUtils.stdin.on("data", (data) => {
77
78
  resolve(data.toString().trim().toLowerCase());
78
79
  });
79
80
  });
80
81
  if (response !== "y" && response !== "yes") {
81
82
  console.log("\n✓ Keeping existing configuration");
82
- process.exit(0);
83
+ processUtils.exit(0);
83
84
  }
84
85
  }
85
86
  else {
86
87
  console.log("\n✓ Keeping existing configuration (non-interactive mode)");
87
- process.exit(0);
88
+ processUtils.exit(0);
88
89
  }
89
90
  }
90
91
  // Add runloop MCP server config
@@ -103,7 +104,8 @@ export async function installMcpConfig() {
103
104
  console.log('\n💡 Tip: Make sure you\'ve run "rli auth" to configure your API key first!');
104
105
  }
105
106
  catch (error) {
106
- console.error("\n❌ Error installing MCP configuration:", error.message);
107
+ const errorMessage = error instanceof Error ? error.message : String(error);
108
+ console.error("\n❌ Error installing MCP configuration:", errorMessage);
107
109
  console.error("\n💡 You can manually add this configuration to your Claude Desktop config:");
108
110
  console.error(`\nFile location: ${getClaudeConfigPath()}`);
109
111
  console.error("\nConfiguration to add:");
@@ -115,6 +117,6 @@ export async function installMcpConfig() {
115
117
  },
116
118
  },
117
119
  }, null, 2));
118
- process.exit(1);
120
+ processUtils.exit(1);
119
121
  }
120
122
  }
@@ -2,6 +2,7 @@
2
2
  import { spawn } from "child_process";
3
3
  import { fileURLToPath } from "url";
4
4
  import { dirname, join } from "path";
5
+ import { processUtils } from "../utils/processUtils.js";
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
7
8
  export async function startMcpServer() {
@@ -14,17 +15,17 @@ export async function startMcpServer() {
14
15
  });
15
16
  serverProcess.on("error", (error) => {
16
17
  console.error("Failed to start MCP server:", error);
17
- process.exit(1);
18
+ processUtils.exit(1);
18
19
  });
19
20
  serverProcess.on("exit", (code) => {
20
21
  if (code !== 0) {
21
22
  console.error(`MCP server exited with code ${code}`);
22
- process.exit(code || 1);
23
+ processUtils.exit(code || 1);
23
24
  }
24
25
  });
25
26
  // Handle Ctrl+C
26
- process.on("SIGINT", () => {
27
+ processUtils.on("SIGINT", () => {
27
28
  serverProcess.kill("SIGINT");
28
- process.exit(0);
29
+ processUtils.exit(0);
29
30
  });
30
31
  }