Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,29 @@ ssh yourserver -t "devbox shell --name shared-api"

---

## IDE Integration

Use your local VS Code, Cursor, or Windsurf to edit code inside the sandbox — full IntelliSense, extensions, and debugging, all running in the isolated VM.

```bash
devbox code # Open VS Code into the sandbox
devbox code --editor cursor # Use Cursor instead
devbox code --editor windsurf # Use Windsurf
devbox code myapp # Open a specific sandbox
devbox code --path /workspace/src # Open a specific directory
```

Devbox automatically:
1. Configures `~/.ssh/config` for the sandbox VM
2. Refreshes the overlay layer (clears stale file handles)
3. Launches the editor with Remote SSH pointed at `/workspace`

Works with any editor that supports [Remote SSH](https://code.visualstudio.com/docs/remote/ssh) — VS Code, Cursor, Windsurf, and others.

> **NixOS compatibility:** Devbox enables `nix-ld` in the VM so VS Code Server and other dynamically linked binaries run without issues.

---

## Quick Start

### Prerequisites
Expand Down Expand Up @@ -309,6 +332,7 @@ All layer operations are also available in the **DevBox Management Panel** insid
| `devbox destroy` | Remove a sandbox |
| `devbox list` | List all sandboxes |
| `devbox status` | Show detailed sandbox status |
| `devbox code` | Open VS Code / Cursor into sandbox via Remote SSH |
| `devbox use <name>` | Switch sandbox to current directory |
| `devbox upgrade --tools <set>` | Add tools to a running sandbox |
| `devbox packages` | Open TUI package manager |
Expand Down
5 changes: 5 additions & 0 deletions nix/devbox-module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ in {
virtualisation.docker.enable = lib.mkDefault (sets.container or false);
services.tailscale.enable = lib.mkDefault (sets.network or false);

# ── Dynamic linker compat ─────────────────────────
# Required for VS Code Server, Cursor, and other dynamically linked
# binaries that expect a standard FHS layout (ld-linux, libc, etc.).
programs.nix-ld.enable = true;

# ── Shell ──────────────────────────────────────────
programs.zsh.enable = true;
security.sudo.wheelNeedsPassword = lib.mkDefault false;
Expand Down
32 changes: 30 additions & 2 deletions src/cli/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use clap::Args;
use crate::runtime::cmd::run_cmd;
use crate::runtime::SandboxStatus;
use crate::sandbox::SandboxManager;
use crate::sandbox::overlay;

#[derive(Args, Debug)]
pub struct CodeArgs {
Expand Down Expand Up @@ -36,6 +37,14 @@ pub async fn run(args: CodeArgs, manager: &SandboxManager) -> Result<()> {
SandboxStatus::Unknown(s) => bail!("Sandbox '{name}' is in unknown state: {s}"),
}

// Refresh overlay before opening editor to avoid stale file handles
if state.mount_mode != "writable" {
println!("Refreshing overlay layer...");
if let Err(e) = overlay::refresh(runtime.as_ref(), &name).await {
eprintln!("Warning: overlay refresh failed: {e}");
}
}

let vm_name = format!("devbox-{name}");
let ssh_host = format!("devbox-{name}");

Expand Down Expand Up @@ -67,7 +76,9 @@ async fn open_via_lima(
);
}

let ssh_config = result.stdout.trim().to_string();
// Lima's output has its own Host line (e.g. "Host lima-devbox-test2").
// Replace it with our ssh_host so VS Code can find the right config entry.
let ssh_config = rewrite_ssh_host(ssh_host, result.stdout.trim());
write_ssh_config(ssh_host, &ssh_config)?;

launch_editor(editor, ssh_host, path)
Expand Down Expand Up @@ -139,6 +150,21 @@ fn extract_incus_ip(json_output: &str) -> Result<String> {
bail!("Could not find IP address for Incus VM. Is it running?")
}

/// Replace the `Host` line in an SSH config block with our desired host alias.
fn rewrite_ssh_host(desired_host: &str, config: &str) -> String {
config
.lines()
.map(|line| {
if line.trim_start().starts_with("Host ") {
format!("Host {desired_host}")
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
}

/// Write or update an SSH config block in ~/.ssh/config for the devbox host.
fn write_ssh_config(host: &str, config_block: &str) -> Result<()> {
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?;
Expand Down Expand Up @@ -199,9 +225,11 @@ fn launch_editor(editor: &str, ssh_host: &str, path: &str) -> Result<()> {

println!("Opening {editor} → {ssh_host}:{path}");

let remote_arg = format!("--remote=ssh-remote+{ssh_host}{path}");
let remote_arg = format!("ssh-remote+{ssh_host}");
let status = std::process::Command::new(editor)
.arg("--remote")
.arg(&remote_arg)
.arg(path)
.status()
.map_err(|e| anyhow::anyhow!("Failed to launch {editor}: {e}"))?;

Expand Down
Loading