@runloop/rl-cli 0.1.2 → 0.2.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 (96) hide show
  1. package/README.md +54 -0
  2. package/dist/cli.js +73 -60
  3. package/dist/commands/auth.js +0 -1
  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 +215 -213
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/blueprint/preview.js +42 -38
  9. package/dist/commands/config.js +117 -0
  10. package/dist/commands/devbox/create.js +120 -40
  11. package/dist/commands/devbox/delete.js +17 -33
  12. package/dist/commands/devbox/download.js +29 -43
  13. package/dist/commands/devbox/exec.js +22 -39
  14. package/dist/commands/devbox/execAsync.js +20 -37
  15. package/dist/commands/devbox/get.js +13 -35
  16. package/dist/commands/devbox/getAsync.js +12 -34
  17. package/dist/commands/devbox/list.js +241 -402
  18. package/dist/commands/devbox/logs.js +20 -38
  19. package/dist/commands/devbox/read.js +29 -43
  20. package/dist/commands/devbox/resume.js +13 -35
  21. package/dist/commands/devbox/rsync.js +26 -78
  22. package/dist/commands/devbox/scp.js +25 -79
  23. package/dist/commands/devbox/sendStdin.js +41 -0
  24. package/dist/commands/devbox/shutdown.js +13 -35
  25. package/dist/commands/devbox/ssh.js +45 -78
  26. package/dist/commands/devbox/suspend.js +13 -35
  27. package/dist/commands/devbox/tunnel.js +36 -88
  28. package/dist/commands/devbox/upload.js +28 -36
  29. package/dist/commands/devbox/write.js +29 -44
  30. package/dist/commands/mcp-install.js +4 -3
  31. package/dist/commands/menu.js +24 -66
  32. package/dist/commands/object/delete.js +12 -34
  33. package/dist/commands/object/download.js +26 -74
  34. package/dist/commands/object/get.js +12 -34
  35. package/dist/commands/object/list.js +15 -93
  36. package/dist/commands/object/upload.js +35 -96
  37. package/dist/commands/snapshot/create.js +23 -39
  38. package/dist/commands/snapshot/delete.js +17 -33
  39. package/dist/commands/snapshot/get.js +16 -0
  40. package/dist/commands/snapshot/list.js +309 -80
  41. package/dist/commands/snapshot/status.js +12 -34
  42. package/dist/components/ActionsPopup.js +63 -39
  43. package/dist/components/Breadcrumb.js +10 -48
  44. package/dist/components/DevboxActionsMenu.js +182 -110
  45. package/dist/components/DevboxCreatePage.js +12 -7
  46. package/dist/components/DevboxDetailPage.js +76 -28
  47. package/dist/components/ErrorBoundary.js +29 -0
  48. package/dist/components/ErrorMessage.js +10 -2
  49. package/dist/components/Header.js +12 -4
  50. package/dist/components/InteractiveSpawn.js +94 -0
  51. package/dist/components/MainMenu.js +36 -32
  52. package/dist/components/MetadataDisplay.js +4 -4
  53. package/dist/components/OperationsMenu.js +1 -1
  54. package/dist/components/ResourceActionsMenu.js +4 -4
  55. package/dist/components/ResourceListView.js +46 -34
  56. package/dist/components/Spinner.js +7 -2
  57. package/dist/components/StatusBadge.js +1 -1
  58. package/dist/components/SuccessMessage.js +12 -2
  59. package/dist/components/Table.js +16 -6
  60. package/dist/hooks/useCursorPagination.js +125 -85
  61. package/dist/hooks/useExitOnCtrlC.js +14 -0
  62. package/dist/hooks/useViewportHeight.js +47 -0
  63. package/dist/mcp/server.js +65 -6
  64. package/dist/router/Router.js +68 -0
  65. package/dist/router/types.js +1 -0
  66. package/dist/screens/BlueprintListScreen.js +7 -0
  67. package/dist/screens/DevboxActionsScreen.js +25 -0
  68. package/dist/screens/DevboxCreateScreen.js +11 -0
  69. package/dist/screens/DevboxDetailScreen.js +60 -0
  70. package/dist/screens/DevboxListScreen.js +23 -0
  71. package/dist/screens/LogsSessionScreen.js +49 -0
  72. package/dist/screens/MenuScreen.js +23 -0
  73. package/dist/screens/SSHSessionScreen.js +55 -0
  74. package/dist/screens/SnapshotListScreen.js +7 -0
  75. package/dist/services/blueprintService.js +105 -0
  76. package/dist/services/devboxService.js +215 -0
  77. package/dist/services/snapshotService.js +81 -0
  78. package/dist/store/blueprintStore.js +89 -0
  79. package/dist/store/devboxStore.js +105 -0
  80. package/dist/store/index.js +7 -0
  81. package/dist/store/navigationStore.js +101 -0
  82. package/dist/store/snapshotStore.js +87 -0
  83. package/dist/utils/CommandExecutor.js +53 -24
  84. package/dist/utils/client.js +0 -2
  85. package/dist/utils/config.js +22 -111
  86. package/dist/utils/interactiveCommand.js +3 -2
  87. package/dist/utils/logFormatter.js +162 -0
  88. package/dist/utils/memoryMonitor.js +85 -0
  89. package/dist/utils/output.js +150 -59
  90. package/dist/utils/screen.js +23 -0
  91. package/dist/utils/ssh.js +3 -1
  92. package/dist/utils/sshSession.js +5 -29
  93. package/dist/utils/terminalDetection.js +97 -0
  94. package/dist/utils/terminalSync.js +39 -0
  95. package/dist/utils/theme.js +147 -13
  96. package/package.json +16 -13
@@ -1,82 +1,24 @@
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 { getSSHKey, waitForReady, generateSSHConfig, checkSSHTools, getProxyCommand, } from "../../utils/ssh.js";
8
+ export async function sshDevbox(devboxId, options = {}) {
9
+ try {
68
10
  // Check if SSH tools are available
69
11
  const sshToolsAvailable = await checkSSHTools();
70
12
  if (!sshToolsAvailable) {
71
- throw new Error("SSH tools (ssh, scp, rsync, openssl) are not available on this system");
13
+ outputError("SSH tools (ssh, scp, rsync, openssl) are not available on this system");
72
14
  }
73
- const client = executor.getClient();
15
+ const client = getClient();
74
16
  // Wait for devbox to be ready unless --no-wait is specified
75
17
  if (!options.noWait) {
76
- console.log(`Waiting for devbox ${devboxId} to be ready...`);
18
+ console.error(`Waiting for devbox ${devboxId} to be ready...`);
77
19
  const isReady = await waitForReady(devboxId, options.timeout || 180, options.pollInterval || 3);
78
20
  if (!isReady) {
79
- throw new Error(`Devbox ${devboxId} is not ready. Please try again later.`);
21
+ outputError(`Devbox ${devboxId} is not ready. Please try again later.`);
80
22
  }
81
23
  }
82
24
  // Get devbox details to determine user
@@ -85,20 +27,45 @@ export async function sshDevbox(devboxId, options) {
85
27
  // Get SSH key
86
28
  const sshInfo = await getSSHKey(devboxId);
87
29
  if (!sshInfo) {
88
- throw new Error("Failed to create SSH key");
30
+ outputError("Failed to create SSH key");
89
31
  }
90
32
  if (options.configOnly) {
91
- return {
92
- config: generateSSHConfig(devboxId, user, sshInfo.keyfilePath, sshInfo.url),
93
- };
33
+ const config = generateSSHConfig(devboxId, user, sshInfo.keyfilePath, sshInfo.url);
34
+ output({ config }, { format: options.output, defaultFormat: "text" });
35
+ return;
94
36
  }
95
- else {
96
- return {
37
+ // If output format is specified, just return the connection info
38
+ if (options.output && options.output !== "text") {
39
+ output({
97
40
  devboxId,
98
41
  user,
99
42
  keyfilePath: sshInfo.keyfilePath,
100
43
  url: sshInfo.url,
101
- };
44
+ }, { format: options.output, defaultFormat: "json" });
45
+ return;
102
46
  }
103
- }, () => _jsx(SSHDevboxUI, { devboxId: devboxId, options: options }));
47
+ // Actually start SSH session
48
+ const proxyCommand = getProxyCommand();
49
+ const sshArgs = [
50
+ "-i",
51
+ sshInfo.keyfilePath,
52
+ "-o",
53
+ `ProxyCommand=${proxyCommand}`,
54
+ "-o",
55
+ "StrictHostKeyChecking=no",
56
+ `${user}@${sshInfo.url}`,
57
+ ];
58
+ const sshProcess = spawn("/usr/bin/ssh", sshArgs, {
59
+ stdio: "inherit",
60
+ });
61
+ sshProcess.on("close", (code) => {
62
+ process.exit(code || 0);
63
+ });
64
+ sshProcess.on("error", (err) => {
65
+ outputError("SSH connection failed", err);
66
+ });
67
+ }
68
+ catch (error) {
69
+ outputError("Failed to setup SSH connection", error);
70
+ }
104
71
  }
@@ -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,43 @@
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";
9
7
  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
8
  export async function createTunnel(devboxId, options) {
75
- const executor = createExecutor({ output: options.outputFormat });
76
- await executor.executeAction(async () => {
9
+ try {
77
10
  // Check if SSH tools are available
78
11
  const sshToolsAvailable = await checkSSHTools();
79
12
  if (!sshToolsAvailable) {
80
- throw new Error("SSH tools (ssh, openssl) are not available on this system");
13
+ outputError("SSH tools (ssh, openssl) are not available on this system");
81
14
  }
82
15
  if (!options.ports.includes(":")) {
83
- throw new Error("Ports must be specified as 'local:remote'");
16
+ outputError("Ports must be specified as 'local:remote'");
84
17
  }
85
18
  const [localPort, remotePort] = options.ports.split(":");
86
- const client = executor.getClient();
19
+ const client = getClient();
87
20
  // Get devbox details to determine user
88
21
  const devbox = await client.devboxes.retrieve(devboxId);
89
22
  const user = devbox.launch_parameters?.user_parameters?.username || "user";
90
23
  // Get SSH key
91
24
  const sshInfo = await getSSHKey(devboxId);
92
25
  if (!sshInfo) {
93
- throw new Error("Failed to create SSH key");
26
+ outputError("Failed to create SSH key");
27
+ }
28
+ // If output format is specified, just return the tunnel info
29
+ if (options.output && options.output !== "text") {
30
+ output({
31
+ devboxId,
32
+ localPort,
33
+ remotePort,
34
+ user,
35
+ keyfilePath: sshInfo.keyfilePath,
36
+ }, { format: options.output, defaultFormat: "json" });
37
+ return;
94
38
  }
95
39
  const proxyCommand = getProxyCommand();
96
- const tunnelCommand = [
97
- "/usr/bin/ssh",
40
+ const tunnelArgs = [
98
41
  "-i",
99
42
  sshInfo.keyfilePath,
100
43
  "-o",
@@ -108,13 +51,18 @@ export async function createTunnel(devboxId, options) {
108
51
  ];
109
52
  console.log(`Starting tunnel: local port ${localPort} -> remote port ${remotePort}`);
110
53
  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 }));
54
+ const tunnelProcess = spawn("/usr/bin/ssh", tunnelArgs, {
55
+ stdio: "inherit",
56
+ });
57
+ tunnelProcess.on("close", (code) => {
58
+ console.log("\nTunnel closed.");
59
+ process.exit(code || 0);
60
+ });
61
+ tunnelProcess.on("error", (err) => {
62
+ outputError("Tunnel creation failed", err);
63
+ });
64
+ }
65
+ catch (error) {
66
+ outputError("Failed to create SSH tunnel", error);
67
+ }
120
68
  }
@@ -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
  }
@@ -26,7 +26,7 @@ function getRliPath() {
26
26
  const path = execSync(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
27
27
  return path;
28
28
  }
29
- catch (error) {
29
+ catch {
30
30
  // If rli not found in PATH, just use 'rli' and hope it works
31
31
  return "rli";
32
32
  }
@@ -48,7 +48,7 @@ export async function installMcpConfig() {
48
48
  config.mcpServers = {};
49
49
  }
50
50
  }
51
- catch (error) {
51
+ catch {
52
52
  console.error("āŒ Error: Claude config file exists but is not valid JSON");
53
53
  console.error("Please fix the file manually or delete it to create a new one");
54
54
  process.exit(1);
@@ -103,7 +103,8 @@ export async function installMcpConfig() {
103
103
  console.log('\nšŸ’” Tip: Make sure you\'ve run "rli auth" to configure your API key first!');
104
104
  }
105
105
  catch (error) {
106
- console.error("\nāŒ Error installing MCP configuration:", error.message);
106
+ const errorMessage = error instanceof Error ? error.message : String(error);
107
+ console.error("\nāŒ Error installing MCP configuration:", errorMessage);
107
108
  console.error("\nšŸ’” You can manually add this configuration to your Claude Desktop config:");
108
109
  console.error(`\nFile location: ${getClaudeConfigPath()}`);
109
110
  console.error("\nConfiguration to add:");
@@ -1,70 +1,28 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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
- // Import the UI components directly
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
- const { exit } = useApp();
13
- const [currentScreen, setCurrentScreen] = React.useState(initialScreen);
14
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
15
- const handleMenuSelect = (key) => {
16
- setCurrentScreen(key);
17
- };
18
- const handleBack = () => {
19
- setCurrentScreen("menu");
20
- };
21
- const handleExit = () => {
22
- exit();
23
- };
24
- // Wrap everything in a full-height container
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
- };
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "ink";
3
+ import { enterAlternateScreenBuffer, exitAlternateScreenBuffer, } from "../utils/screen.js";
4
+ import { Router } from "../router/Router.js";
5
+ import { NavigationProvider } from "../store/navigationStore.js";
6
+ function AppInner() {
7
+ // NavigationProvider already handles initialScreen and initialParams
8
+ // No need for useEffect here - provider sets state on mount
9
+ return _jsx(Router, {});
10
+ }
11
+ function App({ initialScreen = "menu", focusDevboxId, }) {
12
+ return (_jsx(NavigationProvider, { initialScreen: initialScreen, initialParams: focusDevboxId ? { focusDevboxId } : {}, children: _jsx(AppInner, {}) }));
13
+ }
27
14
  export async function runMainMenu(initialScreen = "menu", focusDevboxId) {
28
- // Enter alternate screen buffer once at the start
29
- process.stdout.write("\x1b[?1049h");
30
- let sshSessionConfig = null;
31
- let shouldContinue = true;
32
- let currentInitialScreen = initialScreen;
33
- let currentFocusDevboxId = focusDevboxId;
34
- while (shouldContinue) {
35
- sshSessionConfig = null;
36
- try {
37
- const { waitUntilExit } = render(_jsx(App, { onSSHRequest: (config) => {
38
- sshSessionConfig = config;
39
- }, initialScreen: currentInitialScreen, focusDevboxId: currentFocusDevboxId }));
40
- await waitUntilExit();
41
- shouldContinue = false;
42
- }
43
- catch (error) {
44
- console.error("Error in menu:", error);
45
- shouldContinue = false;
46
- }
47
- // If SSH was requested, handle it now after Ink has exited
48
- if (sshSessionConfig) {
49
- // Exit alternate screen buffer for SSH
50
- process.stdout.write("\x1b[?1049l");
51
- const result = await runSSHSession(sshSessionConfig);
52
- if (result.shouldRestart) {
53
- console.clear();
54
- console.log(`\nSSH session ended. Returning to menu...\n`);
55
- await new Promise((resolve) => setTimeout(resolve, 500));
56
- // Re-enter alternate screen buffer and return to devboxes list
57
- process.stdout.write("\x1b[?1049h");
58
- currentInitialScreen = "devboxes";
59
- currentFocusDevboxId = result.returnToDevboxId;
60
- shouldContinue = true;
61
- }
62
- else {
63
- shouldContinue = false;
64
- }
65
- }
15
+ enterAlternateScreenBuffer();
16
+ try {
17
+ const { waitUntilExit } = render(_jsx(App, { initialScreen: initialScreen, focusDevboxId: focusDevboxId }, `app-${initialScreen}-${focusDevboxId}`), {
18
+ patchConsole: false,
19
+ exitOnCtrlC: false,
20
+ });
21
+ await waitUntilExit();
22
+ }
23
+ catch (error) {
24
+ console.error("Error in menu:", error);
66
25
  }
67
- // Exit alternate screen buffer once at the end
68
- process.stdout.write("\x1b[?1049l");
26
+ exitAlternateScreenBuffer();
69
27
  process.exit(0);
70
28
  }