Skip to content

Commit 7cc315b

Browse files
authored
feat(devbox): add streaming exec viewer with kill support and terminal resize stability (#77)
- Add ExecViewer component with real-time stdout/stderr streaming via SSE - Added secrets for devbox create - Use awaitCompleted long-polling for efficient completion detection - Support killing running executions mid-flight - Persist execution state across terminal resizes via route params - Add StreamingLogsViewer for real-time devbox logs - Improve tunnel UI with "open in browser" functionality
1 parent b04fc98 commit 7cc315b

19 files changed

Lines changed: 2055 additions & 49 deletions

.dockerignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
node_modules
2+
dist
3+
.git
4+
.github
5+
.husky
6+
*.log
7+
*.md
8+
!README.md
9+
.cursor
10+
.env*
11+
coverage
12+
.DS_Store

Dockerfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
FROM ubuntu:22.04
2+
3+
# Avoid prompts during package installation
4+
ENV DEBIAN_FRONTEND=noninteractive
5+
6+
# Install Node.js and build dependencies
7+
RUN apt-get update && apt-get install -y \
8+
curl \
9+
ca-certificates \
10+
git \
11+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
12+
&& apt-get install -y nodejs \
13+
&& apt-get clean \
14+
&& rm -rf /var/lib/apt/lists/*
15+
16+
# Set working directory
17+
WORKDIR /app
18+
19+
# Copy package files first for better caching
20+
COPY package.json package-lock.json ./
21+
22+
# Install dependencies
23+
RUN npm ci
24+
25+
# Copy source code
26+
COPY . .
27+
28+
WORKDIR /workspace
29+
30+
CMD ["/bin/bash"]

src/commands/devbox/create.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface CreateOptions {
1515
entrypoint?: string;
1616
launchCommands?: string[];
1717
envVars?: string[];
18+
secrets?: string[];
1819
codeMounts?: string[];
1920
idleTime?: string;
2021
idleAction?: string;
@@ -42,6 +43,23 @@ function parseEnvVars(envVars: string[]): Record<string, string> {
4243
return result;
4344
}
4445

46+
// Parse secrets from ENV_VAR=SECRET_NAME format
47+
function parseSecrets(secrets: string[]): Record<string, string> {
48+
const result: Record<string, string> = {};
49+
for (const secret of secrets) {
50+
const eqIndex = secret.indexOf("=");
51+
if (eqIndex === -1) {
52+
throw new Error(
53+
`Invalid secret format: ${secret}. Expected ENV_VAR=SECRET_NAME`,
54+
);
55+
}
56+
const envVarName = secret.substring(0, eqIndex);
57+
const secretName = secret.substring(eqIndex + 1);
58+
result[envVarName] = secretName;
59+
}
60+
return result;
61+
}
62+
4563
// Parse code mounts from JSON format
4664
function parseCodeMounts(codeMounts: string[]): unknown[] {
4765
return codeMounts.map((mount) => {
@@ -150,6 +168,11 @@ export async function createDevbox(options: CreateOptions = {}) {
150168
createRequest.code_mounts = parseCodeMounts(options.codeMounts);
151169
}
152170

171+
// Handle secrets
172+
if (options.secrets && options.secrets.length > 0) {
173+
createRequest.secrets = parseSecrets(options.secrets);
174+
}
175+
153176
if (Object.keys(launchParameters).length > 0) {
154177
createRequest.launch_parameters = launchParameters;
155178
}

src/commands/devbox/tunnel.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getSSHKey, getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
1111
interface TunnelOptions {
1212
ports: string;
1313
output?: string;
14+
open?: boolean;
1415
}
1516

1617
export async function createTunnel(devboxId: string, options: TunnelOptions) {
@@ -68,15 +69,41 @@ export async function createTunnel(devboxId: string, options: TunnelOptions) {
6869
`${user}@${sshInfo!.url}`,
6970
];
7071

72+
const tunnelUrl = `http://localhost:${localPort}`;
7173
console.log(
7274
`Starting tunnel: local port ${localPort} -> remote port ${remotePort}`,
7375
);
76+
console.log(`Tunnel URL: ${tunnelUrl}`);
7477
console.log("Press Ctrl+C to stop the tunnel.");
7578

7679
const tunnelProcess = spawn("/usr/bin/ssh", tunnelArgs, {
7780
stdio: "inherit",
7881
});
7982

83+
// Open browser if --open flag is set
84+
if (options.open) {
85+
// Small delay to let the tunnel establish
86+
setTimeout(async () => {
87+
const { exec } = await import("child_process");
88+
const platform = process.platform;
89+
90+
let openCommand: string;
91+
if (platform === "darwin") {
92+
openCommand = `open "${tunnelUrl}"`;
93+
} else if (platform === "win32") {
94+
openCommand = `start "${tunnelUrl}"`;
95+
} else {
96+
openCommand = `xdg-open "${tunnelUrl}"`;
97+
}
98+
99+
exec(openCommand, (error) => {
100+
if (error) {
101+
console.log(`\nCould not open browser: ${error.message}`);
102+
}
103+
});
104+
}, 1000);
105+
}
106+
80107
tunnelProcess.on("close", (code) => {
81108
console.log("\nTunnel closed.");
82109
processUtils.exit(code || 0);

0 commit comments

Comments
 (0)