From fe5cf71d5eb8e47371af2b3eb924b950b1483188 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 02:12:52 +0000 Subject: [PATCH] feat(devops): add Shell for SSH and console access Connects to dev VM via SSH (default) or serial console (--console). Supports SSH agent forwarding for credential access. Co-Authored-By: Claude Opus 4.5 --- pkg/devops/shell.go | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 pkg/devops/shell.go diff --git a/pkg/devops/shell.go b/pkg/devops/shell.go new file mode 100644 index 00000000..fc343d80 --- /dev/null +++ b/pkg/devops/shell.go @@ -0,0 +1,74 @@ +package devops + +import ( + "context" + "fmt" + "os" + "os/exec" +) + +// ShellOptions configures the shell connection. +type ShellOptions struct { + Console bool // Use serial console instead of SSH + Command []string // Command to run (empty = interactive shell) +} + +// Shell connects to the dev environment. +func (d *DevOps) Shell(ctx context.Context, opts ShellOptions) error { + running, err := d.IsRunning(ctx) + if err != nil { + return err + } + if !running { + return fmt.Errorf("dev environment not running (run 'core dev boot' first)") + } + + if opts.Console { + return d.serialConsole(ctx) + } + + return d.sshShell(ctx, opts.Command) +} + +// sshShell connects via SSH. +func (d *DevOps) sshShell(ctx context.Context, command []string) error { + args := []string{ + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=ERROR", + "-A", // Agent forwarding + "-p", "2222", + "root@localhost", + } + + if len(command) > 0 { + args = append(args, command...) + } + + cmd := exec.CommandContext(ctx, "ssh", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// serialConsole attaches to the QEMU serial console. +func (d *DevOps) serialConsole(ctx context.Context) error { + // Find the container to get its console socket + c, err := d.findContainer(ctx, "core-dev") + if err != nil { + return err + } + if c == nil { + return fmt.Errorf("console not available: container not found") + } + + // Use socat to connect to the console socket + socketPath := fmt.Sprintf("/tmp/core-%s-console.sock", c.ID) + cmd := exec.CommandContext(ctx, "socat", "-,raw,echo=0", "unix-connect:"+socketPath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +}