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
89 changes: 87 additions & 2 deletions src/cli/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async fn open_via_lima(
launch_editor(editor, ssh_host, path)
}

/// Incus: get VM IP address, configure SSH, then launch editor.
/// Incus: get VM IP address, configure SSH key auth, then launch editor.
async fn open_via_incus(
ssh_host: &str,
vm_name: &str,
Expand All @@ -104,15 +104,100 @@ async fn open_via_incus(

let ip = extract_incus_ip(&result.stdout)?;

// Detect actual username in the VM
let uid_result = run_cmd(
"incus",
&["exec", vm_name, "--", "bash", "-lc",
"awk -F: '$3 >= 1000 && $3 < 65534 { print $1; exit }' /etc/passwd"],
).await?;
let username = uid_result.stdout.trim();
let username = if username.is_empty() { "dev" } else { username };

// Ensure SSH key-based auth is set up (inject host pubkey into VM)
ensure_ssh_key_auth(vm_name, username).await?;

// Build SSH config block for this VM
let home = dirs::home_dir().unwrap_or_default();
let key_path = home.join(".ssh").join("id_ed25519");
let key_fallback = home.join(".ssh").join("id_rsa");
let identity = if key_path.exists() {
key_path.to_string_lossy().to_string()
} else if key_fallback.exists() {
key_fallback.to_string_lossy().to_string()
} else {
// Will be created by ensure_ssh_key_auth
key_path.to_string_lossy().to_string()
};

let ssh_config = format!(
"Host {ssh_host}\n HostName {ip}\n User devbox\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null"
"Host {ssh_host}\n HostName {ip}\n User {username}\n IdentityFile {identity}\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null"
);
write_ssh_config(ssh_host, &ssh_config)?;

launch_editor(editor, ssh_host, path)
}

/// Ensure SSH key-based auth is configured between host and Incus VM.
/// Generates a host key if needed, then injects the public key into the VM.
async fn ensure_ssh_key_auth(vm_name: &str, username: &str) -> Result<()> {
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot determine home dir"))?;
let ssh_dir = home.join(".ssh");

// Find or generate host SSH key
let key_path = ssh_dir.join("id_ed25519");
let pub_path = ssh_dir.join("id_ed25519.pub");

if !pub_path.exists() {
let rsa_pub = ssh_dir.join("id_rsa.pub");
if !rsa_pub.exists() {
// Generate a new key
println!("Generating SSH key for devbox...");
std::fs::create_dir_all(&ssh_dir)?;
let status = std::process::Command::new("ssh-keygen")
.args(["-t", "ed25519", "-f"])
.arg(&key_path)
.args(["-N", "", "-q"])
.status()?;
if !status.success() {
bail!("Failed to generate SSH key");
}
}
}

// Read the public key
let pub_key_path = if pub_path.exists() { &pub_path } else { &ssh_dir.join("id_rsa.pub") };
let pubkey = std::fs::read_to_string(pub_key_path)
.map_err(|e| anyhow::anyhow!("Cannot read SSH public key: {e}"))?;
let pubkey = pubkey.trim();

// Ensure sshd is enabled and the user's authorized_keys has our pubkey
let setup_cmd = format!(
"mkdir -p /home/{username}/.ssh && \
chmod 700 /home/{username}/.ssh && \
touch /home/{username}/.ssh/authorized_keys && \
chmod 600 /home/{username}/.ssh/authorized_keys && \
grep -qF '{pubkey}' /home/{username}/.ssh/authorized_keys 2>/dev/null || \
echo '{pubkey}' >> /home/{username}/.ssh/authorized_keys && \
chown -R $(id -u {username}):users /home/{username}/.ssh"
);
let result = run_cmd(
"incus",
&["exec", vm_name, "--", "bash", "-lc", &setup_cmd],
).await?;

if result.exit_code != 0 {
eprintln!("Warning: SSH key setup may have failed: {}", result.stderr.trim());
}

// Ensure sshd is running
let _ = run_cmd(
"incus",
&["exec", vm_name, "--", "bash", "-lc", "systemctl enable --now sshd 2>/dev/null || systemctl enable --now ssh 2>/dev/null; true"],
).await;

Ok(())
}

/// Extract the first IPv4 address from `incus list --format json` output.
fn extract_incus_ip(json_output: &str) -> Result<String> {
let arr: Vec<serde_json::Value> = serde_json::from_str(json_output)
Expand Down
6 changes: 3 additions & 3 deletions src/nix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ pub async fn add_package(runtime: &dyn Runtime, sandbox_name: &str, package: &st
let result = runtime
.exec_cmd(
sandbox_name,
&["sudo", "nix", "profile", "install", package],
&["bash", "-lc", &format!("nix profile install {package}")],
false,
)
.await?;
Expand All @@ -84,7 +84,7 @@ pub async fn add_package(runtime: &dyn Runtime, sandbox_name: &str, package: &st
let result = runtime
.exec_cmd(
sandbox_name,
&["sudo", "nix", "profile", "install", &flake_ref],
&["bash", "-lc", &format!("nix profile install {flake_ref}")],
false,
)
.await?;
Expand All @@ -107,7 +107,7 @@ pub async fn remove_package(
let result = runtime
.exec_cmd(
sandbox_name,
&["sudo", "nix", "profile", "remove", package],
&["bash", "-lc", &format!("nix profile remove {package}")],
false,
)
.await?;
Expand Down
8 changes: 4 additions & 4 deletions src/nix/rebuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub async fn nixos_rebuild(runtime: &dyn Runtime, sandbox_name: &str) -> Result<
println!("Running nixos-rebuild switch...");

let result = runtime
.exec_cmd(sandbox_name, &["sudo", "nixos-rebuild", "switch"], false)
.exec_cmd(sandbox_name, &["bash", "-lc", "nixos-rebuild switch"], false)
.await?;

if result.exit_code != 0 {
Expand All @@ -18,7 +18,7 @@ pub async fn nixos_rebuild(runtime: &dyn Runtime, sandbox_name: &str) -> Result<
let rollback = runtime
.exec_cmd(
sandbox_name,
&["sudo", "nixos-rebuild", "switch", "--rollback"],
&["bash", "-lc", "nixos-rebuild switch --rollback"],
false,
)
.await;
Expand Down Expand Up @@ -54,7 +54,7 @@ pub async fn write_state_toml(
.exec_cmd(
sandbox_name,
&[
"sudo", "bash", "-c",
"bash", "-lc",
&format!(
"mkdir -p /etc/devbox && cat > /etc/devbox/devbox-state.toml << 'DEVBOX_EOF'\n{toml_content}\nDEVBOX_EOF"
),
Expand Down Expand Up @@ -84,7 +84,7 @@ pub async fn write_nix_file(
.exec_cmd(
sandbox_name,
&[
"sudo", "bash", "-c",
"bash", "-lc",
&format!(
"mkdir -p /etc/devbox/sets && cat > /etc/devbox/sets/{filename} << 'DEVBOX_EOF'\n{content}\nDEVBOX_EOF"
),
Expand Down
36 changes: 18 additions & 18 deletions src/sandbox/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ pub async fn diff(runtime: &dyn Runtime, sandbox_name: &str) -> Result<Vec<Overl
.exec_cmd(
sandbox_name,
&[
"sudo", "find", UPPER, "-not", "-path", UPPER, "-printf", "%y %P\\n",
"bash", "-lc",
&format!("find {UPPER} -not -path {UPPER} -printf '%y %P\\n'"),
],
false,
)
Expand Down Expand Up @@ -170,7 +171,7 @@ pub async fn commit(
ChangeStatus::Added | ChangeStatus::Modified => {
if change.is_dir {
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "mkdir", "-p", &lower_path], false)
.exec_cmd(sandbox_name, &["mkdir", "-p", &lower_path], false)
.await?;
if result.exit_code != 0 {
eprintln!(
Expand All @@ -191,14 +192,14 @@ pub async fn commit(
);
if !parent.is_empty() && parent != LOWER {
let _ = runtime
.exec_cmd(sandbox_name, &["sudo", "mkdir", "-p", &parent], false)
.exec_cmd(sandbox_name, &["mkdir", "-p", &parent], false)
.await;
}

let result = runtime
.exec_cmd(
sandbox_name,
&["sudo", "cp", "-a", &upper_path, &lower_path],
&["cp", "-a", &upper_path, &lower_path],
false,
)
.await?;
Expand All @@ -214,7 +215,7 @@ pub async fn commit(
}
ChangeStatus::Deleted => {
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "rm", "-rf", &lower_path], false)
.exec_cmd(sandbox_name, &["rm", "-rf", &lower_path], false)
.await?;
if result.exit_code != 0 {
eprintln!(
Expand Down Expand Up @@ -252,7 +253,7 @@ pub async fn discard(
for path in filter_paths {
let upper_path = format!("{UPPER}/{}", path.trim_start_matches('/'));
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "rm", "-rf", &upper_path], false)
.exec_cmd(sandbox_name, &["rm", "-rf", &upper_path], false)
.await?;
if result.exit_code == 0 {
println!(" Discarded: {path}");
Expand All @@ -269,9 +270,8 @@ pub async fn discard(
.exec_cmd(
sandbox_name,
&[
"sudo",
"bash",
"-c",
"-lc",
&format!("rm -rf {UPPER}/* {UPPER}/.[!.]* 2>/dev/null; true"),
],
false,
Expand All @@ -296,7 +296,7 @@ pub async fn stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {

// Move upper to stash
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "mv", UPPER, STASH_DIR], false)
.exec_cmd(sandbox_name, &["mv", UPPER, STASH_DIR], false)
.await?;

if result.exit_code != 0 {
Expand All @@ -305,7 +305,7 @@ pub async fn stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {

// Recreate empty upper directory
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "mkdir", "-p", UPPER], false)
.exec_cmd(sandbox_name, &["mkdir", "-p", UPPER], false)
.await?;

if result.exit_code != 0 {
Expand All @@ -330,7 +330,7 @@ pub async fn stash_pop(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()>
"cp -a {STASH_DIR}/* {UPPER}/ 2>/dev/null; cp -a {STASH_DIR}/.[!.]* {UPPER}/ 2>/dev/null; true"
);
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "bash", "-c", &merge_cmd], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &merge_cmd], false)
.await?;

if result.exit_code != 0 {
Expand All @@ -339,7 +339,7 @@ pub async fn stash_pop(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()>

// Remove the stash directory
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "rm", "-rf", STASH_DIR], false)
.exec_cmd(sandbox_name, &["rm", "-rf", STASH_DIR], false)
.await?;

if result.exit_code != 0 {
Expand All @@ -355,7 +355,7 @@ pub async fn has_stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<bool
// Check if stash directory exists and has contents
let check_cmd = format!("test -d {STASH_DIR} && [ \"$(ls -A {STASH_DIR} 2>/dev/null)\" ]");
let result = runtime
.exec_cmd(sandbox_name, &["bash", "-c", &check_cmd], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &check_cmd], false)
.await?;

Ok(result.exit_code == 0)
Expand All @@ -371,7 +371,7 @@ pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
let result = runtime
.exec_cmd(
sandbox_name,
&["sudo", "mount", "-o", "remount", WORKSPACE],
&["bash", "-lc", &format!("mount -o remount {WORKSPACE}")],
false,
)
.await?;
Expand All @@ -388,7 +388,7 @@ pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
-o lowerdir={LOWER},upperdir={UPPER},workdir={WORK} {WORKSPACE}"
);
let result = runtime
.exec_cmd(sandbox_name, &["sudo", "bash", "-c", &remount_cmd], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &remount_cmd], false)
.await?;

if result.exit_code != 0 {
Expand Down Expand Up @@ -421,7 +421,7 @@ pub async fn conflicts(runtime: &dyn Runtime, sandbox_name: &str) -> Result<Vec<
upper_path, lower_path, upper_path, lower_path
);
let result = runtime
.exec_cmd(sandbox_name, &["bash", "-c", &diff_cmd], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &diff_cmd], false)
.await?;

if result.stdout.trim() == "CONFLICT" {
Expand Down Expand Up @@ -460,7 +460,7 @@ pub async fn lower_layer_changes(runtime: &dyn Runtime, sandbox_name: &str) -> R
LOWER, WORK, LOWER
);
let result = runtime
.exec_cmd(sandbox_name, &["bash", "-c", &cmd], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;

if result.exit_code != 0 {
Expand Down Expand Up @@ -498,7 +498,7 @@ pub async fn conflicts_quiet(
upper_path, lower_path, upper_path, lower_path
);
let result = runtime
.exec_cmd(sandbox_name, &["bash", "-c", &diff_cmd], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &diff_cmd], false)
.await?;

if result.stdout.trim() == "CONFLICT" {
Expand Down
Loading