go/pkg/devops/claude.go
Claude 01d9aa1b73
refactor: rename module from github.com/host-uk/core to forge.lthn.ai/core/cli
Move module identity to our own Forgejo instance. All import paths
updated across 434 Go files, sub-module go.mod files, and go.work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:30:41 +00:00

143 lines
3.6 KiB
Go

package devops
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"forge.lthn.ai/core/cli/pkg/io"
)
// ClaudeOptions configures the Claude sandbox session.
type ClaudeOptions struct {
NoAuth bool // Don't forward any auth
Auth []string // Selective auth: "gh", "anthropic", "ssh", "git"
Model string // Model to use: opus, sonnet
}
// Claude starts a sandboxed Claude session in the dev environment.
func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptions) error {
// Auto-boot if not running
running, err := d.IsRunning(ctx)
if err != nil {
return err
}
if !running {
fmt.Println("Dev environment not running, booting...")
if err := d.Boot(ctx, DefaultBootOptions()); err != nil {
return fmt.Errorf("failed to boot: %w", err)
}
}
// Mount project
if err := d.mountProject(ctx, projectDir); err != nil {
return fmt.Errorf("failed to mount project: %w", err)
}
// Prepare environment variables to forward
envVars := []string{}
if !opts.NoAuth {
authTypes := opts.Auth
if len(authTypes) == 0 {
authTypes = []string{"gh", "anthropic", "ssh", "git"}
}
for _, auth := range authTypes {
switch auth {
case "anthropic":
if key := os.Getenv("ANTHROPIC_API_KEY"); key != "" {
envVars = append(envVars, "ANTHROPIC_API_KEY="+key)
}
case "git":
// Forward git config
name, _ := exec.Command("git", "config", "user.name").Output()
email, _ := exec.Command("git", "config", "user.email").Output()
if len(name) > 0 {
envVars = append(envVars, "GIT_AUTHOR_NAME="+strings.TrimSpace(string(name)))
envVars = append(envVars, "GIT_COMMITTER_NAME="+strings.TrimSpace(string(name)))
}
if len(email) > 0 {
envVars = append(envVars, "GIT_AUTHOR_EMAIL="+strings.TrimSpace(string(email)))
envVars = append(envVars, "GIT_COMMITTER_EMAIL="+strings.TrimSpace(string(email)))
}
}
}
}
// Build SSH command with agent forwarding
args := []string{
"-o", "StrictHostKeyChecking=yes",
"-o", "UserKnownHostsFile=~/.core/known_hosts",
"-o", "LogLevel=ERROR",
"-A", // SSH agent forwarding
"-p", fmt.Sprintf("%d", DefaultSSHPort),
}
args = append(args, "root@localhost")
// Build command to run inside
claudeCmd := "cd /app && claude"
if opts.Model != "" {
claudeCmd += " --model " + opts.Model
}
args = append(args, claudeCmd)
// Set environment for SSH
cmd := exec.CommandContext(ctx, "ssh", args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Pass environment variables through SSH
for _, env := range envVars {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
cmd.Env = append(os.Environ(), env)
}
}
fmt.Println("Starting Claude in sandboxed environment...")
fmt.Println("Project mounted at /app")
fmt.Println("Auth forwarded: SSH agent" + formatAuthList(opts))
fmt.Println()
return cmd.Run()
}
func formatAuthList(opts ClaudeOptions) string {
if opts.NoAuth {
return " (none)"
}
if len(opts.Auth) == 0 {
return ", gh, anthropic, git"
}
return ", " + strings.Join(opts.Auth, ", ")
}
// CopyGHAuth copies GitHub CLI auth to the VM.
func (d *DevOps) CopyGHAuth(ctx context.Context) error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
ghConfigDir := filepath.Join(home, ".config", "gh")
if !io.Local.IsDir(ghConfigDir) {
return nil // No gh config to copy
}
// Use scp to copy gh config
cmd := exec.CommandContext(ctx, "scp",
"-o", "StrictHostKeyChecking=yes",
"-o", "UserKnownHostsFile=~/.core/known_hosts",
"-o", "LogLevel=ERROR",
"-P", fmt.Sprintf("%d", DefaultSSHPort),
"-r", ghConfigDir,
"root@localhost:/root/.config/",
)
return cmd.Run()
}