package main

import (
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"time"
)

var (
	logWriter = io.Discard
)

func parseLogLevel(levelStr string) slog.Level {
	switch levelStr {
	case "debug", "DEBUG":
		return slog.LevelDebug
	case "info", "INFO":
		return slog.LevelInfo
	case "warn", "WARN", "warning", "WARNING":
		return slog.LevelWarn
	case "error", "ERROR":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}

func setupLogger() error {
	var writers []io.Writer
	logFile := filepath.Join(os.TempDir(), "container-use.debug.stderr.log")
	if v, ok := os.LookupEnv("CONTAINER_USE_STDERR_FILE"); ok {
		logFile = v
	}
	file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return fmt.Errorf("failed to open log file %s: %w", logFile, err)
	}
	writers = append(writers, file)
	if len(writers) == 0 {
		fmt.Fprintf(os.Stderr, "%s Logging disabled. Set CONTAINER_USE_STDERR_FILE and CONTAINER_USE_LOG_LEVEL environment variables\n", time.Now().Format(time.DateTime))
	}
	logLevel := parseLogLevel(os.Getenv("CONTAINER_USE_LOG_LEVEL"))
	logWriter = io.MultiWriter(writers...)
	handler := slog.NewTextHandler(logWriter, &slog.HandlerOptions{
		Level: logLevel,
	})
	slog.SetDefault(slog.New(handler))
	return nil
}

package integration

import (
	"context"
	"log/slog"
	"os"
	"path/filepath"
	"sync"
	"testing"

	"dagger.io/dagger"
	"github.com/dagger/container-use/environment"
	"github.com/dagger/container-use/repository"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	testDaggerClient *dagger.Client
	daggerOnce       sync.Once
	daggerErr        error
)

// init sets up logging for tests
func init() {
	// Only show warnings and errors in tests unless TEST_VERBOSE is set
	level := slog.LevelWarn
	if os.Getenv("TEST_VERBOSE") != "" {
		level = slog.LevelInfo
	}

	slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
		Level: level,
	})))
}

// WithRepository runs a test function with an isolated repository and UserActions
func WithRepository(t *testing.T, name string, setup RepositorySetup, fn func(t *testing.T, repo *repository.Repository, user *UserActions)) {
	// Initialize Dagger (needed for environment operations)
	initializeDaggerOnce(t)

	ctx := context.Background()

	// Create isolated temp directories
	repoDir, err := os.MkdirTemp("", "cu-test-"+name+"-*")
	require.NoError(t, err, "Failed to create repo dir")

	configDir, err := os.MkdirTemp("", "cu-test-config-"+name+"-*")
	require.NoError(t, err, "Failed to create config dir")

	// Initialize git repo
	cmds := [][]string{
		{"init"},
		{"config", "user.email", "test@example.com"},
		{"config", "user.name", "Test User"},
		{"config", "commit.gpgsign", "false"},
	}

	for _, cmd := range cmds {
		_, err := repository.RunGitCommand(ctx, repoDir, cmd...)
		require.NoError(t, err, "Failed to run git %v", cmd)
	}

	// Run setup to populate repo
	if setup != nil {
		setup(t, repoDir)
	}

	// Open repository with isolated base path
	repo, err := repository.OpenWithBasePath(ctx, repoDir, configDir)
	require.NoError(t, err, "Failed to open repository")

	// Create UserActions with extended capabilities
	user := NewUserActions(t, repo, testDaggerClient).WithDirectAccess(repoDir, configDir)

	// Cleanup
	t.Cleanup(func() {
		// Clean up any environments created during the test
		envs, _ := repo.List(context.Background())
		for _, env := range envs {
			repo.Delete(context.Background(), env.ID)
		}

		// Remove directories
		os.RemoveAll(repoDir)
		os.RemoveAll(configDir)
	})

	// Run the test function
	fn(t, repo, user)
}

// RepositorySetup is a function that prepares a test repository
type RepositorySetup func(t *testing.T, repoDir string)

// Common repository setups
var (
	SetupPythonRepo = func(t *testing.T, repoDir string) {
		writeFile(t, repoDir, "main.py", "def main():\n    print('Hello World')\n\nif __name__ == '__main__':\n    main()\n")
		writeFile(t, repoDir, "requirements.txt", "requests==2.31.0\nnumpy==1.24.0\n")
		writeFile(t, repoDir, ".gitignore", "__pycache__/\n*.pyc\n.env\nvenv/\n")
		gitCommit(t, repoDir, "Initial Python project")
	}

	SetupPythonRepoNoGitignore = func(t *testing.T, repoDir string) {
		writeFile(t, repoDir, "main.py", "def main():\n    print('Hello World')\n\nif __name__ == '__main__':\n    main()\n")
		writeFile(t, repoDir, "requirements.txt", "requests==2.31.0\nnumpy==1.24.0\n")
		gitCommit(t, repoDir, "Initial Python project")
	}

	SetupNodeRepo = func(t *testing.T, repoDir string) {
		packageJSON := `{
  "name": "test-project",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.0"
  }
}`
		writeFile(t, repoDir, "package.json", packageJSON)
		writeFile(t, repoDir, "index.js", "console.log('Hello from Node.js');\n")
		writeFile(t, repoDir, ".gitignore", "node_modules/\n.env\n")
		gitCommit(t, repoDir, "Initial Node project")
	}

	SetupEmptyRepo = func(t *testing.T, repoDir string) {
		writeFile(t, repoDir, "README.md", "# Test Project\n")
		gitCommit(t, repoDir, "Initial commit")
	}
)

// Helper functions for repository setup
func writeFile(t *testing.T, repoDir, path, content string) {
	fullPath := filepath.Join(repoDir, path)
	dir := filepath.Dir(fullPath)
	err := os.MkdirAll(dir, 0755)
	require.NoError(t, err, "Failed to create dir")
	err = os.WriteFile(fullPath, []byte(content), 0644)
	require.NoError(t, err, "Failed to write file")
}

func gitCommit(t *testing.T, repoDir, message string) {
	ctx := context.Background()
	_, err := repository.RunGitCommand(ctx, repoDir, "add", ".")
	require.NoError(t, err, "Failed to stage files")
	_, err = repository.RunGitCommand(ctx, repoDir, "commit", "-m", message)
	require.NoError(t, err, "Failed to commit")
}

// initializeDaggerOnce initializes Dagger client once for all tests
func initializeDaggerOnce(t *testing.T) {
	daggerOnce.Do(func() {
		if testDaggerClient != nil {
			return
		}

		ctx := context.Background()
		client, err := dagger.Connect(ctx)
		if err != nil {
			daggerErr = err
			return
		}

		testDaggerClient = client
	})

	if daggerErr != nil {
		t.Skipf("Skipping test - Dagger not available: %v", daggerErr)
	}
}

// UserActions provides test helpers that mirror MCP tool behavior exactly
// These represent what a user would experience when using the MCP tools
type UserActions struct {
	t         *testing.T
	ctx       context.Context
	repo      *repository.Repository
	dag       *dagger.Client
	repoDir   string // Source directory (for direct manipulation)
	configDir string // Container-use config directory
}

func NewUserActions(t *testing.T, repo *repository.Repository, dag *dagger.Client) *UserActions {
	return &UserActions{
		t:    t,
		ctx:  context.Background(),
		repo: repo,
		dag:  dag,
	}
}

// WithDirectAccess adds direct filesystem access for edge case testing
func (u *UserActions) WithDirectAccess(repoDir, configDir string) *UserActions {
	u.repoDir = repoDir
	u.configDir = configDir
	return u
}

// FileWrite mirrors environment_file_write MCP tool behavior
func (u *UserActions) FileWrite(envID, targetFile, contents, explanation string) {
	env, err := u.repo.Get(u.ctx, u.dag, envID)
	require.NoError(u.t, err, "Failed to get environment %s", envID)

	err = env.FileWrite(u.ctx, explanation, targetFile, contents)
	require.NoError(u.t, err, "FileWrite should succeed")

	err = u.repo.Update(u.ctx, env, explanation)
	require.NoError(u.t, err, "repo.Update after FileWrite should succeed")
}

// RunCommand mirrors environment_run_cmd MCP tool behavior
func (u *UserActions) RunCommand(envID, command, explanation string) string {
	env, err := u.repo.Get(u.ctx, u.dag, envID)
	require.NoError(u.t, err, "Failed to get environment %s", envID)

	output, err := env.Run(u.ctx, command, "/bin/sh", false)
	require.NoError(u.t, err, "Run command should succeed")

	err = u.repo.Update(u.ctx, env, explanation)
	require.NoError(u.t, err, "repo.Update after Run should succeed")

	return output
}

// CreateEnvironment mirrors environment_create MCP tool behavior
func (u *UserActions) CreateEnvironment(title, explanation string) *environment.Environment {
	env, err := u.repo.Create(u.ctx, u.dag, title, explanation)
	require.NoError(u.t, err, "Create environment should succeed")
	return env
}

// UpdateEnvironment mirrors environment_update MCP tool behavior
func (u *UserActions) UpdateEnvironment(envID, title, explanation string, config *environment.EnvironmentConfig) {
	env, err := u.repo.Get(u.ctx, u.dag, envID)
	require.NoError(u.t, err, "Failed to get environment %s", envID)

	if title != "" {
		env.State.Title = title
	}

	err = env.UpdateConfig(u.ctx, config)
	require.NoError(u.t, err, "UpdateConfig should succeed")

	err = u.repo.Update(u.ctx, env, explanation)
	require.NoError(u.t, err, "repo.Update after UpdateConfig should succeed")
}

// FileDelete mirrors environment_file_delete MCP tool behavior
func (u *UserActions) FileDelete(envID, targetFile, explanation string) {
	env, err := u.repo.Get(u.ctx, u.dag, envID)
	require.NoError(u.t, err, "Failed to get environment %s", envID)

	err = env.FileDelete(u.ctx, explanation, targetFile)
	require.NoError(u.t, err, "FileDelete should succeed")

	err = u.repo.Update(u.ctx, env, explanation)
	require.NoError(u.t, err, "repo.Update after FileDelete should succeed")
}

// FileRead mirrors environment_file_read MCP tool behavior (read-only, no update)
func (u *UserActions) FileRead(envID, targetFile string) string {
	env, err := u.repo.Get(u.ctx, u.dag, envID)
	require.NoError(u.t, err, "Failed to get environment %s", envID)

	content, err := env.FileRead(u.ctx, targetFile, true, 0, 0)
	require.NoError(u.t, err, "FileRead should succeed")
	return content
}

// FileReadExpectError is for testing expected failures
func (u *UserActions) FileReadExpectError(envID, targetFile string) {
	env, err := u.repo.Get(u.ctx, u.dag, envID)
	require.NoError(u.t, err, "Failed to get environment %s", envID)

	_, err = env.FileRead(u.ctx, targetFile, true, 0, 0)
	assert.Error(u.t, err, "FileRead should fail for %s", targetFile)
}

// GetEnvironment retrieves an environment by ID - mirrors how MCP tools work
// Each MCP tool call starts fresh by getting the environment from the repository
func (u *UserActions) GetEnvironment(envID string) *environment.Environment {
	env, err := u.repo.Get(u.ctx, u.dag, envID)
	require.NoError(u.t, err, "Should be able to get environment %s", envID)
	return env
}

// --- Direct manipulation methods for edge case testing ---

// WriteSourceFile writes directly to the source repository
func (u *UserActions) WriteSourceFile(path, content string) {
	require.NotEmpty(u.t, u.repoDir, "Need direct access for source file manipulation")
	fullPath := filepath.Join(u.repoDir, path)
	dir := filepath.Dir(fullPath)

	err := os.MkdirAll(dir, 0755)
	require.NoError(u.t, err, "Failed to create dir")

	err = os.WriteFile(fullPath, []byte(content), 0644)
	require.NoError(u.t, err, "Failed to write source file")
}

// WorktreePath returns the worktree path for an environment, handling errors
func (u *UserActions) WorktreePath(envID string) string {
	worktreePath, err := u.repo.WorktreePath(envID)
	require.NoError(u.t, err, "Failed to get worktree path for environment %s", envID)
	return worktreePath
}

// ReadWorktreeFile reads directly from an environment's worktree
func (u *UserActions) ReadWorktreeFile(envID, path string) string {
	worktreePath := u.WorktreePath(envID)
	fullPath := filepath.Join(worktreePath, path)
	content, err := os.ReadFile(fullPath)
	require.NoError(u.t, err, "Failed to read worktree file")
	return string(content)
}

// CorruptWorktree simulates worktree corruption for recovery testing
func (u *UserActions) CorruptWorktree(envID string) {
	worktreePath := u.WorktreePath(envID)

	// Remove .git directory to corrupt the worktree
	gitDir := filepath.Join(worktreePath, ".git")
	err := os.RemoveAll(gitDir)
	require.NoError(u.t, err, "Failed to corrupt worktree")
}

// GitCommand runs a git command in the source repository
func (u *UserActions) GitCommand(args ...string) string {
	require.NotEmpty(u.t, u.repoDir, "Need direct access for git commands")
	output, err := repository.RunGitCommand(u.ctx, u.repoDir, args...)
	require.NoError(u.t, err, "Git command failed: %v", args)
	return output
}

//go:build windows

package main

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	"golang.org/x/term"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var watchCmd = &cobra.Command{
	Use:   "watch",
	Short: "Watch environment activity in real-time",
	Long: `Continuously display environment activity as agents work.
Shows new commits and environment changes updated every second.
Press Ctrl+C to stop watching.`,
	Example: `# Watch all environment activity
container-use watch

# Monitor agents while they work
container-use watch`,
	RunE: func(app *cobra.Command, _ []string) error {
		ctx := app.Context()

		// Ensure we're in a git repository
		if _, err := repository.Open(ctx, "."); err != nil {
			return err
		}

		// Enter alternate screen buffer and hide cursor
		fmt.Print("\x1b[?1049h\x1b[?25l")
		defer fmt.Print("\x1b[?25h\x1b[?1049l") // restore screen + show cursor

		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()

		// Run once immediately
		if err := runGitLogWindows(ctx); err != nil {
			return err
		}

		for {
			select {
			case <-ctx.Done():
				return nil
			case <-ticker.C:
				if err := runGitLogWindows(ctx); err != nil {
					// Don't exit on git errors, just display them and continue
					fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
				}
			}
		}
	},
}

// runGitLogWindows executes the git log command with output matching Unix watch format
func runGitLogWindows(ctx context.Context) error {
	var buf bytes.Buffer

	// Clear screen and move cursor to home position
	buf.WriteString("\x1b[H\x1b[J")

	// Get hostname
	hostname, err := os.Hostname()
	if err != nil {
		hostname = "unknown"
	}

	// Format timestamp like Unix watch (Day Month DD HH:MM:SS YYYY)
	timestamp := time.Now().Format("Mon Jan 2 15:04:05 2006")

	// Create header that matches Unix watch format exactly
	gitCommand := "git log --color=always --remotes=container-use --oneline --graph --decorate"
	headerLine := fmt.Sprintf("Every 1.0s: %s", gitCommand)

	// Get terminal width, fallback to 80 if unable to determine
	terminalWidth := 80
	if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && width > 0 {
		terminalWidth = width
	}

	// Calculate spaces needed to right-align the hostname and timestamp
	rightPart := fmt.Sprintf("%s: %s", hostname, timestamp)
	spacesNeeded := terminalWidth - len(headerLine) - len(rightPart)

	// Write the header line with responsive spacing
	if spacesNeeded >= 1 {
		// Enough space to fit on one line - right align
		buf.WriteString(headerLine + strings.Repeat(" ", spacesNeeded) + rightPart + "\n")
	} else {
		// Not enough space - wrap to next line with minimum 1 space
		buf.WriteString(headerLine + " " + rightPart + "\n")
	}
	buf.WriteString("\n") // Empty line after header to match Unix watch format

	// Run git log command with same arguments as Unix version
	cmd := exec.CommandContext(ctx, "git", "log",
		"--color=always",
		"--remotes=container-use",
		"--oneline",
		"--graph",
		"--decorate")

	// Create pipe to capture output
	pr, pw, err := os.Pipe()
	if err != nil {
		return fmt.Errorf("failed to create pipe: %w", err)
	}

	cmd.Stdout = pw
	cmd.Stderr = pw

	// Start the command
	if err := cmd.Start(); err != nil {
		pw.Close()
		pr.Close()
		return fmt.Errorf("failed to start git log: %w", err)
	}

	// Close write end so we can read
	pw.Close()

	// Read all output into buffer
	scanner := bufio.NewScanner(pr)
	for scanner.Scan() {
		buf.WriteString(scanner.Text() + "\n")
	}

	pr.Close()

	// Wait for command to complete
	if err := cmd.Wait(); err != nil {
		return fmt.Errorf("git log failed: %w", err)
	}

	// Output everything at once for smooth rendering
	os.Stdout.Write(buf.Bytes())

	return nil
}

func init() {
	rootCmd.AddCommand(watchCmd)
}

package agent

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/dagger/container-use/mcpserver"
	"github.com/spf13/cobra"
)

type MCPServersConfig struct {
	MCPServers map[string]MCPServer `json:"mcpServers"`
}

type MCPServer struct {
	Command       string            `json:"command"`
	Args          []string          `json:"args"`
	Env           map[string]string `json:"env,omitempty"`
	Timeout       *int              `json:"timeout,omitempty"`
	Disabled      *bool             `json:"disabled,omitempty"`
	AutoApprove   []string          `json:"autoApprove,omitempty"`
	AlwaysAllow   []string          `json:"alwaysAllow,omitempty"`
	WorkingDir    *string           `json:"working_directory,omitempty"`
	StartOnLaunch *bool             `json:"start_on_launch,omitempty"`
}

const ContainerUseBinary = "container-use"

var AgentCmd = &cobra.Command{
	Use:   "agent [agent]",
	Short: "Configure MCP server for different agents",
	Long:  `Setup the container-use MCP server according to the specified agent including Claude Code, Goose, Cursor, and others.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if len(args) == 0 {
			return interactiveConfiguration()
		}
		agent, err := selectAgent(args[0])
		if err != nil {
			return err
		}
		return configureAgent(agent)
	},
}

func interactiveConfiguration() error {
	selectedAgent, err := RunAgentSelector()
	if err != nil {
		// If the user quits, it's not an error, just exit gracefully.
		if err.Error() == "no agent selected" {
			return nil
		}
		return fmt.Errorf("failed to select agent: %w", err)
	}

	agent, err := selectAgent(selectedAgent)
	if err != nil {
		return err
	}
	return configureAgent(agent)
}

type ConfigurableAgent interface {
	name() string
	description() string
	editMcpConfig() error
	editRules() error
	isInstalled() bool
}

// Add agents here
func selectAgent(agentKey string) (ConfigurableAgent, error) {
	// Check if agent is supported on current platform
	if runtime.GOOS == "windows" {
		switch agentKey {
		case "codex", "amazonq":
			return nil, fmt.Errorf("agent '%s' is not supported on native Windows.\nTo use this agent, please install and run container-use in Windows Subsystem for Linux (WSL)", agentKey)
		}
	}

	switch agentKey {
	case "claude":
		return &ConfigureClaude{}, nil
	case "goose":
		return &ConfigureGoose{}, nil
	case "cursor":
		return &ConfigureCursor{}, nil
	case "codex":
		return &ConfigureCodex{}, nil
	case "amazonq":
		return &ConfigureQ{}, nil
	}
	return nil, fmt.Errorf("unknown agent: %s", agentKey)
}

func configureAgent(agent ConfigurableAgent) error {
	fmt.Printf("Configuring %s...\n", agent.name())

	// Save MCP config
	err := agent.editMcpConfig()
	if err != nil {
		return err
	}
	fmt.Printf("✓ Configured %s MCP configuration\n", agent.name())

	// Save rules
	err = agent.editRules()
	if err != nil {
		return err
	}
	fmt.Printf("✓ Saved %s container-use rules\n", agent.name())

	fmt.Printf("\n%s configuration complete!\n", agent.name())
	return nil
}

// Helper functions
func saveRulesFile(rulesFile, content string) error {
	dir := filepath.Dir(rulesFile)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	// Append to file if it exists, create if it doesn't TODO make it re-entrant with a marker
	existing, err := os.ReadFile(rulesFile)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to read existing rules: %w", err)
	}
	existingStr := string(existing)

	editedRules, err := editRulesFile(existingStr, content)
	if err != nil {
		return err
	}

	err = os.WriteFile(rulesFile, []byte(editedRules), 0644)
	if err != nil {
		return fmt.Errorf("failed to update rules: %w", err)
	}

	return nil
}

func editRulesFile(existingRules, content string) (string, error) {
	// Look for section markers
	const marker = "<!-- container-use-rules -->"

	if strings.Contains(existingRules, marker) {
		// Update existing section
		parts := strings.Split(existingRules, marker)
		if len(parts) != 3 {
			return "", fmt.Errorf("malformed rules file - expected single section marked with %s", marker)
		}
		newContent := parts[0] + marker + "\n" + content + "\n" + marker + parts[2]
		return newContent, nil
	} else {
		// Append new section
		newContent := existingRules
		if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") {
			newContent += "\n"
		}
		newContent += "\n" + marker + "\n" + content + "\n" + marker + "\n"
		return newContent, nil
	}
}

func tools(prefix string) []string {
	tools := []string{}
	for _, t := range mcpserver.Tools() {
		tools = append(tools, fmt.Sprintf("%s%s", prefix, t.Definition.Name))
	}
	return tools
}

package mcpserver

import (
	"context"
	_ "embed"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"log/slog"
	"os"
	"os/signal"

	"dagger.io/dagger"
	"github.com/dagger/container-use/environment"
	"github.com/dagger/container-use/repository"
	"github.com/dagger/container-use/rules"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

type daggerClientKey struct{}

func openRepository(ctx context.Context, request mcp.CallToolRequest) (*repository.Repository, error) {
	source, err := request.RequireString("environment_source")
	if err != nil {
		return nil, err
	}
	repo, err := repository.Open(ctx, source)
	if err != nil {
		return nil, fmt.Errorf("unable to open repository: %w", err)
	}
	return repo, nil
}

func openEnvironment(ctx context.Context, request mcp.CallToolRequest) (*repository.Repository, *environment.Environment, error) {
	repo, err := openRepository(ctx, request)
	if err != nil {
		return nil, nil, err
	}
	envID, err := request.RequireString("environment_id")
	if err != nil {
		return nil, nil, err
	}
	dag, ok := ctx.Value(daggerClientKey{}).(*dagger.Client)
	if !ok {
		return nil, nil, fmt.Errorf("dagger client not found in context")
	}
	env, err := repo.Get(ctx, dag, envID)
	if err != nil {
		return nil, nil, fmt.Errorf("unable to get environment: %w", err)
	}
	return repo, env, nil
}

type Tool struct {
	Definition mcp.Tool
	Handler    server.ToolHandlerFunc
}

func RunStdioServer(ctx context.Context, dag *dagger.Client) error {
	s := server.NewMCPServer(
		"Dagger",
		"1.0.0",
		server.WithInstructions(rules.AgentRules),
	)

	for _, t := range tools {
		s.AddTool(t.Definition, wrapToolWithClient(t, dag).Handler)
	}

	slog.Info("starting server")

	stdioSrv := server.NewStdioServer(s)
	stdioSrv.SetErrorLogger(log.Default()) // this should re-use our `slog` handler

	ctx, cancel := signal.NotifyContext(ctx, getNotifySignals()...)
	defer cancel()

	err := stdioSrv.Listen(ctx, os.Stdin, os.Stdout)
	if err != nil && !errors.Is(err, context.Canceled) {
		return err
	}
	return nil
}

var tools = []*Tool{}

func Tools() []*Tool {
	return tools
}

func registerTool(tool ...*Tool) {
	for _, t := range tool {
		tools = append(tools, wrapTool(t))
	}
}

func wrapTool(tool *Tool) *Tool {
	return &Tool{
		Definition: tool.Definition,
		Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			slog.Info("Tool called", "tool", tool.Definition.Name)
			defer func() {
				slog.Info("Tool finished", "tool", tool.Definition.Name)
			}()
			response, err := tool.Handler(ctx, request)
			if err != nil {
				return mcp.NewToolResultError(err.Error()), nil
			}
			return response, nil
		},
	}
}

// keeping this modular for now. we could move tool registration to RunStdioServer and collapse the 2 wrapTool functions.
func wrapToolWithClient(tool *Tool, dag *dagger.Client) *Tool {
	return &Tool{
		Definition: tool.Definition,
		Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			ctx = context.WithValue(ctx, daggerClientKey{}, dag)
			return tool.Handler(ctx, request)
		},
	}
}

func init() {
	registerTool(
		EnvironmentOpenTool,
		EnvironmentCreateTool,
		EnvironmentUpdateMetadataTool,
		EnvironmentConfigTool,

		EnvironmentRunCmdTool,

		EnvironmentFileReadTool,
		EnvironmentFileListTool,
		EnvironmentFileWriteTool,
		EnvironmentFileEditTool,
		EnvironmentFileDeleteTool,

		EnvironmentAddServiceTool,

		EnvironmentCheckpointTool,
	)
}

type EnvironmentResponse struct {
	ID              string                         `json:"id"`
	Title           string                         `json:"title"`
	Config          *environment.EnvironmentConfig `json:"config"`
	RemoteRef       string                         `json:"remote_ref"`
	CheckoutCommand string                         `json:"checkout_command_to_share_with_user"`
	LogCommand      string                         `json:"log_command_to_share_with_user"`
	DiffCommand     string                         `json:"diff_command_to_share_with_user"`
	Services        []*environment.Service         `json:"services,omitempty"`
}

func environmentResponseFromEnvInfo(envInfo *environment.EnvironmentInfo) *EnvironmentResponse {
	return &EnvironmentResponse{
		ID:              envInfo.ID,
		Title:           envInfo.State.Title,
		Config:          envInfo.State.Config,
		RemoteRef:       fmt.Sprintf("container-use/%s", envInfo.ID),
		CheckoutCommand: fmt.Sprintf("container-use checkout %s", envInfo.ID),
		LogCommand:      fmt.Sprintf("container-use log %s", envInfo.ID),
		DiffCommand:     fmt.Sprintf("container-use diff %s", envInfo.ID),
		Services:        nil, // EnvironmentInfo doesn't have "active" services, specifically useful for EndpointMappings
	}
}

func environmentResponseFromEnv(env *environment.Environment) *EnvironmentResponse {
	resp := environmentResponseFromEnvInfo(env.EnvironmentInfo)
	resp.Services = env.Services
	return resp
}

func marshalEnvironment(env *environment.Environment) (string, error) {
	out, err := json.Marshal(environmentResponseFromEnv(env))
	if err != nil {
		return "", fmt.Errorf("failed to marshal response: %w", err)
	}
	return string(out), nil
}

func marshalEnvironmentInfo(envInfo *environment.EnvironmentInfo) (string, error) {
	out, err := json.Marshal(environmentResponseFromEnvInfo(envInfo))
	if err != nil {
		return "", fmt.Errorf("failed to marshal response: %w", err)
	}
	return string(out), nil
}

func EnvironmentToCallResult(env *environment.Environment) (*mcp.CallToolResult, error) {
	out, err := marshalEnvironment(env)
	if err != nil {
		return nil, err
	}
	return mcp.NewToolResultText(out), nil
}

func EnvironmentInfoToCallResult(envInfo *environment.EnvironmentInfo) (*mcp.CallToolResult, error) {
	out, err := marshalEnvironmentInfo(envInfo)
	if err != nil {
		return nil, err
	}
	return mcp.NewToolResultText(out), nil
}

var EnvironmentOpenTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_open",
		"Opens an existing environment. Return format is same as environment_create.",
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		_, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}
		return EnvironmentToCallResult(env)
	},
}

var EnvironmentCreateTool = &Tool{
	Definition: newRepositoryTool(
		"environment_create",
		`Creates a new development environment.
The environment is the result of a the setups commands on top of the base image.
Environment configuration is managed by the user via cu config commands.`,
		mcp.WithString("title",
			mcp.Description("Short description of the work that is happening in this environment."),
			mcp.Required(),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, err := openRepository(ctx, request)
		if err != nil {
			return nil, err
		}
		title, err := request.RequireString("title")
		if err != nil {
			return nil, err
		}

		dag, ok := ctx.Value(daggerClientKey{}).(*dagger.Client)
		if !ok {
			return nil, fmt.Errorf("dagger client not found in context")
		}

		env, err := repo.Create(ctx, dag, title, request.GetString("explanation", ""))
		if err != nil {
			return nil, fmt.Errorf("failed to create environment: %w", err)
		}

		out, err := marshalEnvironment(env)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal environment: %w", err)
		}

		dirty, status, err := repo.IsDirty(ctx)
		if err != nil {
			return nil, fmt.Errorf("unable to check if environment is dirty: %w", err)
		}

		if !dirty {
			return mcp.NewToolResultText(out), nil
		}

		return mcp.NewToolResultText(fmt.Sprintf(`%s

CRITICAL: You MUST inform the user that the repository %s has uncommitted changes that are NOT included in this environment. The environment was created from the last committed state only.

Uncommitted changes detected:
%s

You MUST tell the user: To include these changes in the environment, they need to commit them first using git commands outside the environment.`, out, request.GetString("environment_source", ""), status)), nil
	},
}

var EnvironmentUpdateMetadataTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_update_metadata",
		"Update environment metadata such as title. This updates the descriptive information about what work is being done in the environment.",
		mcp.WithString("title",
			mcp.Description("Updated title describing the work being done in this environment."),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}

		// Update title if provided
		if title := request.GetString("title", ""); title != "" {
			env.State.Title = title
		}

		if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
			return nil, fmt.Errorf("unable to update the environment: %w", err)
		}

		out, err := marshalEnvironment(env)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal environment: %w", err)
		}
		return mcp.NewToolResultText(fmt.Sprintf("Environment metadata updated successfully.\n%s", out)), nil
	},
}

var EnvironmentConfigTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_config",
		"Make environment config changes such as base image and setup commands."+
			"If the environment is missing any tools or instructions, you MUST call this function to update the environment."+
			"You MUST update the environment with any useful tools. You will be resumed with no other context than the information provided here",
		mcp.WithObject("config",
			mcp.Required(),
			mcp.Properties(map[string]any{
				"base_image": map[string]any{
					"type":        "string",
					"description": "Base image for the environment",
				},
				"setup_commands": map[string]any{
					"type":        "array",
					"description": "Commands that should be executed on top of the base image to set up the environment. Similar to `RUN` instructions in Dockerfiles.",
					"items":       map[string]any{"type": "string"},
				},
				"envs": map[string]any{
					"type":        "array",
					"description": "The environment variables to set (e.g. `[\"FOO=bar\", \"BAZ=qux\"]`).",
					"items":       map[string]any{"type": "string"},
				},
			}),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}

		updatedConfig := env.State.Config.Copy()

		newConfig, ok := request.GetArguments()["config"].(map[string]any)
		if !ok {
			return nil, errors.New("invalid config")
		}

		if baseImage, ok := newConfig["base_image"].(string); ok {
			updatedConfig.BaseImage = baseImage
		}

		if setupCommands, ok := newConfig["setup_commands"].([]any); ok {
			updatedConfig.SetupCommands = make([]string, len(setupCommands))
			for i, command := range setupCommands {
				updatedConfig.SetupCommands[i] = command.(string)
			}
		}

		if envs, ok := newConfig["envs"].([]any); ok {
			updatedConfig.Env = make([]string, len(envs))
			for i, env := range envs {
				updatedConfig.Env[i] = env.(string)
			}
		}

		if err := env.UpdateConfig(ctx, updatedConfig); err != nil {
			return nil, fmt.Errorf("unable to update the environment: %w", err)
		}

		if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
			return nil, fmt.Errorf("failed to update repository: %w", err)
		}

		out, err := marshalEnvironment(env)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal environment: %w", err)
		}

		message := fmt.Sprintf(`SUCCESS: Configuration successfully applied. Environment has been restarted, all previous commands have been lost.
IMPORTANT: The configuration changes are LOCAL to this environment.
TELL THE USER: To make these changes persistent, they will have to run "cu config import %s"

%s
`, env.ID, out)

		return mcp.NewToolResultText(message), nil
	},
}

var EnvironmentListTool = &Tool{
	Definition: newRepositoryTool(
		"environment_list",
		"List available environments",
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, err := openRepository(ctx, request)
		if err != nil {
			return nil, err
		}
		envInfos, err := repo.List(ctx)
		if err != nil {
			return nil, fmt.Errorf("invalid source: %w", err)
		}

		// Convert EnvironmentInfo slice to EnvironmentResponse slice
		responses := make([]EnvironmentResponse, len(envInfos))
		for i, envInfo := range envInfos {
			responses[i] = *environmentResponseFromEnvInfo(envInfo)
		}

		out, err := json.Marshal(responses)
		if err != nil {
			return nil, err
		}
		return mcp.NewToolResultText(string(out)), nil
	},
}

var EnvironmentRunCmdTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_run_cmd",
		"Run a terminal command inside a NEW container within the environment.",
		mcp.WithString("command",
			mcp.Description("The terminal command to execute. If empty, the environment's default command is used."),
		),
		mcp.WithString("shell",
			mcp.Description("The shell that will be interpreting this command (default: sh)"),
		),
		mcp.WithBoolean("background",
			mcp.Description(`Run the command in the background
Must ALWAYS be set for long running command (e.g. http server).
Failure to do so will result in the tool being stuck, awaiting for the command to finish.`,
			),
		),
		mcp.WithBoolean("use_entrypoint",
			mcp.Description("Use the image entrypoint, if present, by prepending it to the args."),
		),
		mcp.WithArray("ports",
			mcp.Description("Ports to expose. Only works with background environments. For each port, returns the environment_internal (for use inside environments) and host_external (for use by the user) addresses."),
			mcp.Items(map[string]any{"type": "number"}),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}

		command := request.GetString("command", "")
		shell := request.GetString("shell", "sh")

		updateRepo := func() error {
			if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
				return fmt.Errorf("failed to update repository: %w", err)
			}
			return nil
		}

		background := request.GetBool("background", false)
		if background {
			ports := []int{}
			if portList, ok := request.GetArguments()["ports"].([]any); ok {
				for _, port := range portList {
					ports = append(ports, int(port.(float64)))
				}
			}
			endpoints, runErr := env.RunBackground(ctx, command, shell, ports, request.GetBool("use_entrypoint", false))
			// We want to update the repository even if the command failed.
			if err := updateRepo(); err != nil {
				return nil, err
			}
			if runErr != nil {
				return nil, fmt.Errorf("failed to run command: %w", runErr)
			}

			out, err := json.Marshal(endpoints)
			if err != nil {
				return nil, err
			}

			return mcp.NewToolResultText(fmt.Sprintf(`Command started in the background in NEW container. Endpoints are %s

To access from the user's machine: use host_external. To access from other commands in this environment: use environment_internal.

Any changes to the container workdir (%s) WILL NOT be committed to container-use/%s

Background commands are unaffected by filesystem and any other kind of changes. You need to start a new command for changes to take effect.`,
				string(out), env.State.Config.Workdir, env.ID)), nil
		}

		stdout, runErr := env.Run(ctx, command, shell, request.GetBool("use_entrypoint", false))
		// We want to update the repository even if the command failed.
		if err := updateRepo(); err != nil {
			return nil, err
		}
		if runErr != nil {
			return nil, fmt.Errorf("failed to run command: %w", runErr)
		}

		return mcp.NewToolResultText(fmt.Sprintf("%s\n\nAny changes to the container workdir (%s) have been committed and pushed to container-use/ remote", stdout, env.State.Config.Workdir)), nil
	},
}

var EnvironmentFileReadTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_file_read",
		"Read the contents of a file, specifying a line range or the entire file.",
		mcp.WithString("target_file",
			mcp.Description("Path of the file to read, absolute or relative to the workdir"),
			mcp.Required(),
		),
		mcp.WithBoolean("should_read_entire_file",
			mcp.Description("Whether to read the entire file. Defaults to false."),
		),
		mcp.WithNumber("start_line_one_indexed_inclusive",
			mcp.Description("The one-indexed line number to start reading from (inclusive)."),
		),
		mcp.WithNumber("end_line_one_indexed_inclusive",
			mcp.Description("The one-indexed line number to end reading at (inclusive)."),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		_, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}

		targetFile, err := request.RequireString("target_file")
		if err != nil {
			return nil, err
		}
		shouldReadEntireFile := request.GetBool("should_read_entire_file", false)
		startLineOneIndexedInclusive := request.GetInt("start_line_one_indexed_inclusive", 0)
		endLineOneIndexedInclusive := request.GetInt("end_line_one_indexed_inclusive", 0)

		fileContents, err := env.FileRead(ctx, targetFile, shouldReadEntireFile, startLineOneIndexedInclusive, endLineOneIndexedInclusive)
		if err != nil {
			return nil, fmt.Errorf("failed to read file: %w", err)
		}

		return mcp.NewToolResultText(fileContents), nil
	},
}

var EnvironmentFileListTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_file_list",
		"List the contents of a directory",
		mcp.WithString("path",
			mcp.Description("Path of the directory to list contents of, absolute or relative to the workdir"),
			mcp.Required(),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		_, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}

		path, err := request.RequireString("path")
		if err != nil {
			return nil, err
		}

		out, err := env.FileList(ctx, path)
		if err != nil {
			return nil, fmt.Errorf("failed to list directory: %w", err)
		}

		return mcp.NewToolResultText(out), nil
	},
}

var EnvironmentFileWriteTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_file_write",
		"Write the contents of a file.",
		mcp.WithString("target_file",
			mcp.Description("Path of the file to write, absolute or relative to the workdir."),
			mcp.Required(),
		),
		mcp.WithString("contents",
			mcp.Description("Full text content of the file you want to write."),
			mcp.Required(),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}

		targetFile, err := request.RequireString("target_file")
		if err != nil {
			return nil, err
		}
		contents, err := request.RequireString("contents")
		if err != nil {
			return nil, err
		}

		if err := env.FileWrite(ctx, request.GetString("explanation", ""), targetFile, contents); err != nil {
			return nil, fmt.Errorf("failed to write file: %w", err)
		}

		if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
			return nil, fmt.Errorf("unable to update the environment: %w", err)
		}

		return mcp.NewToolResultText(fmt.Sprintf("file %s written successfully and committed to container-use/ remote", targetFile)), nil
	},
}

var EnvironmentFileEditTool = &Tool{
	Definition: mcp.NewTool("environment_file_edit",
		mcp.WithDescription("Find and replace text in a file."),
		mcp.WithString("explanation",
			mcp.Description("One sentence explanation for why this file is being edited."),
		),
		mcp.WithString("environment_source",
			mcp.Description("Absolute path to the source git repository for the environment."),
			mcp.Required(),
		),
		mcp.WithString("environment_id",
			mcp.Description("The ID of the environment for this command. Must call `environment_create` first."),
			mcp.Required(),
		),
		mcp.WithString("target_file",
			mcp.Description("Path of the file to write, absolute or relative to the workdir."),
			mcp.Required(),
		),
		mcp.WithString("search_text",
			mcp.Description("The text to find and replace."),
			mcp.Required(),
		),
		mcp.WithString("replace_text",
			mcp.Description("The text to insert."),
			mcp.Required(),
		),
		mcp.WithString("which_match",
			mcp.Description("The ID of the match to replace, if there were multiple matches."),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, env, err := openEnvironment(ctx, request)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("unable to open the environment", err), nil
		}

		targetFile, err := request.RequireString("target_file")
		if err != nil {
			return nil, err
		}
		search, err := request.RequireString("search_text")
		if err != nil {
			return nil, err
		}
		replace, err := request.RequireString("replace_text")
		if err != nil {
			return nil, err
		}

		if err := env.FileEdit(ctx,
			request.GetString("explanation", ""),
			targetFile,
			search,
			replace,
			request.GetString("which_match", ""),
		); err != nil {
			return mcp.NewToolResultErrorFromErr("failed to write file", err), nil
		}

		if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
			return mcp.NewToolResultErrorFromErr("unable to update the environment", err), nil
		}

		return mcp.NewToolResultText(fmt.Sprintf("file %s edited successfully and committed to container-use/ remote", targetFile)), nil
	},
}

var EnvironmentFileDeleteTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_file_delete",
		"Deletes a file at the specified path.",
		mcp.WithString("target_file",
			mcp.Description("Path of the file to delete, absolute or relative to the workdir."),
			mcp.Required(),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}

		targetFile, err := request.RequireString("target_file")
		if err != nil {
			return nil, err
		}

		if err := env.FileDelete(ctx, request.GetString("explanation", ""), targetFile); err != nil {
			return nil, fmt.Errorf("failed to delete file: %w", err)
		}

		if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
			return nil, fmt.Errorf("failed to update env: %w", err)
		}

		return mcp.NewToolResultText(fmt.Sprintf("file %s deleted successfully and committed to container-use/ remote", targetFile)), nil
	},
}

var EnvironmentCheckpointTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_checkpoint",
		"Checkpoints an environment in its current state as a container.",
		mcp.WithString("destination",
			mcp.Description("Container image destination to checkpoint to (e.g. registry.com/user/image:tag"),
			mcp.Required(),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		_, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}
		destination, err := request.RequireString("destination")
		if err != nil {
			return nil, err
		}

		endpoint, err := env.Checkpoint(ctx, destination)
		if err != nil {
			return nil, fmt.Errorf("failed to checkpoint environment: %w", err)
		}
		return mcp.NewToolResultText(fmt.Sprintf("Checkpoint pushed to %q. You MUST use the full content addressed (@sha256:...) reference in `docker` commands. The entrypoint is set to `sh`, keep that in mind when giving commands to the container.", endpoint)), nil
	},
}

var EnvironmentAddServiceTool = &Tool{
	Definition: newEnvironmentTool(
		"environment_add_service",
		"Add a service to the environment (e.g. database, cache, etc.)",
		mcp.WithString("name",
			mcp.Description("The name of the service to start."),
			mcp.Required(),
		),
		mcp.WithString("image",
			mcp.Description("The image of the service to start."),
			mcp.Required(),
		),
		mcp.WithString("command",
			mcp.Description("The command to start the service. If not provided the image default command will be used."),
		),
		mcp.WithArray("ports",
			mcp.Description("Ports to expose. For each port, returns the container_internal (for use by environments) and host_external (for use by the user) address."),
			mcp.Items(map[string]any{"type": "number"}),
		),
		mcp.WithArray("envs",
			mcp.Description("The environment variables to set (e.g. `[\"FOO=bar\", \"BAZ=qux\"]`)."),
			mcp.Items(map[string]any{"type": "string"}),
		),
	),
	Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		repo, env, err := openEnvironment(ctx, request)
		if err != nil {
			return nil, err
		}
		serviceName, err := request.RequireString("name")
		if err != nil {
			return nil, err
		}
		image, err := request.RequireString("image")
		if err != nil {
			return nil, err
		}
		command := request.GetString("command", "")
		ports := []int{}
		if portList, ok := request.GetArguments()["ports"].([]any); ok {
			for _, port := range portList {
				ports = append(ports, int(port.(float64)))
			}
		}

		envs := request.GetStringSlice("envs", []string{})

		service, err := env.AddService(ctx, request.GetString("explanation", ""), &environment.ServiceConfig{
			Name:         serviceName,
			Image:        image,
			Command:      command,
			ExposedPorts: ports,
			Env:          envs,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to add service: %w", err)
		}

		if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
			return nil, fmt.Errorf("failed to update env: %w", err)
		}

		output, err := json.Marshal(service)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal service: %w", err)
		}

		return mcp.NewToolResultText(fmt.Sprintf("Service added and started successfully: %s", string(output))), nil
	},
}

package repository

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"slices"
	"strings"

	"dagger.io/dagger"
	"github.com/dagger/container-use/environment"
	"github.com/mitchellh/go-homedir"
)

const (
	maxFileSizeForTextCheck = 10 * 1024 * 1024 // 10MB
)

var (
	urlSchemeRegExp  = regexp.MustCompile(`^[^:]+://`)
	scpLikeURLRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5})(?:\/|:))?(?P<path>[^\\].*\/[^\\].*)$`)
)

// RunGitCommand executes a git command in the specified directory.
// This is exported for use in tests and other packages that need direct git access.
func RunGitCommand(ctx context.Context, dir string, args ...string) (out string, rerr error) {
	slog.Info(fmt.Sprintf("[%s] $ git %s", dir, strings.Join(args, " ")))
	defer func() {
		slog.Info(fmt.Sprintf("[%s] $ git %s (DONE)", dir, strings.Join(args, " ")), "err", rerr)
	}()

	cmd := exec.CommandContext(ctx, "git", args...)
	cmd.Dir = dir

	output, err := cmd.CombinedOutput()
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			return "", fmt.Errorf("git command failed (exit code %d): %w\nOutput: %s",
				exitErr.ExitCode(), err, string(output))
		}
		return "", fmt.Errorf("git command failed: %w", err)
	}

	return string(output), nil
}

// RunInteractiveGitCommand executes a git command in the specified directory in interactive mode.
func RunInteractiveGitCommand(ctx context.Context, dir string, w io.Writer, args ...string) (rerr error) {
	slog.Info(fmt.Sprintf("[%s] $ git %s", dir, strings.Join(args, " ")))
	defer func() {
		slog.Info(fmt.Sprintf("[%s] $ git %s (DONE)", dir, strings.Join(args, " ")), "err", rerr)
	}()

	cmd := exec.CommandContext(ctx, "git", args...)
	cmd.Dir = dir
	cmd.Stdout = w
	cmd.Stderr = w

	return cmd.Run()
}

func getContainerUseRemote(ctx context.Context, repo string) (string, error) {
	// Check if we already have a container-use remote
	cuRemote, err := RunGitCommand(ctx, repo, "remote", "get-url", "container-use")
	if err != nil {
		// Check for exit code 2 which means the remote doesn't exist
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) && exitErr.ExitCode() == 2 {
			return "", os.ErrNotExist
		}
		return "", err
	}

	return strings.TrimSpace(cuRemote), nil
}

func (r *Repository) WorktreePath(id string) (string, error) {
	return homedir.Expand(filepath.Join(r.getWorktreePath(), id))
}

func (r *Repository) deleteWorktree(id string) error {
	worktreePath, err := r.WorktreePath(id)
	if err != nil {
		return err
	}
	fmt.Printf("Deleting worktree at %s\n", worktreePath)
	return os.RemoveAll(worktreePath)
}

func (r *Repository) deleteLocalRemoteBranch(id string) error {
	slog.Info("Pruning git worktrees", "repo", r.forkRepoPath)
	if _, err := RunGitCommand(context.Background(), r.forkRepoPath, "worktree", "prune"); err != nil {
		slog.Error("Failed to prune git worktrees", "repo", r.forkRepoPath, "err", err)
		return err
	}

	slog.Info("Deleting local branch", "repo", r.forkRepoPath, "branch", id)
	if _, err := RunGitCommand(context.Background(), r.forkRepoPath, "branch", "-D", id); err != nil {
		slog.Error("Failed to delete local branch", "repo", r.forkRepoPath, "branch", id, "err", err)
		return err
	}

	if _, err := RunGitCommand(context.Background(), r.userRepoPath, "remote", "prune", containerUseRemote); err != nil {
		slog.Error("Failed to fetch and prune container-use remote", "local-repo", r.userRepoPath, "err", err)
		return err
	}

	return nil
}

func (r *Repository) initializeWorktree(ctx context.Context, id string) (string, error) {
	worktreePath, err := r.WorktreePath(id)
	if err != nil {
		return "", err
	}

	if _, err := os.Stat(worktreePath); err == nil {
		return worktreePath, nil
	}

	slog.Info("Initializing worktree", "repository", r.userRepoPath, "container-id", id)

	currentHead, err := RunGitCommand(ctx, r.userRepoPath, "rev-parse", "HEAD")
	if err != nil {
		return "", err
	}
	currentHead = strings.TrimSpace(currentHead)

	_, err = RunGitCommand(ctx, r.userRepoPath, "push", containerUseRemote, fmt.Sprintf("%s:refs/heads/%s", currentHead, id))
	if err != nil {
		return "", err
	}

	_, err = RunGitCommand(ctx, r.forkRepoPath, "worktree", "add", worktreePath, id)
	if err != nil {
		return "", err
	}

	_, err = RunGitCommand(ctx, r.userRepoPath, "fetch", containerUseRemote, id)
	if err != nil {
		return "", err
	}

	return worktreePath, nil
}

// createInitialCommit creates an empty commit with the environment creation message - this prevents multiple environments from overwriting the container-use-state on the parent commit
func (r *Repository) createInitialCommit(ctx context.Context, worktreePath, id, title string) error {
	commitMessage := fmt.Sprintf("Create environment %s: %s", id, title)
	_, err := RunGitCommand(ctx, worktreePath, "commit", "--allow-empty", "-m", commitMessage)
	return err
}

func (r *Repository) propagateToWorktree(ctx context.Context, env *environment.Environment, explanation string) (rerr error) {
	slog.Info("Propagating to worktree...",
		"environment.id", env.ID,
		"workdir", env.State.Config.Workdir,
		"id", env.ID)
	defer func() {
		slog.Info("Propagating to worktree... (DONE)",
			"environment.id", env.ID,
			"workdir", env.State.Config.Workdir,
			"id", env.ID,
			"err", rerr)
	}()

	if err := r.exportEnvironment(ctx, env); err != nil {
		return err
	}

	worktreePath, err := r.WorktreePath(env.ID)
	if err != nil {
		return fmt.Errorf("failed to get worktree path: %w", err)
	}
	if err := r.commitWorktreeChanges(ctx, worktreePath, explanation); err != nil {
		return fmt.Errorf("failed to commit worktree changes: %w", err)
	}

	if err := r.saveState(ctx, env); err != nil {
		return fmt.Errorf("failed to add notes: %w", err)
	}

	slog.Info("Fetching container-use remote in source repository")
	if _, err := RunGitCommand(ctx, r.userRepoPath, "fetch", containerUseRemote, env.ID); err != nil {
		return err
	}

	if err := r.propagateGitNotes(ctx, gitNotesStateRef); err != nil {
		return err
	}

	return nil
}

func (r *Repository) exportEnvironment(ctx context.Context, env *environment.Environment) error {
	worktreePointer := fmt.Sprintf("gitdir: %s", filepath.Join(r.forkRepoPath, "worktrees", env.ID))

	worktreePath, err := r.WorktreePath(env.ID)
	if err != nil {
		return fmt.Errorf("failed to get worktree path: %w", err)
	}

	_, err = env.Workdir().
		WithNewFile(".git", worktreePointer).
		Export(
			ctx,
			worktreePath,
			dagger.DirectoryExportOpts{Wipe: true},
		)
	if err != nil {
		return err
	}

	return nil
}
func (r *Repository) propagateGitNotes(ctx context.Context, ref string) error {
	fullRef := fmt.Sprintf("refs/notes/%s", ref)
	fetch := func() error {
		_, err := RunGitCommand(ctx, r.userRepoPath, "fetch", containerUseRemote, fullRef+":"+fullRef)
		return err
	}

	if err := fetch(); err != nil {
		if strings.Contains(err.Error(), "[rejected]") {
			if _, err := RunGitCommand(ctx, r.userRepoPath, "update-ref", "-d", fullRef); err == nil {
				return fetch()
			}
		}
		return err
	}
	return nil
}

func (r *Repository) saveState(ctx context.Context, env *environment.Environment) error {
	state, err := env.State.Marshal()
	if err != nil {
		return err
	}
	worktreePath, err := r.WorktreePath(env.ID)
	if err != nil {
		return fmt.Errorf("failed to get worktree path: %w", err)
	}

	f, err := os.CreateTemp(os.TempDir(), ".container-use-git-notes-*")
	if err != nil {
		return err
	}
	defer f.Close()
	if _, err := f.Write(state); err != nil {
		return err
	}

	_, err = RunGitCommand(ctx, worktreePath, "notes", "--ref", gitNotesStateRef, "add", "-f", "-F", f.Name())
	if err != nil {
		return err
	}
	return nil
}

func (r *Repository) loadState(ctx context.Context, worktreePath string) ([]byte, error) {
	buff, err := RunGitCommand(ctx, worktreePath, "notes", "--ref", gitNotesStateRef, "show")
	if err != nil {
		if strings.Contains(err.Error(), "no note found") {
			return nil, nil
		}
		return nil, err
	}
	return []byte(buff), nil
}

func (r *Repository) addGitNote(ctx context.Context, env *environment.Environment, note string) error {
	worktreePath, err := r.WorktreePath(env.ID)
	if err != nil {
		return fmt.Errorf("failed to get worktree path: %w", err)
	}
	_, err = RunGitCommand(ctx, worktreePath, "notes", "--ref", gitNotesLogRef, "append", "-m", note)
	if err != nil {
		return err
	}
	return r.propagateGitNotes(ctx, gitNotesLogRef)
}

func (r *Repository) currentUserBranch(ctx context.Context) (string, error) {
	return RunGitCommand(ctx, r.userRepoPath, "branch", "--show-current")
}

func (r *Repository) mergeBase(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
	currentBranch, err := r.currentUserBranch(ctx)
	if err != nil {
		return "", err
	}
	currentBranch = strings.TrimSpace(currentBranch)
	if currentBranch == "" {
		currentBranch = "HEAD"
	}
	envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
	mergeBase, err := RunGitCommand(ctx, r.userRepoPath, "merge-base", currentBranch, envGitRef)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(mergeBase), nil
}

func (r *Repository) revisionRange(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
	mergeBase, err := r.mergeBase(ctx, env)
	if err != nil {
		return "", err
	}
	envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
	return fmt.Sprintf("%s..%s", mergeBase, envGitRef), nil
}

func (r *Repository) commitWorktreeChanges(ctx context.Context, worktreePath, explanation string) error {
	status, err := RunGitCommand(ctx, worktreePath, "status", "--porcelain")
	if err != nil {
		return err
	}

	if strings.TrimSpace(status) == "" {
		return nil
	}

	if err := r.addNonBinaryFiles(ctx, worktreePath); err != nil {
		return err
	}

	_, err = RunGitCommand(ctx, worktreePath, "commit", "--allow-empty", "--allow-empty-message", "-m", explanation)
	return err
}

// AI slop below!
// this is just to keep us moving fast because big git repos get hard to work with
// and our demos like to download large dependencies.
func (r *Repository) addNonBinaryFiles(ctx context.Context, worktreePath string) error {
	statusOutput, err := RunGitCommand(ctx, worktreePath, "status", "--porcelain")
	if err != nil {
		return err
	}

	for line := range strings.SplitSeq(strings.TrimSpace(statusOutput), "\n") {
		if line == "" {
			continue
		}
		if len(line) < 3 {
			continue
		}

		indexStatus := line[0]
		workTreeStatus := line[1]
		fileName := strings.TrimSpace(line[2:])
		if fileName == "" {
			continue
		}

		if r.shouldSkipFile(fileName) {
			continue
		}

		switch {
		case indexStatus == '?' && workTreeStatus == '?':
			// ?? = untracked files or directories
			if strings.HasSuffix(fileName, "/") {
				// Untracked directory - traverse and add non-binary files
				dirName := strings.TrimSuffix(fileName, "/")
				if err := r.addFilesFromUntrackedDirectory(ctx, worktreePath, dirName); err != nil {
					return err
				}
			} else if !r.isBinaryFile(worktreePath, fileName) {
				// Untracked file - add if not binary

				_, err = RunGitCommand(ctx, worktreePath, "add", fileName)
				if err != nil {
					return err
				}
			}
		case indexStatus == 'A':
			// A = already staged, skip
			continue
		case indexStatus == 'D' || workTreeStatus == 'D':
			// D = deleted files (always stage deletion)
			_, err = RunGitCommand(ctx, worktreePath, "add", fileName)
			if err != nil {
				return err
			}
		default:
			// M, R, C and other statuses - add if not binary
			if !r.isBinaryFile(worktreePath, fileName) {
				_, err = RunGitCommand(ctx, worktreePath, "add", fileName)
				if err != nil {
					return err
				}
			}
		}
	}

	return nil
}

func (r *Repository) shouldSkipFile(fileName string) bool {
	skipExtensions := []string{
		".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz",
		".zip", ".rar", ".7z", ".gz", ".bz2", ".xz",
		".exe", ".bin", ".dmg", ".pkg", ".msi",
		".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg",
		".mp3", ".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv",
		".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
		".so", ".dylib", ".dll", ".a", ".lib",
	}

	lowerName := strings.ToLower(fileName)
	for _, ext := range skipExtensions {
		if strings.HasSuffix(lowerName, ext) {
			return true
		}
	}

	// Use cross-platform path-aware directory patterns
	skipDirNames := []string{
		"node_modules", ".git", "__pycache__", "venv", ".venv", "env", ".env",
		"target", "build", "dist", ".next",
	}

	skipFilePatterns := []string{
		".DS_Store", "*.tmp", "*.temp", "*.cache", "*.log",
	}

	// Check if the path contains any of the skip directory names
	pathParts := strings.Split(filepath.ToSlash(lowerName), "/")
	for _, part := range pathParts {
		for _, skipDir := range skipDirNames {
			if part == strings.ToLower(skipDir) {
				return true
			}
		}
	}

	// Check file patterns
	for _, pattern := range skipFilePatterns {
		if strings.Contains(lowerName, strings.ToLower(pattern)) {
			return true
		}
	}

	return false
}

func (r *Repository) IsDirty(ctx context.Context) (bool, string, error) {
	status, err := RunGitCommand(ctx, r.userRepoPath, "status", "--porcelain")
	if err != nil {
		return false, "", err
	}

	if strings.TrimSpace(status) == "" {
		return false, "", nil
	}

	return true, status, nil
}

func (r *Repository) addFilesFromUntrackedDirectory(ctx context.Context, worktreePath, dirName string) error {
	dirPath := filepath.Join(worktreePath, dirName)

	return filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relPath, err := filepath.Rel(worktreePath, path)
		if err != nil {
			return err
		}

		if info.IsDir() {
			if r.shouldSkipFile(relPath) {
				return filepath.SkipDir
			}
			return nil
		}

		if r.shouldSkipFile(relPath) {
			return nil
		}

		if !r.isBinaryFile(worktreePath, relPath) {
			_, err = RunGitCommand(ctx, worktreePath, "add", relPath)
			if err != nil {
				return err
			}
		}

		return nil
	})
}

func (r *Repository) isBinaryFile(worktreePath, fileName string) bool {
	fullPath := filepath.Join(worktreePath, fileName)

	stat, err := os.Stat(fullPath)
	if err != nil {
		return true
	}

	if stat.IsDir() {
		return false
	}

	if stat.Size() > maxFileSizeForTextCheck {
		return true
	}

	// Empty files should be treated as text files so `touch .gitkeep` and friends work correctly
	if stat.Size() == 0 {
		return false
	}

	file, err := os.Open(fullPath)
	if err != nil {
		slog.Error("Error opening file", "err", err)
		return true
	}
	defer file.Close()

	buffer := make([]byte, 8000)
	n, err := file.Read(buffer)
	if err != nil && n == 0 {
		return true
	}

	buffer = buffer[:n]
	return slices.Contains(buffer, 0)
}

func (r *Repository) normalizeForkPath(ctx context.Context, repo string) (string, error) {
	// Check if there's an origin remote
	origin, err := RunGitCommand(ctx, repo, "remote", "get-url", "origin")
	if err != nil {
		// If not -- this repository is a local one, we're going to use the filesystem path for the container-use repo
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) && exitErr.ExitCode() == 2 {
			// Exit code 2 means the remote doesn't exist
			// Create a safe path component from the absolute repo path
			safeRepoPath := createSafePathFromAbsolute(repo)
			return homedir.Expand(filepath.Join(r.getRepoPath(), safeRepoPath))
		}
		return "", err
	}

	// Otherwise, let's use the normalized origin as path
	normalizedOrigin, err := normalizeGitURL(strings.TrimSpace(origin))
	if err != nil {
		return "", err
	}
	return homedir.Expand(filepath.Join(r.getRepoPath(), normalizedOrigin))
}

// createSafePathFromAbsolute converts an absolute path to a safe relative path component
// This handles Windows drive letters and special characters that can't be used in paths
func createSafePathFromAbsolute(absPath string) string {
	// Convert to forward slashes for consistency
	normalized := filepath.ToSlash(absPath)

	// Remove leading slash if present
	normalized = strings.TrimPrefix(normalized, "/")

	// Replace colons (from Windows drive letters) with underscores
	normalized = strings.ReplaceAll(normalized, ":", "_")

	// Replace any remaining problematic characters
	normalized = strings.ReplaceAll(normalized, "<", "_")
	normalized = strings.ReplaceAll(normalized, ">", "_")
	normalized = strings.ReplaceAll(normalized, "|", "_")
	normalized = strings.ReplaceAll(normalized, "?", "_")
	normalized = strings.ReplaceAll(normalized, "*", "_")
	normalized = strings.ReplaceAll(normalized, "\"", "_")

	return normalized
}

func normalizeGitURL(endpoint string) (string, error) {
	if e, ok := normalizeSCPLike(endpoint); ok {
		return e, nil
	}

	return normalizeURL(endpoint)
}

func normalizeURL(endpoint string) (string, error) {
	u, err := url.Parse(endpoint)
	if err != nil {
		return "", err
	}

	if !u.IsAbs() {
		return "", fmt.Errorf(
			"invalid endpoint: %s", endpoint,
		)
	}

	return fmt.Sprintf("%s%s", u.Hostname(), strings.TrimSuffix(u.Path, ".git")), nil
}

func normalizeSCPLike(endpoint string) (string, bool) {
	if matchesURLScheme(endpoint) || !matchesScpLike(endpoint) {
		return "", false
	}

	_, host, _, path := findScpLikeComponents(endpoint)

	return fmt.Sprintf("%s/%s", host, strings.TrimSuffix(path, ".git")), true
}

// matchesURLScheme returns true if the given string matches a URL-like
// format scheme.
func matchesURLScheme(url string) bool {
	return urlSchemeRegExp.MatchString(url)
}

// matchesScpLike returns true if the given string matches an SCP-like
// format scheme.
func matchesScpLike(url string) bool {
	return scpLikeURLRegExp.MatchString(url)
}

// findScpLikeComponents returns the user, host, port and path of the
// given SCP-like URL.
func findScpLikeComponents(url string) (user, host, port, path string) {
	m := scpLikeURLRegExp.FindStringSubmatch(url)
	return m[1], m[2], m[3], m[4]
}

package repository

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sort"
	"strings"

	"dagger.io/dagger"
	"github.com/dagger/container-use/environment"
	petname "github.com/dustinkirkland/golang-petname"
	"github.com/mitchellh/go-homedir"
)

const (
	containerUseRemote = "container-use"
	gitNotesLogRef     = "container-use"
	gitNotesStateRef   = "container-use-state"
)

// getDefaultConfigPath returns the default configuration path for the current OS
func getDefaultConfigPath() string {
	if runtime.GOOS == "windows" {
		// On Windows, use APPDATA or LOCALAPPDATA
		if appData := os.Getenv("APPDATA"); appData != "" {
			return filepath.Join(appData, "container-use")
		}
		if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" {
			return filepath.Join(localAppData, "container-use")
		}
		// Fallback to home directory
		if home, err := homedir.Dir(); err == nil {
			return filepath.Join(home, "AppData", "Roaming", "container-use")
		}
		return "container-use" // Last resort fallback
	}
	// On Unix-like systems (Linux, macOS, etc.)
	if home, err := homedir.Dir(); err == nil {
		return filepath.Join(home, ".config", "container-use")
	}
	return "~/.config/container-use" // Fallback for compatibility
}

var (
	cuGlobalConfigPath = getDefaultConfigPath()
)

type Repository struct {
	userRepoPath string
	forkRepoPath string
	basePath     string // defaults to OS-appropriate config path if empty
}

// getRepoPath returns the path for storing repository data
func (r *Repository) getRepoPath() string {
	return filepath.Join(r.basePath, "repos")
}

// getWorktreePath returns the path for storing worktrees
func (r *Repository) getWorktreePath() string {
	return filepath.Join(r.basePath, "worktrees")
}

func Open(ctx context.Context, repo string) (*Repository, error) {
	return OpenWithBasePath(ctx, repo, cuGlobalConfigPath)
}

// OpenWithBasePath opens a repository with a custom base path for container-use data.
// This is useful for tests that need isolated environments.
func OpenWithBasePath(ctx context.Context, repo string, basePath string) (*Repository, error) {
	// Expand tilde in basePath for cross-platform compatibility
	expandedBasePath, err := homedir.Expand(basePath)
	if err != nil {
		// If expansion fails, use the original path
		expandedBasePath = basePath
	}

	output, err := RunGitCommand(ctx, repo, "rev-parse", "--show-toplevel")
	if err != nil {
		// Check for exit code 128 which means not a git repository
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) && exitErr.ExitCode() == 128 {
			return nil, errors.New("you must be in a git repository to use container-use")
		}
		return nil, err
	}
	userRepoPath := strings.TrimSpace(output)

	forkRepoPath, err := getContainerUseRemote(ctx, userRepoPath)
	if err != nil {
		if !errors.Is(err, os.ErrNotExist) {
			return nil, err
		}
		// Create a temporary repository to get the normalized fork path
		tempRepo := &Repository{basePath: expandedBasePath}
		forkRepoPath, err = tempRepo.normalizeForkPath(ctx, userRepoPath)
		if err != nil {
			return nil, err
		}
	}

	r := &Repository{
		userRepoPath: userRepoPath,
		forkRepoPath: forkRepoPath,
		basePath:     expandedBasePath,
	}

	if err := r.ensureFork(ctx); err != nil {
		return nil, fmt.Errorf("unable to fork the repository: %w", err)
	}
	if err := r.ensureUserRemote(ctx); err != nil {
		return nil, fmt.Errorf("unable to set container-use remote: %w", err)
	}

	return r, nil
}

func (r *Repository) ensureFork(ctx context.Context) error {
	// Make sure the fork repo path exists, otherwise create it
	_, err := os.Stat(r.forkRepoPath)
	if err == nil {
		return nil
	}
	if !os.IsNotExist(err) {
		return err
	}

	slog.Info("Initializing local remote", "user-repo", r.userRepoPath, "fork-repo", r.forkRepoPath)
	if err := os.MkdirAll(r.forkRepoPath, 0755); err != nil {
		return err
	}
	_, err = RunGitCommand(ctx, r.forkRepoPath, "init", "--bare")
	if err != nil {
		return err
	}
	return nil
}

func (r *Repository) ensureUserRemote(ctx context.Context) error {
	currentForkPath, err := getContainerUseRemote(ctx, r.userRepoPath)
	if err != nil {
		if !errors.Is(err, os.ErrNotExist) {
			return err
		}
		_, err := RunGitCommand(ctx, r.userRepoPath, "remote", "add", containerUseRemote, r.forkRepoPath)
		return err
	}

	if currentForkPath != r.forkRepoPath {
		_, err := RunGitCommand(ctx, r.userRepoPath, "remote", "set-url", containerUseRemote, r.forkRepoPath)
		return err
	}

	return nil
}

func (r *Repository) SourcePath() string {
	return r.userRepoPath
}

func (r *Repository) exists(ctx context.Context, id string) error {
	if _, err := RunGitCommand(ctx, r.forkRepoPath, "rev-parse", "--verify", id); err != nil {
		if strings.Contains(err.Error(), "Needed a single revision") {
			return fmt.Errorf("environment %q not found", id)
		}
		return err
	}
	return nil
}

// Create creates a new environment with the given description and explanation.
// Requires a dagger client for container operations during environment initialization.
func (r *Repository) Create(ctx context.Context, dag *dagger.Client, description, explanation string) (*environment.Environment, error) {
	id := petname.Generate(2, "-")
	worktree, err := r.initializeWorktree(ctx, id)
	if err != nil {
		return nil, err
	}

	if err := r.createInitialCommit(ctx, worktree, id, description); err != nil {
		return nil, fmt.Errorf("failed to create initial commit: %w", err)
	}

	worktreeHead, err := RunGitCommand(ctx, worktree, "rev-parse", "HEAD")
	if err != nil {
		return nil, err
	}
	worktreeHead = strings.TrimSpace(worktreeHead)

	baseSourceDir, err := dag.
		Host().
		Directory(r.forkRepoPath, dagger.HostDirectoryOpts{NoCache: true}). // bust cache for each Create call
		AsGit().
		Ref(worktreeHead).
		Tree(dagger.GitRefTreeOpts{DiscardGitDir: true}).
		Sync(ctx) // don't bust cache when loading from state
	if err != nil {
		return nil, fmt.Errorf("failed loading initial source directory: %w", err)
	}

	config := environment.DefaultConfig()
	if err := config.Load(r.userRepoPath); err != nil {
		return nil, err
	}

	env, err := environment.New(ctx, dag, id, description, config, baseSourceDir)
	if err != nil {
		return nil, err
	}

	if err := r.propagateToWorktree(ctx, env, explanation); err != nil {
		return nil, err
	}

	return env, nil
}

// Get retrieves a full Environment with dagger client embedded for container operations.
// Use this when you need to perform container operations like running commands, terminals, etc.
// For basic metadata access without container operations, use Info() instead.
func (r *Repository) Get(ctx context.Context, dag *dagger.Client, id string) (*environment.Environment, error) {
	if err := r.exists(ctx, id); err != nil {
		return nil, err
	}

	worktree, err := r.initializeWorktree(ctx, id)
	if err != nil {
		return nil, err
	}

	state, err := r.loadState(ctx, worktree)
	if err != nil {
		return nil, err
	}

	env, err := environment.Load(ctx, dag, id, state, worktree)
	if err != nil {
		return nil, err
	}

	return env, nil
}

// Info retrieves environment metadata without requiring dagger operations.
// This is more efficient than Get() when you only need access to configuration,
// state, and other metadata without performing container operations.
func (r *Repository) Info(ctx context.Context, id string) (*environment.EnvironmentInfo, error) {
	if err := r.exists(ctx, id); err != nil {
		return nil, err
	}

	worktree, err := r.initializeWorktree(ctx, id)
	if err != nil {
		return nil, err
	}

	state, err := r.loadState(ctx, worktree)
	if err != nil {
		return nil, err
	}

	envInfo, err := environment.LoadInfo(ctx, id, state, worktree)
	if err != nil {
		return nil, err
	}

	return envInfo, nil
}

// List returns information about all environments in the repository.
// Returns EnvironmentInfo slice avoiding dagger client initialization.
// Use Get() on individual environments when you need full Environment with container operations.
func (r *Repository) List(ctx context.Context) ([]*environment.EnvironmentInfo, error) {
	branches, err := RunGitCommand(ctx, r.forkRepoPath, "branch", "--format", "%(refname:short)")
	if err != nil {
		return nil, err
	}

	envs := []*environment.EnvironmentInfo{}
	for branch := range strings.SplitSeq(branches, "\n") {
		branch = strings.TrimSpace(branch)

		// FIXME(aluzzardi): This is a hack to make sure the branch is actually an environment.
		// There must be a better way to do this.
		worktree, err := r.WorktreePath(branch)
		if err != nil {
			return nil, err
		}
		state, err := r.loadState(ctx, worktree)
		if err != nil || state == nil {
			continue
		}

		envInfo, err := r.Info(ctx, branch)
		if err != nil {
			return nil, err
		}

		envs = append(envs, envInfo)
	}

	// Sort by most recently updated environments first
	sort.Slice(envs, func(i, j int) bool {
		return envs[i].State.UpdatedAt.After(envs[j].State.UpdatedAt)
	})

	return envs, nil
}

// ListDescendantEnvironments returns environments that are descendants of the given commit.
// This filters environments to only those where the provided commit is an ancestor
// of the environment's current HEAD. Environments are sorted by most recently updated first.
func (r *Repository) ListDescendantEnvironments(ctx context.Context, ancestorCommit string) ([]*environment.EnvironmentInfo, error) {
	allEnvs, err := r.List(ctx)
	if err != nil {
		return nil, err
	}

	var filteredEnvs []*environment.EnvironmentInfo
	for _, env := range allEnvs {
		if r.isDescendantOfCommit(ctx, ancestorCommit, env.ID) {
			filteredEnvs = append(filteredEnvs, env)
		}
	}

	return filteredEnvs, nil
}

// isDescendantOfCommit checks if the environment is a descendant of the given commit
// using git merge-base --is-ancestor which is the canonical way to check ancestry
func (r *Repository) isDescendantOfCommit(ctx context.Context, ancestorCommit, envID string) bool {
	envRef := fmt.Sprintf("container-use/%s", envID)

	// Use git merge-base --is-ancestor to check if ancestorCommit is an ancestor of envRef
	// This returns exit code 0 if ancestorCommit is an ancestor of envRef
	_, err := RunGitCommand(ctx, r.userRepoPath, "merge-base", "--is-ancestor", ancestorCommit, envRef)

	return err == nil
}

// Update saves the provided environment to the repository.
// Writes configuration and source code changes to the worktree and history + state to git notes.
func (r *Repository) Update(ctx context.Context, env *environment.Environment, explanation string) error {
	if err := r.propagateToWorktree(ctx, env, explanation); err != nil {
		return err
	}
	if note := env.Notes.Pop(); note != "" {
		return r.addGitNote(ctx, env, note)
	}

	return nil
}

// Delete removes an environment from the repository.
func (r *Repository) Delete(ctx context.Context, id string) error {
	if err := r.exists(ctx, id); err != nil {
		return err
	}

	if err := r.deleteWorktree(id); err != nil {
		return err
	}
	if err := r.deleteLocalRemoteBranch(id); err != nil {
		return err
	}
	return nil
}

// Checkout changes the user's current branch to that of the identified environment.
// It attempts to get the most recent commit from the environment without discarding any user changes.
func (r *Repository) Checkout(ctx context.Context, id, branch string) (string, error) {
	if err := r.exists(ctx, id); err != nil {
		return "", err
	}

	if branch == "" {
		branch = "cu-" + id
	}

	// set up remote tracking branch if it's not already there
	_, err := RunGitCommand(ctx, r.userRepoPath, "show-ref", "--verify", "--quiet", fmt.Sprintf("refs/heads/%s", branch))
	localBranchExists := err == nil
	if !localBranchExists {
		_, err = RunGitCommand(ctx, r.userRepoPath, "branch", "--track", branch, fmt.Sprintf("%s/%s", containerUseRemote, id))
		if err != nil {
			return "", err
		}
	}

	_, err = RunGitCommand(ctx, r.userRepoPath, "checkout", branch)
	if err != nil {
		return "", err
	}

	if localBranchExists {
		remoteRef := fmt.Sprintf("%s/%s", containerUseRemote, id)

		counts, err := RunGitCommand(ctx, r.userRepoPath, "rev-list", "--left-right", "--count", fmt.Sprintf("HEAD...%s", remoteRef))
		if err != nil {
			return branch, err
		}

		parts := strings.Split(strings.TrimSpace(counts), "\t")
		if len(parts) != 2 {
			return branch, fmt.Errorf("unexpected git rev-list output: %s", counts)
		}
		aheadCount, behindCount := parts[0], parts[1]

		if behindCount != "0" && aheadCount == "0" {
			_, err = RunGitCommand(ctx, r.userRepoPath, "merge", "--ff-only", remoteRef)
			if err != nil {
				return branch, err
			}
		} else if behindCount != "0" {
			return branch, fmt.Errorf("switched to %s, but %s is %s ahead and container-use/ remote has %s additional commits", branch, branch, aheadCount, behindCount)
		}
	}

	return branch, err
}

func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer) error {
	envInfo, err := r.Info(ctx, id)
	if err != nil {
		return err
	}

	logArgs := []string{
		"log",
		fmt.Sprintf("--notes=%s", gitNotesLogRef),
	}

	if patch {
		logArgs = append(logArgs, "--patch")
	} else {
		logArgs = append(logArgs, "--format=%C(yellow)%h%Creset  %s %Cgreen(%cr)%Creset %+N")
	}

	revisionRange, err := r.revisionRange(ctx, envInfo)
	if err != nil {
		return err
	}

	logArgs = append(logArgs, revisionRange)

	return RunInteractiveGitCommand(ctx, r.userRepoPath, w, logArgs...)
}

func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
	envInfo, err := r.Info(ctx, id)
	if err != nil {
		return err
	}

	diffArgs := []string{
		"diff",
	}

	revisionRange, err := r.revisionRange(ctx, envInfo)
	if err != nil {
		return err
	}

	diffArgs = append(diffArgs, revisionRange)

	return RunInteractiveGitCommand(ctx, r.userRepoPath, w, diffArgs...)
}

func (r *Repository) Merge(ctx context.Context, id string, w io.Writer) error {
	envInfo, err := r.Info(ctx, id)
	if err != nil {
		return err
	}

	return RunInteractiveGitCommand(ctx, r.userRepoPath, w, "merge", "--no-ff", "--autostash", "-m", "Merge environment "+envInfo.ID, "--", "container-use/"+envInfo.ID)
}

func (r *Repository) Apply(ctx context.Context, id string, w io.Writer) error {
	envInfo, err := r.Info(ctx, id)
	if err != nil {
		return err
	}

	return RunInteractiveGitCommand(ctx, r.userRepoPath, w, "merge", "--autostash", "--squash", "--", "container-use/"+envInfo.ID)
}

package agent

import (
	"fmt"
	"runtime"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// Agent represents an agent configuration option
type Agent struct {
	Key         string
	Name        string
	Description string
}

// Available agents
var agents = []Agent{
	{
		Key:         "claude",
		Name:        "Claude Code",
		Description: "Anthropic's Claude Code",
	},
	{
		Key:         "goose",
		Name:        "Goose",
		Description: "an open source, extensible AI agent that goes beyond code suggestions",
	},
	{
		Key:         "cursor",
		Name:        "Cursor",
		Description: "AI-powered code editor",
	},
	{
		Key:         "codex",
		Name:        "OpenAI Codex",
		Description: "OpenAI's lightweight coding agent that runs in your terminal (Linux/macOS/WSL)",
	},
	{
		Key:         "amazonq",
		Name:        "Amazon Q Developer",
		Description: "Amazon's agentic chat experience in your terminal (Linux/macOS/WSL)",
	},
}

// getSupportedAgents returns agents that are supported on the current platform
func getSupportedAgents() []Agent {
	if runtime.GOOS == "windows" {
		// Filter out Windows-incompatible agents
		var supportedAgents []Agent
		for _, agent := range agents {
			if agent.Key != "codex" && agent.Key != "amazonq" {
				supportedAgents = append(supportedAgents, agent)
			}
		}
		return supportedAgents
	}
	return agents
}

// AgentSelectorModel represents the bubbletea model for agent selection
type AgentSelectorModel struct {
	cursor   int
	selected string
	quit     bool
}

// InitialModel creates the initial model for agent selection
func InitialModel() AgentSelectorModel {
	return AgentSelectorModel{}
}

// Init initializes the model
func (m AgentSelectorModel) Init() tea.Cmd {
	return nil
}

// Update handles incoming messages and updates the model
func (m AgentSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	supportedAgents := getSupportedAgents()
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q", "esc":
			m.quit = true
			return m, tea.Quit
		case "up", "k":
			if m.cursor > 0 {
				m.cursor--
			}
		case "down", "j":
			if m.cursor < len(supportedAgents)-1 {
				m.cursor++
			}
		case "enter", " ":
			m.selected = supportedAgents[m.cursor].Key
			m.quit = true
			return m, tea.Quit
		}
	default:
		return m, nil
	}
	return m, nil
}

// View renders the interface
func (m AgentSelectorModel) View() string {
	if m.quit {
		return ""
	}

	// Styles
	titleStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("#FAFAFA")).
		Background(lipgloss.Color("#7D56F4")).
		Padding(0, 1).
		Margin(1, 0).
		Bold(true)

	headerStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("#7D56F4")).
		Bold(true).
		Margin(1, 0, 0, 0)

	selectedStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("#FAFAFA")).
		Background(lipgloss.Color("#F25D94")).
		Padding(0, 1).
		Bold(true)

	normalStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("#04B575")).
		Padding(0, 1)

	descriptionStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("#626262")).
		Padding(0, 1, 0, 3).
		Italic(true)

	footerStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("#626262")).
		Margin(1, 0, 0, 0)

	// Build the view
	var s strings.Builder

	// Title
	s.WriteString(titleStyle.Render("🛠  Container Use Configuration"))
	s.WriteString("\n")

	// Header
	s.WriteString(headerStyle.Render("Select an agent to configure:"))
	s.WriteString("\n\n")

	// Show WSL note for Windows users
	if runtime.GOOS == "windows" {
		wslNoteStyle := lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFA500")).
			Padding(0, 1).
			Italic(true)
		s.WriteString(wslNoteStyle.Render("Note: OpenAI Codex and Amazon Q Developer are available in WSL"))
		s.WriteString("\n\n")
	}

	// Agent list TODO: filter or sort agents based on if they are installed (ConfigurableAgent.isInstalled())
	supportedAgents := getSupportedAgents()
	for i, agent := range supportedAgents {
		cursor := "  " // not selected
		if m.cursor == i {
			cursor = "▶ " // selected
		}

		agentLine := fmt.Sprintf("%s%s", cursor, agent.Name)
		if m.cursor == i {
			s.WriteString(selectedStyle.Render(agentLine))
		} else {
			s.WriteString(normalStyle.Render(agentLine))
		}

		s.WriteString("\n")

		// Show description for selected item
		if m.cursor == i {
			s.WriteString(descriptionStyle.Render(agent.Description))
			s.WriteString("\n")
		}
	}

	// Footer
	s.WriteString("\n")
	s.WriteString(footerStyle.Render("Use ↑/↓ or j/k to navigate • Enter/Space to select • q/Ctrl+C/Esc to quit"))

	return s.String()
}

// RunAgentSelector runs the interactive agent selector and returns the selected agent key
func RunAgentSelector() (string, error) {
	p := tea.NewProgram(InitialModel())
	finalModel, err := p.Run()
	if err != nil {
		return "", fmt.Errorf("error running agent selector: %w", err)
	}

	m := finalModel.(AgentSelectorModel)
	if m.selected == "" {
		return "", fmt.Errorf("no agent selected")
	}

	return m.selected, nil
}

package environment

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

const (
	defaultImage    = "ubuntu:24.04"
	alpineImage     = "alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c"
	configDir       = ".container-use"
	environmentFile = "environment.json"
)

func DefaultConfig() *EnvironmentConfig {
	return &EnvironmentConfig{
		BaseImage: defaultImage,
		Workdir:   "/workdir",
	}
}

type EnvironmentConfig struct {
	Workdir         string         `json:"workdir,omitempty"`
	BaseImage       string         `json:"base_image,omitempty"`
	SetupCommands   []string       `json:"setup_commands,omitempty"`
	InstallCommands []string       `json:"install_commands,omitempty"`
	Env             KVList         `json:"env,omitempty"`
	Secrets         KVList         `json:"secrets,omitempty"`
	Services        ServiceConfigs `json:"services,omitempty"`
}

type ServiceConfig struct {
	Name         string   `json:"name,omitempty"`
	Image        string   `json:"image,omitempty"`
	Command      string   `json:"command,omitempty"`
	ExposedPorts []int    `json:"exposed_ports,omitempty"`
	Env          []string `json:"env,omitempty"`
}

type ServiceConfigs []*ServiceConfig

func (sc ServiceConfigs) Get(name string) *ServiceConfig {
	for _, cfg := range sc {
		if cfg.Name == name {
			return cfg
		}
	}
	return nil
}

// KVList represents a list of key-value pairs in the format KEY=VALUE
type KVList []string

// parseKeyValue parses a key-value string in the format "KEY=VALUE"
func (kv KVList) parseKeyValue(raw string) (key, value string) {
	k, v, _ := strings.Cut(raw, "=")
	return k, v
}

// Set adds or updates a key-value pair
func (kv *KVList) Set(key, value string) {
	// Remove existing key if it exists
	kv.Unset(key)
	// Add new key-value pair
	*kv = append(*kv, fmt.Sprintf("%s=%s", key, value))
}

// Unset removes a key-value pair by key and returns true if the key was found
func (kv *KVList) Unset(key string) bool {
	found := false
	newList := make([]string, 0, len(*kv))
	for _, item := range *kv {
		if itemKey, _ := kv.parseKeyValue(item); itemKey != key {
			newList = append(newList, item)
		} else {
			found = true
		}
	}
	*kv = newList
	return found
}

// Clear removes all key-value pairs
func (kv *KVList) Clear() {
	*kv = []string{}
}

// Keys returns all keys in the list
func (kv KVList) Keys() []string {
	keys := make([]string, 0, len(kv))
	for _, item := range kv {
		if key, _ := kv.parseKeyValue(item); key != "" {
			keys = append(keys, key)
		}
	}
	return keys
}

// Get returns the value for a given key, or empty string if not found
func (kv KVList) Get(key string) string {
	for _, item := range kv {
		if itemKey, value := kv.parseKeyValue(item); itemKey == key {
			return value
		}
	}
	return ""
}

func (config *EnvironmentConfig) Copy() *EnvironmentConfig {
	copy := *config
	copy.Services = make(ServiceConfigs, len(config.Services))
	for i, svc := range config.Services {
		svcCopy := *svc
		copy.Services[i] = &svcCopy
	}
	return &copy
}

func (config *EnvironmentConfig) Save(baseDir string) error {
	configPath := filepath.Join(baseDir, configDir)
	if err := os.MkdirAll(configPath, 0755); err != nil {
		return err
	}

	// Use a custom encoder to prevent HTML escaping of characters like &, <, >
	var buf bytes.Buffer
	encoder := json.NewEncoder(&buf)
	encoder.SetIndent("", "  ")
	encoder.SetEscapeHTML(false) // This prevents & from being escaped as \u0026

	if err := encoder.Encode(config); err != nil {
		return err
	}

	if err := os.WriteFile(filepath.Join(configPath, environmentFile), buf.Bytes(), 0644); err != nil {
		return err
	}

	return nil
}

func (config *EnvironmentConfig) Load(baseDir string) error {
	configPath := filepath.Join(baseDir, configDir)

	data, err := os.ReadFile(filepath.Join(configPath, environmentFile))
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	if err == nil {
		if err := json.Unmarshal(data, config); err != nil {
			return err
		}
	}

	return nil
}

package environment

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"sync"
	"time"

	"dagger.io/dagger"
)

// EnvironmentInfo contains basic metadata about an environment
// without requiring dagger operations
type EnvironmentInfo struct {
	State *State `json:"state,omitempty"`

	ID string `json:"id,omitempty"`
}

type Environment struct {
	*EnvironmentInfo

	dag *dagger.Client

	Services []*Service
	Notes    Notes

	mu sync.RWMutex
}

func New(ctx context.Context, dag *dagger.Client, id, title string, config *EnvironmentConfig, initialSourceDir *dagger.Directory) (*Environment, error) {
	env := &Environment{
		EnvironmentInfo: &EnvironmentInfo{
			ID: id,
			State: &State{
				Config:    config,
				Title:     title,
				CreatedAt: time.Now(),
				UpdatedAt: time.Now(),
			},
		},
		dag: dag,
	}

	container, err := env.buildBase(ctx, initialSourceDir)
	if err != nil {
		return nil, err
	}

	slog.Info("Creating environment", "id", env.ID, "workdir", env.State.Config.Workdir)

	if err := env.apply(ctx, container); err != nil {
		return nil, err
	}

	return env, nil
}

func (env *Environment) Workdir() *dagger.Directory {
	return env.container().Directory(env.State.Config.Workdir)
}

func (env *Environment) container() *dagger.Container {
	env.mu.RLock()
	defer env.mu.RUnlock()

	return env.dag.LoadContainerFromID(dagger.ContainerID(env.State.Container))
}

func Load(ctx context.Context, dag *dagger.Client, id string, state []byte, worktree string) (*Environment, error) {
	envInfo, err := LoadInfo(ctx, id, state, worktree)
	if err != nil {
		return nil, err
	}
	env := &Environment{
		EnvironmentInfo: envInfo,
		dag:             dag,
		// Services: ?
	}

	return env, nil
}

// LoadInfo loads basic environment metadata without requiring dagger operations.
// This is useful for operations that only need access to configuration and state
// information without the overhead of initializing container operations.
func LoadInfo(ctx context.Context, id string, state []byte, worktree string) (*EnvironmentInfo, error) {
	envInfo := &EnvironmentInfo{
		ID:    id,
		State: &State{},
	}

	if err := envInfo.State.Unmarshal(state); err != nil {
		return nil, err
	}

	// Backward compatibility: if there's no config in the state, load it from the worktree
	if envInfo.State.Config == nil {
		config := DefaultConfig()
		if err := config.Load(worktree); err != nil {
			return nil, err
		}
		envInfo.State.Config = config
	}

	return envInfo, nil
}

func (env *Environment) apply(ctx context.Context, newState *dagger.Container) error {
	// TODO(braa): is this sync redundant with newState.ID?
	if _, err := newState.Sync(ctx); err != nil {
		return err
	}

	containerID, err := newState.ID(ctx)
	if err != nil {
		return err
	}

	env.mu.Lock()
	defer env.mu.Unlock()
	env.State.UpdatedAt = time.Now()
	env.State.Container = string(containerID)

	return nil
}

func containerWithEnvAndSecrets(dag *dagger.Client, container *dagger.Container, envs, secrets []string) (*dagger.Container, error) {
	for _, env := range envs {
		k, v, found := strings.Cut(env, "=")
		if !found {
			return nil, fmt.Errorf("invalid env variable: %s", env)
		}
		if !found {
			return nil, fmt.Errorf("invalid environment variable: %s", env)
		}
		container = container.WithEnvVariable(k, v)
	}

	for _, secret := range secrets {
		k, v, found := strings.Cut(secret, "=")
		if !found {
			return nil, fmt.Errorf("invalid secret: %s", secret)
		}
		container = container.WithSecretVariable(k, dag.Secret(v))
	}

	return container, nil
}

func (env *Environment) buildBase(ctx context.Context, baseSourceDir *dagger.Directory) (*dagger.Container, error) {
	container := env.dag.
		Container().
		From(env.State.Config.BaseImage).
		WithWorkdir(env.State.Config.Workdir)

	container, err := containerWithEnvAndSecrets(env.dag, container, env.State.Config.Env, env.State.Config.Secrets)
	if err != nil {
		return nil, err
	}

	runCommands := func(commands []string) error {
		for _, command := range commands {
			var err error

			container = container.WithExec([]string{"sh", "-c", command})

			exitCode, err := container.ExitCode(ctx)
			if err != nil {
				var exitErr *dagger.ExecError
				if errors.As(err, &exitErr) {
					env.Notes.AddCommand(command, exitErr.ExitCode, exitErr.Stdout, exitErr.Stderr)
					return fmt.Errorf("exit code %d.\nstdout: %s\nstderr: %s\n%w", exitErr.ExitCode, exitErr.Stdout, exitErr.Stderr, err)
				}

				return err
			}
			stdout, err := container.Stdout(ctx)
			if err != nil {
				return fmt.Errorf("failed to get stdout: %w", err)
			}

			stderr, err := container.Stderr(ctx)
			if err != nil {
				return fmt.Errorf("failed to get stderr: %w", err)
			}

			env.Notes.AddCommand(command, exitCode, stdout, stderr)
		}

		return nil
	}

	// Run setup commands without the source directory for caching purposes
	if err := runCommands(env.State.Config.SetupCommands); err != nil {
		return nil, fmt.Errorf("setup command failed: %w", err)
	}

	env.Services, err = env.startServices(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to start services: %w", err)
	}
	for _, service := range env.Services {
		container = container.WithServiceBinding(service.Config.Name, service.svc)
	}

	container = container.WithDirectory(".", baseSourceDir)

	// Run the install commands after the source directory is set up
	if err := runCommands(env.State.Config.InstallCommands); err != nil {
		return nil, fmt.Errorf("install command failed: %w", err)
	}

	return container, nil
}

func (env *Environment) UpdateConfig(ctx context.Context, newConfig *EnvironmentConfig) error {
	env.State.Config = newConfig

	// Re-build the base image with the new config
	container, err := env.buildBase(ctx, env.Workdir())
	if err != nil {
		return err
	}

	if err := env.apply(ctx, container); err != nil {
		return err
	}

	return nil
}

func (env *Environment) Run(ctx context.Context, command, shell string, useEntrypoint bool) (string, error) {
	args := []string{}
	if command != "" {
		args = []string{shell, "-c", command}
	}
	newState := env.container().WithExec(args, dagger.ContainerWithExecOpts{
		UseEntrypoint:                 useEntrypoint,
		Expect:                        dagger.ReturnTypeAny, // Don't treat non-zero exit as error
		ExperimentalPrivilegedNesting: true,
	})

	exitCode, err := newState.ExitCode(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to get exit code: %w", err)
	}

	stdout, err := newState.Stdout(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to get stdout: %w", err)
	}

	stderr, err := newState.Stderr(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to get stderr: %w", err)
	}

	// Log the command execution with all details
	env.Notes.AddCommand(command, exitCode, stdout, stderr)

	// Always apply the container state (preserving changes even on non-zero exit)
	if err := env.apply(ctx, newState); err != nil {
		return stdout, fmt.Errorf("failed to apply container state: %w", err)
	}

	// Return combined output (stdout + stderr if there was stderr)
	combinedOutput := stdout
	if stderr != "" {
		if stdout != "" {
			combinedOutput += "\n"
		}
		combinedOutput += "stderr: " + stderr
	}
	return combinedOutput, nil
}

func (env *Environment) RunBackground(ctx context.Context, command, shell string, ports []int, useEntrypoint bool) (EndpointMappings, error) {
	args := []string{}
	if command != "" {
		args = []string{shell, "-c", command}
	}
	displayCommand := command + " &"
	serviceState := env.container()

	// Expose ports
	for _, port := range ports {
		serviceState = serviceState.WithExposedPort(port, dagger.ContainerWithExposedPortOpts{
			Protocol:    dagger.NetworkProtocolTcp,
			Description: fmt.Sprintf("Port %d", port),
		})
	}

	// Start the service
	startCtx, cancel := context.WithTimeout(ctx, serviceStartTimeout)
	defer cancel()
	svc, err := serviceState.AsService(dagger.ContainerAsServiceOpts{
		Args:          args,
		UseEntrypoint: useEntrypoint,
	}).Start(startCtx)
	if err != nil {
		var exitErr *dagger.ExecError
		if errors.As(err, &exitErr) {
			env.Notes.AddCommand(displayCommand, exitErr.ExitCode, exitErr.Stdout, exitErr.Stderr)
			return nil, fmt.Errorf("command failed with exit code %d.\nstdout: %s\nstderr: %s", exitErr.ExitCode, exitErr.Stdout, exitErr.Stderr)
		}
		if errors.Is(err, context.DeadlineExceeded) {
			err = fmt.Errorf("service failed to start within %s timeout", serviceStartTimeout)
			env.Notes.AddCommand(displayCommand, 137, "", err.Error())
			return nil, err
		}
		return nil, err
	}

	env.Notes.AddCommand(displayCommand, 0, "", "")

	endpoints := EndpointMappings{}
	for _, port := range ports {
		endpoint := &EndpointMapping{}
		endpoints[port] = endpoint

		// Expose port on the host
		tunnel, err := env.dag.Host().Tunnel(svc, dagger.HostTunnelOpts{
			Ports: []dagger.PortForward{
				{
					Backend:  port,
					Protocol: dagger.NetworkProtocolTcp,
				},
			},
		}).Start(ctx)
		if err != nil {
			return nil, err
		}

		externalEndpoint, err := tunnel.Endpoint(ctx, dagger.ServiceEndpointOpts{
			Scheme: "tcp",
		})
		if err != nil {
			return nil, err
		}
		endpoint.HostExternal = externalEndpoint

		internalEndpoint, err := svc.Endpoint(ctx, dagger.ServiceEndpointOpts{
			Port:   port,
			Scheme: "tcp",
		})
		if err != nil {
			return nil, err
		}
		endpoint.EnvironmentInternal = internalEndpoint
	}

	return endpoints, nil
}

func (env *Environment) Terminal(ctx context.Context) error {
	container := env.container()
	var cmd []string
	var sourceRC string
	if shells, err := container.File("/etc/shells").Contents(ctx); err == nil {
		for shell := range strings.Lines(shells) {
			if shell[0] == '#' {
				continue
			}
			shell = strings.TrimRight(shell, "\n")
			if strings.HasSuffix(shell, "/bash") {
				sourceRC = fmt.Sprintf("[ -f ~/.bashrc ] && . ~/.bashrc; %q --version | head -4; ", shell)
				cmd = []string{shell, "--rcfile", "/cu/rc.sh", "-i"}
				break
			}
		}
	}
	// Try to show the same pretty PS1 as for the default /bin/sh terminal in dagger
	container = container.WithNewFile("/cu/rc.sh", sourceRC+`export PS1="\033[33mcu\033[0m \033[02m\$(pwd | sed \"s|^\$HOME|~|\")\033[0m \$ "`+"\n")
	if cmd == nil {
		// If bash not available, assume POSIX shell
		container = container.WithEnvVariable("ENV", "/cu/rc.sh")
		cmd = []string{"sh"}
	}
	if _, err := container.Terminal(dagger.ContainerTerminalOpts{
		ExperimentalPrivilegedNesting: true,
		Cmd:                           cmd,
	}).Sync(ctx); err != nil {
		return err
	}
	return nil
}

func (env *Environment) Checkpoint(ctx context.Context, target string) (string, error) {
	return env.container().Publish(ctx, target)
}

package main

import (
	"encoding/json"
	"fmt"
	"os"
	"text/tabwriter"

	"github.com/dagger/container-use/cmd/container-use/agent"
	"github.com/dagger/container-use/environment"
	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

// Helper function for read-only config operations
func withConfig(cmd *cobra.Command, fn func(*environment.EnvironmentConfig) error) error {
	ctx := cmd.Context()
	repo, err := repository.Open(ctx, ".")
	if err != nil {
		return fmt.Errorf("failed to open repository: %w", err)
	}

	config := environment.DefaultConfig()
	if err := config.Load(repo.SourcePath()); err != nil {
		return fmt.Errorf("failed to load configuration: %w", err)
	}

	return fn(config)
}

// Helper function for config update operations
func updateConfig(cmd *cobra.Command, fn func(*environment.EnvironmentConfig) error) error {
	ctx := cmd.Context()
	repo, err := repository.Open(ctx, ".")
	if err != nil {
		return fmt.Errorf("failed to open repository: %w", err)
	}

	config := environment.DefaultConfig()
	if err := config.Load(repo.SourcePath()); err != nil {
		return fmt.Errorf("failed to load configuration: %w", err)
	}

	if err := fn(config); err != nil {
		return err
	}

	if err := config.Save(repo.SourcePath()); err != nil {
		return fmt.Errorf("failed to save configuration: %w", err)
	}

	return nil
}

var configCmd = &cobra.Command{
	Use:   "config",
	Short: "Manage environment configuration",
	Long: `Configure the development environment settings such as base image and setup commands.
These settings are stored in .container-use/environment.json and apply to all new environments.`,
}

func init() {
	configShowCmd.Flags().Bool("json", false, "Dump the configuration in JSON")
}

var configShowCmd = &cobra.Command{
	Use:   "show [<env>]",
	Short: "Show environment configuration",
	Long: `Display environment configuration including base image and setup commands.
Without an environment argument, shows the default configuration used for new environments.
With an environment argument, shows the configuration for that specific environment.`,
	Example: `# Show the default environment configuration
container-use config show

# Show the configuration for a specific environment
container-use config show my-env
`,
	Args:              cobra.MaximumNArgs(1),
	ValidArgsFunction: suggestEnvironments,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := cmd.Context()

		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return fmt.Errorf("failed to open repository: %w", err)
		}

		var config *environment.EnvironmentConfig

		// If no environment is specified, use the default configuration
		if len(args) == 0 {
			config = environment.DefaultConfig()
			if err := config.Load(repo.SourcePath()); err != nil {
				return fmt.Errorf("failed to load configuration: %w", err)
			}
		} else {
			envID := args[0]
			env, err := repo.Info(ctx, envID)
			if err != nil {
				return err
			}
			config = env.State.Config
		}

		if ok, _ := cmd.Flags().GetBool("json"); ok {
			enc := json.NewEncoder(os.Stdout)
			enc.SetIndent("", "  ")
			return enc.Encode(config)
		}

		tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		defer tw.Flush()

		fmt.Fprintf(tw, "Base Image:\t%s\n", config.BaseImage)
		fmt.Fprintf(tw, "Workdir:\t%s\n", config.Workdir)

		if len(config.SetupCommands) > 0 {
			fmt.Fprintf(tw, "Setup Commands:\t\n")
			for i, cmd := range config.SetupCommands {
				fmt.Fprintf(tw, "  %d.\t%s\n", i+1, cmd)
			}
		} else {
			fmt.Fprintf(tw, "Setup Commands:\t(none)\n")
		}

		if len(config.InstallCommands) > 0 {
			fmt.Fprintf(tw, "Install Commands:\t\n")
			for i, cmd := range config.InstallCommands {
				fmt.Fprintf(tw, "  %d.\t%s\n", i+1, cmd)
			}
		} else {
			fmt.Fprintf(tw, "Install Commands:\t(none)\n")
		}

		envKeys := config.Env.Keys()
		if len(envKeys) > 0 {
			fmt.Fprintf(tw, "Environment Variables:\t\n")
			for i, key := range envKeys {
				value := config.Env.Get(key)
				fmt.Fprintf(tw, "  %d.\t%s=%s\n", i+1, key, value)
			}
		} else {
			fmt.Fprintf(tw, "Environment Variables:\t(none)\n")
		}

		secretKeys := config.Secrets.Keys()
		if len(secretKeys) > 0 {
			fmt.Fprintf(tw, "Secrets:\t\n")
			for i, key := range secretKeys {
				value := config.Secrets.Get(key)
				fmt.Fprintf(tw, "  %d.\t%s=%s\n", i+1, key, value)
			}
		} else {
			fmt.Fprintf(tw, "Secrets:\t(none)\n")
		}

		return nil
	},
}

var configImportCmd = &cobra.Command{
	Use:   "import <env>",
	Short: "Import configuration from an environment",
	Long: `Import configuration from an existing environment and set it as the default.
This copies the environment's base image, setup commands, environment variables,
and secrets to be used as defaults for new environments.`,
	Example: `# Import configuration from an environment
container-use config import my-env

# View the configuration before importing
container-use config show my-env
container-use config import my-env`,
	Args:              cobra.ExactArgs(1),
	ValidArgsFunction: suggestEnvironments,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := cmd.Context()

		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return fmt.Errorf("failed to open repository: %w", err)
		}

		envID := args[0]
		env, err := repo.Info(ctx, envID)
		if err != nil {
			return err
		}
		if err := env.State.Config.Save(repo.SourcePath()); err != nil {
			return fmt.Errorf("failed to save configuration: %w", err)
		}

		fmt.Printf("Configuration imported from environment '%s'\n", envID)
		return nil
	},
}

// Base image object commands
var configBaseImageCmd = &cobra.Command{
	Use:   "base-image",
	Short: "Manage base container image",
	Long:  `Manage the base container image for new environments.`,
}

var configBaseImageSetCmd = &cobra.Command{
	Use:   "set <image>",
	Short: "Set the base container image",
	Long:  `Set the base container image for new environments (e.g., python:3.11, node:18, ubuntu:22.04).`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		baseImage := args[0]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.BaseImage = baseImage
			fmt.Printf("Base image set to: %s\n", baseImage)
			return nil
		})
	},
}

var configBaseImageGetCmd = &cobra.Command{
	Use:   "get",
	Short: "Get the current base container image",
	Long:  `Display the current base container image.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return withConfig(cmd, func(config *environment.EnvironmentConfig) error {
			fmt.Println(config.BaseImage)
			return nil
		})
	},
}

var configBaseImageResetCmd = &cobra.Command{
	Use:   "reset",
	Short: "Reset base image to default",
	Long:  `Reset the base container image to the default (ubuntu:24.04).`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			defaultConfig := environment.DefaultConfig()
			config.BaseImage = defaultConfig.BaseImage
			fmt.Printf("Base image reset to default: %s\n", defaultConfig.BaseImage)
			return nil
		})
	},
}

// Setup command object commands
var configSetupCommandCmd = &cobra.Command{
	Use:   "setup-command",
	Short: "Manage setup commands",
	Long:  `Manage setup commands that are run when creating environments.`,
}

var configSetupCommandAddCmd = &cobra.Command{
	Use:   "add <command>",
	Short: "Add a setup command",
	Long:  `Add a command to be run when creating new environments (e.g., "apt update && apt install -y python3").`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		command := args[0]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.SetupCommands = append(config.SetupCommands, command)
			fmt.Printf("Setup command added: %s\n", command)
			return nil
		})
	},
}

var configSetupCommandRemoveCmd = &cobra.Command{
	Use:   "remove <command>",
	Short: "Remove a setup command",
	Long:  `Remove a setup command from the environment configuration.`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		command := args[0]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			found := false
			newCommands := make([]string, 0, len(config.SetupCommands))
			for _, existing := range config.SetupCommands {
				if existing != command {
					newCommands = append(newCommands, existing)
				} else {
					found = true
				}
			}

			if !found {
				return fmt.Errorf("setup command not found: %s", command)
			}

			config.SetupCommands = newCommands
			fmt.Printf("Setup command removed: %s\n", command)
			return nil
		})
	},
}

var configSetupCommandListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all setup commands",
	Long:  `List all setup commands that will be run when creating environments.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return withConfig(cmd, func(config *environment.EnvironmentConfig) error {
			if len(config.SetupCommands) == 0 {
				fmt.Println("No setup commands configured")
				return nil
			}

			for i, command := range config.SetupCommands {
				fmt.Printf("%d. %s\n", i+1, command)
			}
			return nil
		})
	},
}

var configSetupCommandClearCmd = &cobra.Command{
	Use:   "clear",
	Short: "Clear all setup commands",
	Long:  `Remove all setup commands from the environment configuration.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.SetupCommands = []string{}
			fmt.Println("All setup commands cleared")
			return nil
		})
	},
}

// Install command object commands
var configInstallCommandCmd = &cobra.Command{
	Use:   "install-command",
	Short: "Manage install commands",
	Long:  `Manage install commands that are run after copying code to environments.`,
}

var configInstallCommandAddCmd = &cobra.Command{
	Use:   "add <command>",
	Short: "Add an install command",
	Long:  `Add a command to be run after copying code to new environments (e.g., "go mod download").`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		command := args[0]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.InstallCommands = append(config.InstallCommands, command)
			fmt.Printf("Install command added: %s\n", command)
			return nil
		})
	},
}

var configInstallCommandRemoveCmd = &cobra.Command{
	Use:   "remove <command>",
	Short: "Remove an install command",
	Long:  `Remove an install command from the environment configuration.`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		command := args[0]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			found := false
			newCommands := make([]string, 0, len(config.InstallCommands))
			for _, existing := range config.InstallCommands {
				if existing != command {
					newCommands = append(newCommands, existing)
				} else {
					found = true
				}
			}

			if !found {
				return fmt.Errorf("install command not found: %s", command)
			}

			config.InstallCommands = newCommands
			fmt.Printf("Install command removed: %s\n", command)
			return nil
		})
	},
}

var configInstallCommandListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all install commands",
	Long:  `List all install commands that will be run after copying code to environments.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return withConfig(cmd, func(config *environment.EnvironmentConfig) error {
			if len(config.InstallCommands) == 0 {
				fmt.Println("No install commands configured")
				return nil
			}

			for i, command := range config.InstallCommands {
				fmt.Printf("%d. %s\n", i+1, command)
			}
			return nil
		})
	},
}

var configInstallCommandClearCmd = &cobra.Command{
	Use:   "clear",
	Short: "Clear all install commands",
	Long:  `Remove all install commands from the environment configuration.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.InstallCommands = []string{}
			fmt.Println("All install commands cleared")
			return nil
		})
	},
}

// Environment variable object commands
var configEnvCmd = &cobra.Command{
	Use:   "env",
	Short: "Manage environment variables",
	Long:  `Manage environment variables that are set when creating environments.`,
}

var configEnvSetCmd = &cobra.Command{
	Use:   "set <key> <value>",
	Short: "Set an environment variable",
	Long:  `Set an environment variable to be used when creating new environments (e.g., "PATH" "/usr/local/bin:$PATH").`,
	Args:  cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		key := args[0]
		value := args[1]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.Env.Set(key, value)
			fmt.Printf("Environment variable set: %s=%s\n", key, value)
			return nil
		})
	},
}

var configEnvUnsetCmd = &cobra.Command{
	Use:   "unset <key>",
	Short: "Unset an environment variable",
	Long:  `Unset an environment variable from the environment configuration.`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		key := args[0]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			if !config.Env.Unset(key) {
				return fmt.Errorf("environment variable not found: %s", key)
			}
			fmt.Printf("Environment variable unset: %s\n", key)
			return nil
		})
	},
}

var configEnvListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all environment variables",
	Long:  `List all environment variables that will be set when creating environments.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return withConfig(cmd, func(config *environment.EnvironmentConfig) error {
			keys := config.Env.Keys()
			if len(keys) == 0 {
				fmt.Println("No environment variables configured")
				return nil
			}

			for i, key := range keys {
				value := config.Env.Get(key)
				fmt.Printf("%d. %s=%s\n", i+1, key, value)
			}
			return nil
		})
	},
}

var configEnvClearCmd = &cobra.Command{
	Use:   "clear",
	Short: "Clear all environment variables",
	Long:  `Remove all environment variables from the environment configuration.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.Env.Clear()
			fmt.Println("All environment variables cleared")
			return nil
		})
	},
}

// Secret object commands
var configSecretCmd = &cobra.Command{
	Use:   "secret",
	Short: "Manage secrets",
	Long:  `Manage secrets that are set when creating environments.`,
}

var configSecretSetCmd = &cobra.Command{
	Use:   "set <key> <value>",
	Short: "Set a secret",
	Long:  `Set a secret to be used when creating new environments (e.g., "API_KEY" "op://vault/item/field").`,
	Args:  cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		key := args[0]
		value := args[1]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.Secrets.Set(key, value)
			fmt.Printf("Secret set: %s=%s\n", key, value)
			return nil
		})
	},
}

var configSecretUnsetCmd = &cobra.Command{
	Use:   "unset <key>",
	Short: "Unset a secret",
	Long:  `Unset a secret from the environment configuration.`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		key := args[0]
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			if !config.Secrets.Unset(key) {
				return fmt.Errorf("secret not found: %s", key)
			}
			fmt.Printf("Secret unset: %s\n", key)
			return nil
		})
	},
}

var configSecretListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all secrets",
	Long:  `List all secrets that will be set when creating environments.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return withConfig(cmd, func(config *environment.EnvironmentConfig) error {
			keys := config.Secrets.Keys()
			if len(keys) == 0 {
				fmt.Println("No secrets configured")
				return nil
			}

			for i, key := range keys {
				value := config.Secrets.Get(key)
				fmt.Printf("%d. %s=%s\n", i+1, key, value)
			}
			return nil
		})
	},
}

var configSecretClearCmd = &cobra.Command{
	Use:   "clear",
	Short: "Clear all secrets",
	Long:  `Remove all secrets from the environment configuration.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return updateConfig(cmd, func(config *environment.EnvironmentConfig) error {
			config.Secrets.Clear()
			fmt.Println("All secrets cleared")
			return nil
		})
	},
}

func init() {
	// Add base-image commands
	configBaseImageCmd.AddCommand(configBaseImageSetCmd)
	configBaseImageCmd.AddCommand(configBaseImageGetCmd)
	configBaseImageCmd.AddCommand(configBaseImageResetCmd)

	// Add setup-command commands
	configSetupCommandCmd.AddCommand(configSetupCommandAddCmd)
	configSetupCommandCmd.AddCommand(configSetupCommandRemoveCmd)
	configSetupCommandCmd.AddCommand(configSetupCommandListCmd)
	configSetupCommandCmd.AddCommand(configSetupCommandClearCmd)

	// Add install-command commands
	configInstallCommandCmd.AddCommand(configInstallCommandAddCmd)
	configInstallCommandCmd.AddCommand(configInstallCommandRemoveCmd)
	configInstallCommandCmd.AddCommand(configInstallCommandListCmd)
	configInstallCommandCmd.AddCommand(configInstallCommandClearCmd)

	// Add env commands
	configEnvCmd.AddCommand(configEnvSetCmd)
	configEnvCmd.AddCommand(configEnvUnsetCmd)
	configEnvCmd.AddCommand(configEnvListCmd)
	configEnvCmd.AddCommand(configEnvClearCmd)

	// Add secret commands
	configSecretCmd.AddCommand(configSecretSetCmd)
	configSecretCmd.AddCommand(configSecretUnsetCmd)
	configSecretCmd.AddCommand(configSecretListCmd)
	configSecretCmd.AddCommand(configSecretClearCmd)

	// Add object commands to config
	configCmd.AddCommand(configBaseImageCmd)
	configCmd.AddCommand(configSetupCommandCmd)
	configCmd.AddCommand(configInstallCommandCmd)
	configCmd.AddCommand(configEnvCmd)
	configCmd.AddCommand(configSecretCmd)
	configCmd.AddCommand(configShowCmd)
	configCmd.AddCommand(configImportCmd)

	// Add agent command
	configCmd.AddCommand(agent.AgentCmd)

	// Add config command to root
	rootCmd.AddCommand(configCmd)
}

package environment

import (
	"context"
	"crypto/sha256"
	"fmt"
	"strings"

	godiffpatch "github.com/sourcegraph/go-diff-patch"
)

func (env *Environment) FileRead(ctx context.Context, targetFile string, shouldReadEntireFile bool, startLineOneIndexedInclusive int, endLineOneIndexedInclusive int) (string, error) {
	file, err := env.container().File(targetFile).Contents(ctx)
	if err != nil {
		return "", err
	}
	if shouldReadEntireFile {
		return file, err
	}

	lines := strings.Split(file, "\n")
	start := startLineOneIndexedInclusive - 1
	start = max(start, 0)
	if start >= len(lines) {
		start = len(lines) - 1
	}
	if start < 0 {
		return "", fmt.Errorf("error reading file: start_line_one_indexed_inclusive (%d) cannot be less than 1", startLineOneIndexedInclusive)
	}
	end := endLineOneIndexedInclusive

	if end >= len(lines) {
		end = len(lines) - 1
	}
	if end < start {
		return "", fmt.Errorf("error reading file: end_line_one_indexed_inclusive (%d) must be greater than start_line_one_indexed_inclusive (%d)", endLineOneIndexedInclusive, startLineOneIndexedInclusive)
	}

	return strings.Join(lines[start:end], "\n"), nil
}

func (env *Environment) FileWrite(ctx context.Context, explanation, targetFile, contents string) error {
	err := env.apply(ctx, env.container().WithNewFile(targetFile, contents))
	if err != nil {
		return fmt.Errorf("failed applying file write, skipping git propagation: %w", err)
	}
	env.Notes.Add("Write %s", targetFile)
	return nil
}

func (env *Environment) FileEdit(ctx context.Context, explanation, targetFile, search, replace, matchID string) error {
	contents, err := env.container().File(targetFile).Contents(ctx)
	if err != nil {
		return err
	}

	// Find all matches of the search text
	matches := []int{}
	cursor := 0
	for {
		index := strings.Index(contents[cursor:], search)
		if index == -1 {
			break
		}
		actualIndex := cursor + index
		matches = append(matches, actualIndex)
		cursor = actualIndex + 1
	}

	if len(matches) == 0 {
		return fmt.Errorf("search text not found in file %s", targetFile)
	}

	// If there are multiple matches and no matchID is provided, return an error with all matches
	if len(matches) > 1 && matchID == "" {
		var matchDescriptions []string
		for i, matchIndex := range matches {
			// Generate a unique ID for each match
			id := generateMatchID(targetFile, search, replace, i)

			// Get context around the match (3 lines before and after)
			context := getMatchContext(contents, matchIndex)

			matchDescriptions = append(matchDescriptions, fmt.Sprintf("Match %d (ID: %s):\n%s", i+1, id, context))
		}

		return fmt.Errorf("multiple matches found for search text in %s. Please specify which_match parameter with one of the following IDs:\n\n%s",
			targetFile, strings.Join(matchDescriptions, "\n\n"))
	}

	// Determine which match to replace
	var targetMatchIndex int
	if len(matches) == 1 {
		targetMatchIndex = matches[0]
	} else {
		// Find the match with the specified ID
		found := false
		for i, matchIndex := range matches {
			id := generateMatchID(targetFile, search, replace, i)
			if id == matchID {
				targetMatchIndex = matchIndex
				found = true
				break
			}
		}
		if !found {
			return fmt.Errorf("match ID %s not found", matchID)
		}
	}

	// Replace the specific match
	newContents := contents[:targetMatchIndex] + replace + contents[targetMatchIndex+len(search):]

	// Apply the changes using `Directory.withPatch` so we don't have to spit out
	// the entire contents
	patch := godiffpatch.GeneratePatch(targetFile, contents, newContents)
	ctr := env.container()
	err = env.apply(ctx, ctr.WithDirectory(".", ctr.Directory(".").WithPatch(patch)))
	if err != nil {
		return fmt.Errorf("failed applying file edit, skipping git propagation: %w", err)
	}
	env.Notes.Add("Edit %s", targetFile)
	return nil
}

func (env *Environment) FileDelete(ctx context.Context, explanation, targetFile string) error {
	err := env.apply(ctx, env.container().WithoutFile(targetFile))
	if err != nil {
		return fmt.Errorf("failed applying file delete, skipping git propagation: %w", err)
	}
	env.Notes.Add("Delete %s", targetFile)
	return nil
}

func (env *Environment) FileList(ctx context.Context, path string) (string, error) {
	entries, err := env.container().Directory(path).Entries(ctx)
	if err != nil {
		return "", err
	}
	out := &strings.Builder{}
	for _, entry := range entries {
		fmt.Fprintf(out, "%s\n", entry)
	}
	return out.String(), nil
}

// generateMatchID creates a unique ID for a match based on file, search, replace, and index
func generateMatchID(targetFile, search, replace string, index int) string {
	data := fmt.Sprintf("%s:%s:%s:%d", targetFile, search, replace, index)
	hash := sha256.Sum256([]byte(data))
	return fmt.Sprintf("%x", hash)[:8] // Use first 8 characters of hash
}

// getMatchContext returns the context around a match (3 lines before and after)
func getMatchContext(contents string, matchIndex int) string {
	lines := strings.Split(contents, "\n")

	// Find which line contains the match
	currentPos := 0
	matchLine := 0
	for i, line := range lines {
		if currentPos+len(line) >= matchIndex {
			matchLine = i
			break
		}
		currentPos += len(line) + 1 // +1 for newline
	}

	// Get context lines (3 before, match line, 3 after)
	start := max(0, matchLine-3)
	end := min(len(lines), matchLine+4)

	contextLines := make([]string, 0, end-start)
	for i := start; i < end; i++ {
		prefix := "  "
		if i == matchLine {
			prefix = "> " // Mark the line containing the match
		}
		// Include line numbers, which may help the model determine the right match
		prefix += fmt.Sprintf("%4d | ", i+1)
		contextLines = append(contextLines, fmt.Sprintf("%s%s", prefix, lines[i]))
	}

	return strings.Join(contextLines, "\n")
}

package main

import (
	"fmt"
	"os"
	"text/tabwriter"

	"github.com/dagger/container-use/repository"
	"github.com/dustin/go-humanize"
	"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
	Use:   "list",
	Short: "List all environments",
	Long: `Display all active environments with their IDs, titles, and timestamps.
Use -q for environment IDs only, useful for scripting.`,
	RunE: func(app *cobra.Command, _ []string) error {
		ctx := app.Context()
		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}
		envInfos, err := repo.List(ctx)
		if err != nil {
			return err
		}
		if quiet, _ := app.Flags().GetBool("quiet"); quiet {
			for _, envInfo := range envInfos {
				fmt.Println(envInfo.ID)
			}
			return nil
		}

		tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		fmt.Fprintln(tw, "ID\tTITLE\tCREATED\tUPDATED")

		defer tw.Flush()
		for _, envInfo := range envInfos {
			fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", envInfo.ID, truncate(app, envInfo.State.Title, 40), humanize.Time(envInfo.State.CreatedAt), humanize.Time(envInfo.State.UpdatedAt))
		}
		return nil
	},
}

func truncate(app *cobra.Command, s string, max int) string {
	if noTrunc, _ := app.Flags().GetBool("no-trunc"); noTrunc {
		return s
	}
	if len(s) > max {
		return s[:max] + "…"
	}
	return s
}

func init() {
	listCmd.Flags().BoolP("quiet", "q", false, "Display only environment IDs")
	listCmd.Flags().BoolP("no-trunc", "", false, "Don't truncate output")
	rootCmd.AddCommand(listCmd)
}

package main

import (
	"fmt"
	"runtime/debug"

	"github.com/spf13/cobra"
)

var (
	version = "dev"
	commit  = "unknown"
	date    = "unknown"
)

func init() {
	if version == "dev" {
		if buildCommit, buildTime := getBuildInfoFromBinary(); buildCommit != "unknown" {
			commit = buildCommit
			date = buildTime
		}
	}
}

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print version information",
	Long:  `Print the version, commit hash, and build date of the container-use binary.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("container-use version %s\n", version)
		if commit != "unknown" {
			fmt.Printf("commit: %s\n", commit)
		}
		if date != "unknown" {
			fmt.Printf("built: %s\n", date)
		}
	},
}

func init() {
	rootCmd.AddCommand(versionCmd)
}

func getBuildInfoFromBinary() (string, string) {
	buildInfo, ok := debug.ReadBuildInfo()
	if !ok {
		return "unknown", "unknown"
	}

	var revision, buildTime, modified string

	for _, setting := range buildInfo.Settings {
		switch setting.Key {
		case "vcs.revision":
			revision = setting.Value
		case "vcs.time":
			buildTime = setting.Value
		case "vcs.modified":
			modified = setting.Value
		}
	}

	// Format commit hash (use short version)
	if len(revision) > 7 {
		revision = revision[:7]
	}
	if modified == "true" {
		revision += "-dirty"
	}

	if revision == "" {
		revision = "unknown"
	}
	if buildTime == "" {
		buildTime = "unknown"
	}

	return revision, buildTime
}

package agent

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"

	"github.com/dagger/container-use/rules"
	"github.com/mitchellh/go-homedir"
	"gopkg.in/yaml.v3"
)

type ConfigureGoose struct {
	Name        string
	Description string
}

func NewConfigureGoose() *ConfigureGoose {
	return &ConfigureGoose{
		Name:        "Goose",
		Description: "an open source, extensible AI agent that goes beyond code suggestions",
	}
}

// Return the agents full name
func (a *ConfigureGoose) name() string {
	return a.Name
}

// Return a description of the agent
func (a *ConfigureGoose) description() string {
	return a.Description
}

// Save the MCP config with container-use enabled
func (a *ConfigureGoose) editMcpConfig() error {
	var configPath string
	var err error

	if runtime.GOOS == "windows" {
		// Windows: %APPDATA%\Block\goose\config\config.yaml
		// Reference: https://block.github.io/goose/docs/guides/config-file
		appData := os.Getenv("APPDATA")
		if appData == "" {
			return fmt.Errorf("APPDATA environment variable not set")
		}
		configPath = filepath.Join(appData, "Block", "goose", "config", "config.yaml")
	} else {
		// macOS/Linux: ~/.config/goose/config.yaml
		configPath, err = homedir.Expand(filepath.Join("~", ".config", "goose", "config.yaml"))
		if err != nil {
			return err
		}
	}

	// Create directory if it doesn't exist
	if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	// Read existing config or create new
	var config map[string]any
	if data, err := os.ReadFile(configPath); err == nil {
		if err := yaml.Unmarshal(data, &config); err != nil {
			return fmt.Errorf("failed to parse existing config: %w", err)
		}
	} else {
		config = make(map[string]any)
	}

	data, err := a.updateGooseConfig(config)
	if err != nil {
		return err
	}

	if err := os.WriteFile(configPath, data, 0644); err != nil {
		return fmt.Errorf("failed to write config: %w", err)
	}
	return nil
}

func (a *ConfigureGoose) updateGooseConfig(config map[string]any) ([]byte, error) {
	// Get extensions map
	var extensions map[string]any
	if ext, ok := config["extensions"]; ok {
		extensions = ext.(map[string]any)
	} else {
		extensions = make(map[string]any)
		config["extensions"] = extensions
	}

	// Add container-use extension
	extensions["container-use"] = map[string]any{
		"name":    "container-use",
		"type":    "stdio",
		"enabled": true,
		"cmd":     ContainerUseBinary,
		"args":    []any{"stdio"},
		"envs":    map[string]any{},
	}

	// Write config back
	data, err := yaml.Marshal(&config)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal config: %w", err)
	}
	return data, nil
}

// Save the agent rules with the container-use prompt
func (a *ConfigureGoose) editRules() error {
	return saveRulesFile(".goosehints", rules.AgentRules)
}

func (a *ConfigureGoose) isInstalled() bool {
	_, err := exec.LookPath("goose")
	return err == nil
}

package main

import (
	"os"
	"strings"

	"github.com/spf13/cobra"
)

var commandName string

func init() {
	// Override cobra's default completion command
	completionCmd := &cobra.Command{
		Use:   "completion [bash|zsh|fish|powershell]",
		Short: "Generate the autocompletion script for the specified shell",
		Long: `Generate the autocompletion script for container-use for the specified shell.
See each sub-command's help for details on how to use the generated script.

Use --command-name to generate completions for a different command name (e.g., 'cu').`,
		DisableFlagsInUseLine: true,
		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
		Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
		RunE: func(cmd *cobra.Command, args []string) error {
			return generateCompletionForBinary(args[0])
		},
	}

	completionCmd.PersistentFlags().StringVar(&commandName, "command-name", "container-use", "Command name to use in completions")

	// Add help subcommands that show usage instructions
	for _, shell := range []string{"bash", "zsh", "fish", "powershell"} {
		shell := shell // capture loop variable
		helpCmd := &cobra.Command{
			Use:   shell,
			Short: "Generate the autocompletion script for " + shell,
			Long:  generateHelpText(shell),
			RunE: func(cmd *cobra.Command, args []string) error {
				return generateCompletionForBinary(shell)
			},
		}
		completionCmd.AddCommand(helpCmd)
	}

	rootCmd.AddCommand(completionCmd)
}

func generateCompletionForBinary(shell string) error {
	tempRootCmd := &cobra.Command{
		Use:   commandName,
		Short: rootCmd.Short,
		Long:  rootCmd.Long,
	}

	for _, subCmd := range rootCmd.Commands() {
		if subCmd.Name() != "completion" {
			tempRootCmd.AddCommand(subCmd)
		}
	}

	switch shell {
	case "bash":
		return tempRootCmd.GenBashCompletion(os.Stdout)
	case "zsh":
		return tempRootCmd.GenZshCompletion(os.Stdout)
	case "fish":
		return tempRootCmd.GenFishCompletion(os.Stdout, true)
	case "powershell":
		return tempRootCmd.GenPowerShellCompletion(os.Stdout)
	}
	return nil
}

func generateHelpText(shell string) string {
	// Generate help text dynamically based on the shell
	// This reduces duplication of the static help text
	templates := map[string]string{
		"bash": `Generate the autocompletion script for the bash shell.

This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.

To load completions in your current shell session:
	source <({{.command}} completion bash)

To load completions for every new session, save the output to your bash completion directory.
Common locations include:
  - Linux: /usr/local/etc/bash_completion.d/{{.command}}
  - macOS: $(brew --prefix)/etc/bash_completion.d/{{.command}}
  - Windows (Git Bash): /usr/share/bash-completion/completions/{{.command}}

Example:
	{{.command}} completion bash > /path/to/bash_completion.d/{{.command}}`,

		"zsh": `Generate the autocompletion script for the zsh shell.

If shell completion is not already enabled in your environment,
you will need to enable it. You can execute the following once:
	echo "autoload -U compinit; compinit" >> ~/.zshrc

To load completions in your current shell session:
	source <({{.command}} completion zsh)

To load completions for every new session, save the output to your zsh completion directory.
Common locations include:
  - Linux: /usr/local/share/zsh/site-functions/_{{.command}}
  - macOS: $(brew --prefix)/share/zsh/site-functions/_{{.command}}
  - Custom: Any directory in your $fpath

Example:
	{{.command}} completion zsh > /path/to/zsh/site-functions/_{{.command}}`,

		"fish": `Generate the autocompletion script for the fish shell.

To load completions in your current shell session:
	{{.command}} completion fish | source

To load completions for every new session, save the output to your fish completion directory.
Common locations include:
  - Linux/macOS: ~/.config/fish/completions/{{.command}}.fish
  - Windows: %APPDATA%\fish\completions\{{.command}}.fish

Example:
	{{.command}} completion fish > ~/.config/fish/completions/{{.command}}.fish`,

		"powershell": `Generate the autocompletion script for PowerShell.

To load completions in your current shell session:
	{{.command}} completion powershell | Out-String | Invoke-Expression

To load completions for every new session, add the output to your PowerShell profile.
Common profile locations include:
  - Windows: $PROFILE (usually %USERPROFILE%\Documents\PowerShell\Microsoft.PowerShell_profile.ps1)
  - Linux/macOS: ~/.config/powershell/Microsoft.PowerShell_profile.ps1

Example:
	{{.command}} completion powershell >> $PROFILE

Note: You may need to create the profile file if it doesn't exist.`,
	}

	template := templates[shell]
	return strings.ReplaceAll(template, "{{.command}}", commandName)
}

package main

import (
	"os"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var logCmd = &cobra.Command{
	Use:   "log [<env>]",
	Short: "View what an agent did step-by-step",
	Long: `Display the complete development history for an environment.
Shows all commits made by the agent plus command execution notes.
Use -p to include code patches in the output.

If no environment is specified, automatically selects from environments
that are descendants of the current HEAD.`,
	Args:              cobra.MaximumNArgs(1),
	ValidArgsFunction: suggestEnvironments,
	Example: `# See what agent did
container-use log fancy-mallard

# Include code changes
container-use log fancy-mallard -p

# Auto-select environment
container-use log`,
	RunE: func(app *cobra.Command, args []string) error {
		ctx := app.Context()

		// Ensure we're in a git repository
		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}

		envID, err := resolveEnvironmentID(ctx, repo, args)
		if err != nil {
			return err
		}

		patch, _ := app.Flags().GetBool("patch")

		return repo.Log(ctx, envID, patch, os.Stdout)
	},
}

func init() {
	logCmd.Flags().BoolP("patch", "p", false, "Generate patch")
	rootCmd.AddCommand(logCmd)
}

package main

import (
	"log/slog"
	"os"

	"dagger.io/dagger"
	"github.com/dagger/container-use/mcpserver"
	"github.com/spf13/cobra"
)

var stdioCmd = &cobra.Command{
	Use:   "stdio",
	Short: "Start MCP server for agent integration",
	Long:  `Start the Model Context Protocol server that enables AI agents to create and manage containerized environments. This is typically used by agents like Claude Code, Cursor, or VSCode.`,
	RunE: func(app *cobra.Command, _ []string) error {
		ctx := app.Context()

		slog.Info("connecting to dagger")

		dag, err := dagger.Connect(ctx, dagger.WithLogOutput(logWriter))
		if err != nil {
			slog.Error("Error starting dagger", "error", err)

			if isDockerDaemonError(err) {
				handleDockerDaemonError()
			}

			os.Exit(1)
		}
		defer dag.Close()

		return mcpserver.RunStdioServer(ctx, dag)
	},
}

func init() {
	rootCmd.AddCommand(stdioCmd)
}

package agent

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"

	"github.com/dagger/container-use/rules"
	"github.com/mitchellh/go-homedir"
	"github.com/pelletier/go-toml/v2"
)

type ConfigureCodex struct {
	Name        string
	Description string
}

func NewConfigureCodex() *ConfigureCodex {
	return &ConfigureCodex{
		Name:        "OpenAI Codex",
		Description: "OpenAI's lightweight coding agent that runs in your terminal",
	}
}

// Return the agents full name
func (a *ConfigureCodex) name() string {
	return a.Name
}

// Return a description of the agent
func (a *ConfigureCodex) description() string {
	return a.Description
}

// Save the MCP config with container-use enabled
func (a *ConfigureCodex) editMcpConfig() error {
	configPath, err := homedir.Expand(filepath.Join("~", ".codex", "config.toml"))
	if err != nil {
		return err
	}

	// Create directory if it doesn't exist
	if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	// Read existing config or create new
	var config map[string]any
	if data, err := os.ReadFile(configPath); err == nil {
		if err := toml.Unmarshal(data, &config); err != nil {
			return fmt.Errorf("failed to parse existing config: %w", err)
		}
	} else {
		config = make(map[string]any)
	}

	data, err := a.updateCodexConfig(config)
	if err != nil {
		return err
	}

	err = os.WriteFile(configPath, data, 0644)
	if err != nil {
		return fmt.Errorf("failed to write config: %w", err)
	}
	return nil
}

func (a *ConfigureCodex) updateCodexConfig(config map[string]any) ([]byte, error) {
	// Get mcp_servers map
	var mcpServers map[string]any
	if servers, ok := config["mcp_servers"]; ok {
		mcpServers = servers.(map[string]any)
	} else {
		mcpServers = make(map[string]any)
		config["mcp_servers"] = mcpServers
	}

	// Add container-use server
	mcpServers["container-use"] = map[string]any{
		"command":      ContainerUseBinary,
		"args":         []any{"stdio"},
		"auto_approve": tools(""),
	}

	// Write config back
	data, err := toml.Marshal(&config)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal config: %w", err)
	}
	return data, nil
}

// Save the agent rules with the container-use prompt
func (a *ConfigureCodex) editRules() error {
	agentsFile := "AGENTS.md"
	return saveRulesFile(agentsFile, rules.AgentRules)
}

func (a *ConfigureCodex) isInstalled() bool {
	_, err := exec.LookPath("codex")
	return err == nil
}

package agent

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/dagger/container-use/rules"
)

type ConfigureClaude struct {
	Name        string
	Description string
}

func NewConfigureClaude() *ConfigureClaude {
	return &ConfigureClaude{
		Name:        "Claude Code",
		Description: "Anthropic's Claude Code",
	}
}

type ClaudeSettingsLocal struct {
	Permissions *ClaudePermissions `json:"permissions,omitempty"`
	Env         map[string]string  `json:"env,omitempty"`
}

type ClaudePermissions struct {
	Allow []string `json:"allow,omitempty"`
	Deny  []string `json:"deny,omitempty"`
}

func (c *ConfigureClaude) name() string {
	return c.Name
}

func (c *ConfigureClaude) description() string {
	return c.Description
}

func (c *ConfigureClaude) editMcpConfig() error {
	// Add MCP server
	cmd := exec.Command("claude", "mcp", "add", "container-use", "--", ContainerUseBinary, "stdio")
	err := cmd.Run()
	if err != nil {
		return fmt.Errorf("could not automatically add MCP server: %w", err)
	}

	// Configure auto approve settings
	configPath := filepath.Join(".claude", "settings.local.json")
	// Create directory if it doesn't exist
	if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}
	var config ClaudeSettingsLocal
	if data, err := os.ReadFile(configPath); err == nil {
		if err := json.Unmarshal(data, &config); err != nil {
			return fmt.Errorf("failed to parse existing config: %w", err)
		}
	}

	data, err := c.updateSettingsLocal(config)
	if err != nil {
		return err
	}

	err = os.WriteFile(configPath, data, 0644)
	if err != nil {
		return fmt.Errorf("failed to write config: %w", err)
	}
	return nil
}

func (c *ConfigureClaude) updateSettingsLocal(config ClaudeSettingsLocal) ([]byte, error) {
	// Initialize permissions map if nil
	if config.Permissions == nil {
		config.Permissions = &ClaudePermissions{Allow: []string{}}
	}

	// remove save non-container-use items from allow
	allows := []string{}
	for _, tool := range config.Permissions.Allow {
		if !strings.HasPrefix(tool, "mcp__container-use") {
			allows = append(allows, tool)
		}
	}

	// Add container-use tools to allow
	tools := tools("mcp__container-use__")
	allows = append(allows, tools...)
	config.Permissions.Allow = allows

	// Write config back
	data, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal config: %w", err)
	}
	return data, nil
}

func (c *ConfigureClaude) editRules() error {
	return saveRulesFile("CLAUDE.md", rules.AgentRules)
}

func (c *ConfigureClaude) isInstalled() bool {
	_, err := exec.LookPath("claude")
	return err == nil
}

package agent

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"

	"github.com/dagger/container-use/rules"
)

type ConfigureCursor struct {
	Name        string
	Description string
}

func NewConfigureCursor() *ConfigureCursor {
	return &ConfigureCursor{
		Name:        "Cursor",
		Description: "AI-powered code editor",
	}
}

// Return the agents full name
func (a *ConfigureCursor) name() string {
	return a.Name
}

// Return a description of the agent
func (a *ConfigureCursor) description() string {
	return a.Description
}

// Save the MCP config with container-use enabled
func (a *ConfigureCursor) editMcpConfig() error {
	configPath := filepath.Join(".cursor", "mcp.json")

	// Create directory if it doesn't exist
	if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	// Read existing config or create new
	var config MCPServersConfig
	if data, err := os.ReadFile(configPath); err == nil {
		if err := json.Unmarshal(data, &config); err != nil {
			return fmt.Errorf("failed to parse existing config: %w", err)
		}
	}

	data, err := a.updateMcpConfig(config)
	if err != nil {
		return err
	}

	err = os.WriteFile(configPath, data, 0644)
	if err != nil {
		return fmt.Errorf("failed to write config: %w", err)
	}
	return nil
}

func (a *ConfigureCursor) updateMcpConfig(config MCPServersConfig) ([]byte, error) {
	// Initialize mcpServers map if nil
	if config.MCPServers == nil {
		config.MCPServers = make(map[string]MCPServer)
	}

	// Add container-use server
	config.MCPServers["container-use"] = MCPServer{
		Command: ContainerUseBinary,
		Args:    []string{"stdio"},
	}

	// Write config back
	data, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal config: %w", err)
	}
	return data, nil
}

// Save the agent rules with the container-use prompt
func (a *ConfigureCursor) editRules() error {
	rulesFile := filepath.Join(".cursor", "rules", "container-use.mdc")
	return saveRulesFile(rulesFile, rules.CursorRules)
}

func (a *ConfigureCursor) isInstalled() bool {
	return true
}

package agent

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"

	"github.com/dagger/container-use/rules"
)

type ConfigureQ struct {
	Name        string
	Description string
}

func NewConfigureQ() *ConfigureQ {
	return &ConfigureQ{
		Name:        "Amazon Q Developer",
		Description: "Amazon's agentic chat experience in your terminal",
	}
}

// Return the agents full name
func (a *ConfigureQ) name() string {
	return a.Name
}

// Return a description of the agent
func (a *ConfigureQ) description() string {
	return a.Description
}

// Save the MCP config with container-use enabled
func (a *ConfigureQ) editMcpConfig() error {
	configPath := filepath.Join(".amazonq", "mcp.json")

	// Create directory if it doesn't exist
	if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	// Read existing config or create new
	var config MCPServersConfig
	if data, err := os.ReadFile(configPath); err == nil {
		if err := json.Unmarshal(data, &config); err != nil {
			return fmt.Errorf("failed to parse existing config: %w", err)
		}
	}

	data, err := a.updateMcpConfig(config)
	if err != nil {
		return err
	}

	err = os.WriteFile(configPath, data, 0644)
	if err != nil {
		return fmt.Errorf("failed to write config: %w", err)
	}
	return nil
}

func (a *ConfigureQ) updateMcpConfig(config MCPServersConfig) ([]byte, error) {
	// Initialize mcpServers map if nil
	if config.MCPServers == nil {
		config.MCPServers = make(map[string]MCPServer)
	}

	// Add container-use server
	config.MCPServers["container-use"] = MCPServer{
		Command: ContainerUseBinary,
		Args:    []string{"stdio"},
		Env:     map[string]string{},
		Timeout: &[]int{60000}[0],
	}

	// Write config back
	data, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal config: %w", err)
	}
	return data, nil
}

// Save the agent rules with the container-use prompt
func (a *ConfigureQ) editRules() error {
	return saveRulesFile(".amazonq/rules/container-use.md", rules.AgentRules)
}

func (a *ConfigureQ) isInstalled() bool {
	_, err := exec.LookPath("q")
	return err == nil
}

package main

import (
	"context"
	_ "embed"
	"fmt"
	"os"

	"github.com/charmbracelet/fang"
	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var (
	rootCmd = &cobra.Command{
		Use:   "container-use",
		Short: "Containerized environments for coding agents",
		Long: `Container Use creates isolated development environments for AI agents.
Each environment runs in its own container with dedicated git branches.`,
	}
)

func main() {
	ctx := context.Background()
	setupSignalHandling()

	if err := setupLogger(); err != nil {
		fmt.Fprintf(os.Stderr, "Failed to setup logger: %v\n", err)
		os.Exit(1)
	}

	// FIXME(aluzzardi): `fang` misbehaves with the `stdio` command.
	// It hangs on Ctrl-C. Traced the hang back to `lipgloss.HasDarkBackground(os.Stdin, os.Stdout)`
	// I'm assuming it's not playing nice the mcpserver listening on stdio.
	if len(os.Args) > 1 && os.Args[1] == "stdio" {
		if err := rootCmd.ExecuteContext(ctx); err != nil {
			os.Exit(1)
		}
		return
	}

	if err := fang.Execute(
		ctx,
		rootCmd,
		fang.WithVersion(version),
		fang.WithCommit(commit),
		fang.WithNotifySignal(getNotifySignals()...),
	); err != nil {
		os.Exit(1)
	}
}

func suggestEnvironments(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	ctx := cmd.Context()

	repo, err := repository.Open(ctx, ".")
	if err != nil {
		return nil, cobra.ShellCompDirectiveError
	}

	envs, err := repo.List(ctx)
	if err != nil {
		return nil, cobra.ShellCompDirectiveError
	}

	ids := []string{}
	for _, e := range envs {
		ids = append(ids, e.ID)
	}
	return ids, cobra.ShellCompDirectiveKeepOrder
}

package main

import (
	"fmt"
	"os"
	"strings"
)

// isDockerDaemonError checks if the error is related to Docker daemon connectivity
func isDockerDaemonError(err error) bool {
	if err == nil {
		return false
	}

	errStr := strings.ToLower(err.Error())

	// Linux: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
	if strings.Contains(errStr, "cannot connect to the docker daemon") {
		return true
	}

	// Windows: error during connect: Get "http://%2F%2F.%2Fpipe%2FdockerDesktopLinuxEngine/v1.51/containers/json": open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified.
	if strings.Contains(errStr, "error during connect") && strings.Contains(errStr, "pipe/dockerdesktoplinuxengine") && strings.Contains(errStr, "the system cannot find the file specified") {
		return true
	}

	// macOS: request returned 500 Internal Server Error for API route and version http://%2FUsers%2Fb1tank%2F.docker%2Frun%2Fdocker.sock/v1.50/containers/json, check if the server supports the requested API version
	if strings.Contains(errStr, "request returned 500 internal server error") && strings.Contains(errStr, "docker.sock") && strings.Contains(errStr, "check if the server supports the requested api version") {
		return true
	}

	// Generic fallbacks
	return strings.Contains(errStr, "docker daemon") ||
		strings.Contains(errStr, "docker.sock")
}

// handleDockerDaemonError prints a helpful error message for Docker daemon issues
func handleDockerDaemonError() {
	fmt.Fprintf(os.Stderr, "\nError: Docker daemon is not running.\n")
	fmt.Fprintf(os.Stderr, "Please start Docker and try again.\n\n")
}

package main

import (
	"errors"
	"fmt"
	"os"
	"os/exec"

	"dagger.io/dagger"
	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var terminalCmd = &cobra.Command{
	Use:   "terminal [<env>]",
	Short: "Get a shell inside an environment's container",
	Long: `Open an interactive terminal in the exact container environment the agent used. Perfect for debugging, testing, or hands-on exploration.

If no environment is specified, automatically selects from environments
that are descendants of the current HEAD.`,
	Args:              cobra.MaximumNArgs(1),
	ValidArgsFunction: suggestEnvironments,
	Example: `# Drop into environment's container
container-use terminal fancy-mallard

# Debug agent's work interactively
container-use terminal backend-api

# Auto-select environment
container-use terminal`,
	RunE: func(app *cobra.Command, args []string) error {
		ctx := app.Context()

		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}

		// FIXME(aluzzardi): This is a hack to make sure we're wrapped in `dagger run` since `Terminal()` only works with the CLI.
		// If not, it will auto-wrap this command in a `dagger run`.
		if _, ok := os.LookupEnv("DAGGER_SESSION_TOKEN"); !ok {
			daggerBin, err := exec.LookPath("dagger")
			if err != nil {
				if errors.Is(err, exec.ErrNotFound) {
					return fmt.Errorf("dagger is not installed. Please install it from https://docs.dagger.io/install/")
				}
				return fmt.Errorf("failed to look up dagger binary: %w", err)
			}
			return execDaggerRun(daggerBin, append([]string{"dagger", "run"}, os.Args...), os.Environ())
		}

		dag, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
		if err != nil {
			if isDockerDaemonError(err) {
				handleDockerDaemonError()
			}
			return fmt.Errorf("failed to connect to dagger: %w", err)
		}
		defer dag.Close()

		envID, err := resolveEnvironmentID(ctx, repo, args)
		if err != nil {
			return err
		}

		env, err := repo.Get(ctx, dag, envID)
		if err != nil {
			return err
		}

		return env.Terminal(ctx)
	},
}

func init() {
	rootCmd.AddCommand(terminalCmd)
}

package main

import (
	"fmt"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var deleteCmd = &cobra.Command{
	Use:   "delete [<env>...]",
	Short: "Delete environments and start fresh",
	Long: `Delete one or more environments and their associated resources.
This permanently removes the environment's branch and container state.
Use this when starting over with a different approach.

Use --all to delete all environments at once.`,
	Args: func(cmd *cobra.Command, args []string) error {
		all, _ := cmd.Flags().GetBool("all")
		if all && len(args) > 0 {
			return fmt.Errorf("cannot specify environment names when using --all flag")
		}
		if !all && len(args) == 0 {
			return fmt.Errorf("must specify at least one environment name or use --all flag")
		}
		return nil
	},
	ValidArgsFunction: suggestEnvironments,
	Example: `# Delete a single environment
container-use delete fancy-mallard

# Delete multiple environments at once
container-use delete env1 env2 env3

# Delete all environments
container-use delete --all`,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := cmd.Context()
		all, _ := cmd.Flags().GetBool("all")

		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return fmt.Errorf("failed to open repository: %w", err)
		}

		var envIDs []string
		if all {
			// Get all environment IDs
			envs, err := repo.List(ctx)
			if err != nil {
				return fmt.Errorf("failed to list environments: %w", err)
			}
			if len(envs) == 0 {
				fmt.Println("No environments found to delete.")
				return nil
			}
			for _, env := range envs {
				envIDs = append(envIDs, env.ID)
			}
			fmt.Printf("Deleting %d environment(s)...\n", len(envIDs))
		} else {
			envIDs = args
		}

		for _, envID := range envIDs {
			if err := repo.Delete(ctx, envID); err != nil {
				return fmt.Errorf("failed to delete environment '%s': %w", envID, err)
			}
			fmt.Printf("Environment '%s' deleted successfully.\n", envID)
		}

		if all {
			fmt.Printf("Successfully deleted %d environment(s).\n", len(envIDs))
		}

		return nil
	},
}

func init() {
	rootCmd.AddCommand(deleteCmd)
	deleteCmd.Flags().Bool("all", false, "Delete all environments")
}

package environment

import (
	"context"
	"errors"
	"fmt"
	"time"

	"dagger.io/dagger"
)

var (
	serviceStartTimeout = 30 * time.Second
)

type Service struct {
	Config    *ServiceConfig   `json:"config"`
	Endpoints EndpointMappings `json:"endpoints"`

	svc *dagger.Service
}

type EndpointMapping struct {
	EnvironmentInternal string `json:"environment_internal"`
	HostExternal        string `json:"host_external"`
}

type EndpointMappings map[int]*EndpointMapping

func (env *Environment) startServices(ctx context.Context) ([]*Service, error) {
	services := []*Service{}
	for _, cfg := range env.State.Config.Services {
		service, err := env.startService(ctx, cfg)
		if err != nil {
			return nil, err
		}
		services = append(services, service)
	}
	return services, nil
}

func (env *Environment) startService(ctx context.Context, cfg *ServiceConfig) (*Service, error) {
	container := env.dag.Container().From(cfg.Image)
	container, err := containerWithEnvAndSecrets(env.dag, container, cfg.Env, env.State.Config.Secrets)
	if err != nil {
		return nil, err
	}

	if cfg.Command != "" {
		container = container.WithExec([]string{"sh", "-c", cfg.Command})
	}

	args := []string{}
	if cfg.Command != "" {
		args = []string{"sh", "-c", cfg.Command}
	}

	// Expose ports
	for _, port := range cfg.ExposedPorts {
		container = container.WithExposedPort(port, dagger.ContainerWithExposedPortOpts{
			Protocol:    dagger.NetworkProtocolTcp,
			Description: fmt.Sprintf("Port %d", port),
		})
	}

	// Start the service
	startCtx, cancel := context.WithTimeout(ctx, serviceStartTimeout)
	defer cancel()
	svc, err := container.AsService(dagger.ContainerAsServiceOpts{
		Args:          args,
		UseEntrypoint: true,
	}).Start(startCtx)
	if err != nil {
		var exitErr *dagger.ExecError
		if errors.As(err, &exitErr) {
			return nil, fmt.Errorf("command failed with exit code %d.\nstdout: %s\nstderr: %s", exitErr.ExitCode, exitErr.Stdout, exitErr.Stderr)
		}
		if errors.Is(err, context.DeadlineExceeded) {
			return nil, fmt.Errorf("service failed to start within %s timeout", serviceStartTimeout)
		}
		return nil, err
	}

	endpoints := EndpointMappings{}
	for _, port := range cfg.ExposedPorts {
		endpoint := &EndpointMapping{
			EnvironmentInternal: fmt.Sprintf("tcp://%s:%d", cfg.Name, port),
		}
		endpoints[port] = endpoint

		// Expose ports on the host
		tunnel, err := env.dag.Host().Tunnel(svc, dagger.HostTunnelOpts{
			Ports: []dagger.PortForward{
				{
					Backend:  port,
					Frontend: 0,
					Protocol: dagger.NetworkProtocolTcp,
				},
			},
		}).Start(ctx)
		if err != nil {
			return nil, err
		}

		externalEndpoint, err := tunnel.Endpoint(ctx, dagger.ServiceEndpointOpts{
			Scheme: "tcp",
		})
		if err != nil {
			return nil, fmt.Errorf("failed to get endpoint for service %s: %w", cfg.Name, err)
		}
		endpoint.HostExternal = externalEndpoint
	}

	return &Service{
		Config:    cfg,
		Endpoints: endpoints,
		svc:       svc,
	}, nil
}

func (env *Environment) AddService(ctx context.Context, explanation string, cfg *ServiceConfig) (*Service, error) {
	if env.State.Config.Services.Get(cfg.Name) != nil {
		return nil, fmt.Errorf("service %s already exists", cfg.Name)
	}
	svc, err := env.startService(ctx, cfg)
	if err != nil {
		return nil, err
	}
	env.State.Config.Services = append(env.State.Config.Services, cfg)
	env.Services = append(env.Services, svc)

	state := env.container().WithServiceBinding(cfg.Name, svc.svc)
	if err := env.apply(ctx, state); err != nil {
		return nil, err
	}

	env.Notes.Add("Add service %s\n%s\n\n", cfg.Name, explanation)

	return svc, nil
}

package main

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/charmbracelet/huh"
	"github.com/dagger/container-use/environment"
	"github.com/dagger/container-use/repository"
)

// resolveEnvironmentID resolves the environment ID for commands that take env_id as the only positional argument.
// If no args are provided, it filters environments to those where the local repo head is a parent of the environment's head,
// then either auto-selects if there's only one match or prompts the user to select from multiple options.
func resolveEnvironmentID(ctx context.Context, repo *repository.Repository, args []string) (string, error) {
	if len(args) == 1 {
		return args[0], nil
	}
	if len(args) > 1 {
		return "", errors.New("too many arguments")
	}

	// Get current user repo head - this could easily go inside ListDescendantEnvironments, but keeping it outside simplifies testing
	currentHead, err := repository.RunGitCommand(ctx, repo.SourcePath(), "rev-parse", "HEAD")
	if err != nil {
		return "", fmt.Errorf("failed to get current HEAD: %w", err)
	}
	currentHead = strings.TrimSpace(currentHead)

	// Get environments that are descendants of current HEAD
	filteredEnvs, err := repo.ListDescendantEnvironments(ctx, currentHead)
	if err != nil {
		return "", fmt.Errorf("failed to list descendant environments: %w", err)
	}

	if len(filteredEnvs) == 0 {
		return "", errors.New("no environments found that are descendants of the current HEAD")
	}

	// If only one environment matches, use it
	if len(filteredEnvs) == 1 {
		return filteredEnvs[0].ID, nil
	}

	// Multiple environments - prompt user to select
	return promptForEnvironmentSelection(filteredEnvs)
}

// promptForEnvironmentSelection prompts the user to select from multiple environments
func promptForEnvironmentSelection(envs []*environment.EnvironmentInfo) (string, error) {
	var options []huh.Option[string]

	for _, env := range envs {
		title := env.State.Title
		if title == "" {
			title = "No description"
		}

		label := fmt.Sprintf("%s - %s", env.ID, title)
		options = append(options, huh.NewOption(label, env.ID))
	}

	var selectedID string
	prompt := huh.NewSelect[string]().
		Title("Select an environment:").
		Options(options...).
		Value(&selectedID)

	if err := prompt.Run(); err != nil {
		return "", err
	}

	return selectedID, nil
}

//go:build !windows

package main

import (
	"time"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
	watch "github.com/tiborvass/go-watch"
)

var watchCmd = &cobra.Command{
	Use:   "watch",
	Short: "Watch environment activity in real-time",
	Long: `Continuously display environment activity as agents work.
Shows new commits and environment changes updated every second.
Press Ctrl+C to stop watching.`,
	Example: `# Watch all environment activity
container-use watch

# Monitor agents while they work
container-use watch`,
	RunE: func(app *cobra.Command, _ []string) error {
		ctx := app.Context()

		// Ensure we're in a git repository
		if _, err := repository.Open(ctx, "."); err != nil {
			return err
		}

		w := watch.Watcher{Interval: time.Second}
		w.Watch(app.Context(), "git", "log", "--color=always", "--remotes=container-use", "--oneline", "--graph", "--decorate")
		return nil
	},
}

func init() {
	rootCmd.AddCommand(watchCmd)
}

//go:build !windows

package main

import (
	"io"
	"os"
	"os/signal"
	"runtime"
	"syscall"
)

func setupSignalHandling() {
	sigusrCh := make(chan os.Signal, 1)
	signal.Notify(sigusrCh, syscall.SIGUSR1)
	go handleSIGUSR(sigusrCh)
}

func handleSIGUSR(sigusrCh <-chan os.Signal) {
	for sig := range sigusrCh {
		if sig == syscall.SIGUSR1 {
			dumpStacks()
		}
	}
}

func dumpStacks() {
	buf := make([]byte, 1<<20) // 1MB buffer
	n := runtime.Stack(buf, true)
	io.MultiWriter(logWriter, os.Stderr).Write(buf[:n])
}

package environment

import (
	"encoding/json"
	"fmt"
	"time"
)

type State struct {
	CreatedAt time.Time `json:"created_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`

	Config    *EnvironmentConfig `json:"config,omitempty"`
	Container string             `json:"container,omitempty"`
	Title     string             `json:"title,omitempty"`
}

func (s *State) Marshal() ([]byte, error) {
	return json.MarshalIndent(s, "", "  ")
}

func (s *State) Unmarshal(data []byte) error {
	if err := json.Unmarshal(data, &s); err != nil {
		// Try to migrate the legacy state
		legacySt, err := migrateLegacyState(data)
		if err != nil {
			return fmt.Errorf("failed to load state: %w", err)
		}
		*s = *legacySt
	}
	return nil
}

func migrateLegacyState(state []byte) (*State, error) {
	var history legacyState
	if err := json.Unmarshal(state, &history); err != nil {
		return nil, fmt.Errorf("failed to load state: %w", err)
	}
	latest := history.Latest()
	if latest == nil {
		return nil, fmt.Errorf("no latest revision found")
	}

	return &State{
		Container: latest.State,
		CreatedAt: latest.CreatedAt,
		UpdatedAt: latest.CreatedAt,
	}, nil
}

type legacyState []*legacyRevision

func (h legacyState) Latest() *legacyRevision {
	if len(h) == 0 {
		return nil
	}
	return h[len(h)-1]
}

func (h legacyState) LatestVersion() int {
	latest := h.Latest()
	if latest == nil {
		return 0
	}
	return latest.Version
}

func (h legacyState) Get(version int) *legacyRevision {
	for _, revision := range h {
		if revision.Version == version {
			return revision
		}
	}
	return nil
}

type legacyRevision struct {
	Version     int       `json:"version"`
	Name        string    `json:"name"`
	Explanation string    `json:"explanation"`
	Output      string    `json:"output,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	State       string    `json:"state"`
}

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var (
	mergeDelete bool
)

var mergeCmd = &cobra.Command{
	Use:   "merge [<env>]",
	Short: "Accept an environment's work into your branch",
	Long: `Merge an environment's changes into your current git branch.
This makes the agent's work permanent in your repository.
Your working directory will be automatically stashed and restored.

If no environment is specified, automatically selects from environments
that are descendants of the current HEAD.`,
	Args:              cobra.MaximumNArgs(1),
	ValidArgsFunction: suggestEnvironments,
	Example: `# Accept agent's work into current branch
container-use merge backend-api

# Merge and delete the environment after successful merge
container-use merge -d backend-api
container-use merge --delete backend-api

# Auto-select environment
container-use merge`,
	RunE: func(app *cobra.Command, args []string) error {
		ctx := app.Context()

		// Ensure we're in a git repository
		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}

		envID, err := resolveEnvironmentID(ctx, repo, args)
		if err != nil {
			return err
		}

		if err := repo.Merge(ctx, envID, os.Stdout); err != nil {
			return fmt.Errorf("failed to merge environment: %w", err)
		}

		return deleteAfterMerge(ctx, repo, envID, mergeDelete, "merged")
	},
}

func deleteAfterMerge(ctx context.Context, repo *repository.Repository, env string, delete bool, verb string) error {
	if !delete {
		fmt.Printf("Environment '%s' %s successfully.\n", env, verb)
		return nil
	}
	if err := repo.Delete(ctx, env); err != nil {
		return fmt.Errorf("environment '%s' %s but delete failed: %w", env, verb, err)
	}
	fmt.Printf("Environment '%s' %s and deleted successfully.\n", env, verb)
	return nil
}

func init() {
	mergeCmd.Flags().BoolVarP(&mergeDelete, "delete", "d", false, "Delete the environment after successful merge")

	rootCmd.AddCommand(mergeCmd)
}

//go:build windows

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func execDaggerRun(daggerBin string, args []string, env []string) error {
	cmd := exec.Command(daggerBin, args...)
	cmd.Args = append([]string{"dagger", "run"}, os.Args...)
	cmd.Env = env
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to execute dagger run: %w", err)
	}

	// On Windows, we can't replace the current process, so we exit
	os.Exit(0)
	return nil
}

package main

import (
	"fmt"
	"os"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var (
	applyDelete bool
)

var applyCmd = &cobra.Command{
	Use:   "apply [<env>]",
	Short: "Apply an environment's work as staged changes to your branch",
	Long: `Apply an environment's changes to your current git branch as staged modifications.
Unlike 'merge' which preserves the original commit history, 'apply' stages all changes
for you to commit manually, discarding the original commit sequence. This lets you
review and customize the final commit before making the agent's work permanent.
Your working directory will be automatically stashed and restored.

If no environment is specified, automatically selects from environments
that are descendants of the current HEAD.`,
	Args:              cobra.MaximumNArgs(1),
	ValidArgsFunction: suggestEnvironments,
	Example: `# Apply agent's work as staged changes to current branch
cu apply backend-api

# Apply and delete the environment after successful application
cu apply -d backend-api
cu apply --delete backend-api

# After applying, you can review and commit the changes
git status
git commit -m "Add backend API implementation"

# Auto-select environment
cu apply`,
	RunE: func(app *cobra.Command, args []string) error {
		ctx := app.Context()

		// Ensure we're in a git repository
		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}

		envID, err := resolveEnvironmentID(ctx, repo, args)
		if err != nil {
			return err
		}

		if err := repo.Apply(ctx, envID, os.Stdout); err != nil {
			return fmt.Errorf("failed to apply environment: %w", err)
		}

		return deleteAfterMerge(ctx, repo, envID, applyDelete, "applied")
	},
}

func init() {
	applyCmd.Flags().BoolVarP(&applyDelete, "delete", "d", false, "Delete the environment after successful application")

	rootCmd.AddCommand(applyCmd)
}

package main

import (
	"fmt"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var checkoutCmd = &cobra.Command{
	Use:   "checkout [<env>]",
	Short: "Switch to an environment's branch locally",
	Long: `Bring an environment's work into your local git workspace.
This creates a local branch from the environment's state so you can
explore files in your IDE, make changes, or continue development.

If no environment is specified, automatically selects from environments
that are descendants of the current HEAD.`,
	Args:              cobra.MaximumNArgs(1),
	ValidArgsFunction: suggestEnvironments,
	Example: `# Switch to environment's branch locally
container-use checkout fancy-mallard

# Create custom branch name
container-use checkout fancy-mallard -b my-review-branch

# Auto-select environment
container-use checkout`,
	RunE: func(app *cobra.Command, args []string) error {
		ctx := app.Context()

		// Ensure we're in a git repository
		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}

		envID, err := resolveEnvironmentID(ctx, repo, args)
		if err != nil {
			return err
		}

		branchName, err := app.Flags().GetString("branch")
		if err != nil {
			return err
		}

		branch, err := repo.Checkout(ctx, envID, branchName)
		if err != nil {
			return err
		}

		fmt.Printf("Switched to branch '%s'\n", branch)
		return nil
	},
}

func init() {
	checkoutCmd.Flags().StringP("branch", "b", "", "Local branch name to use")
	rootCmd.AddCommand(checkoutCmd)
}

package main

import (
	"encoding/json"
	"fmt"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var inspectCmd = &cobra.Command{
	Use:               "inspect [<env>]",
	Short:             "Inspect an environment",
	Long:              "This is an internal command used by the CLI to inspect an environment. It is not meant to be used by users.",
	Args:              cobra.MaximumNArgs(1),
	Hidden:            true,
	ValidArgsFunction: suggestEnvironments,
	RunE: func(app *cobra.Command, args []string) error {
		ctx := app.Context()
		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}

		envID, err := resolveEnvironmentID(ctx, repo, args)
		if err != nil {
			return err
		}

		envInfo, err := repo.Info(ctx, envID)
		if err != nil {
			return err
		}

		envInfo.State.Container = ""
		out, err := json.MarshalIndent(envInfo, "", "  ")
		if err != nil {
			return err
		}

		fmt.Println(string(out))
		return nil
	},
}

func init() {
	rootCmd.AddCommand(inspectCmd)
}

package environment

import (
	"fmt"
	"strings"
	"sync"
)

type Notes struct {
	items []string
	mu    sync.Mutex
}

func (n *Notes) Add(format string, a ...any) {
	n.mu.Lock()
	defer n.mu.Unlock()

	n.items = append(n.items, fmt.Sprintf(format, a...))
}

func (n *Notes) AddCommand(command string, exitCode int, stdout, stderr string) {
	msg := fmt.Sprintf("$ %s", strings.TrimSpace(command))
	if exitCode != 0 {
		msg += fmt.Sprintf("\nexit %d", exitCode)
	}
	if strings.TrimSpace(stdout) != "" {
		msg += fmt.Sprintf("\n%s", stdout)
	}
	if strings.TrimSpace(stderr) != "" {
		msg += fmt.Sprintf("\nstderr: %s", stderr)
	}

	n.Add("%s", msg)
}

func (n *Notes) Clear() {
	n.mu.Lock()
	defer n.mu.Unlock()

	n.items = []string{}
}

func (n *Notes) String() string {
	n.mu.Lock()
	defer n.mu.Unlock()

	return strings.TrimSpace(strings.Join(n.items, "\n"))
}

func (n *Notes) Pop() string {
	n.mu.Lock()
	defer n.mu.Unlock()

	out := strings.TrimSpace(strings.Join(n.items, "\n"))
	n.items = []string{}

	return out
}

package main

import (
	"os"

	"github.com/dagger/container-use/repository"
	"github.com/spf13/cobra"
)

var diffCmd = &cobra.Command{
	Use:   "diff [<env>]",
	Short: "Show what files an agent changed",
	Long: `Display the code changes made by an agent in an environment.
Shows a git diff between the environment's state and your current branch.

If no environment is specified, automatically selects from environments
that are descendants of the current HEAD.`,
	Args:              cobra.MaximumNArgs(1),
	ValidArgsFunction: suggestEnvironments,
	Example: `# See what changes the agent made
container-use diff fancy-mallard

# Quick assessment before merging
container-use diff backend-api

# Auto-select environment
container-use diff`,
	RunE: func(app *cobra.Command, args []string) error {
		ctx := app.Context()

		// Ensure we're in a git repository
		repo, err := repository.Open(ctx, ".")
		if err != nil {
			return err
		}

		envID, err := resolveEnvironmentID(ctx, repo, args)
		if err != nil {
			return err
		}

		return repo.Diff(ctx, envID, os.Stdout)
	},
}

func init() {
	rootCmd.AddCommand(diffCmd)
}

package mcpserver

import "github.com/mark3labs/mcp-go/mcp"

var (
	explainationArgument = mcp.WithString("explanation",
		mcp.Description("One sentence explanation for why this directory is being listed."),
	)
	environmentSourceArgument = mcp.WithString("environment_source",
		mcp.Description("Absolute path to the source git repository for the environment."),
		mcp.Required(),
	)
	environmentIDArgument = mcp.WithString("environment_id",
		mcp.Description("The ID of the environment for this command. Must call `environment_create` first."),
		mcp.Required(),
	)
)

func newRepositoryTool(name string, description string, args ...mcp.ToolOption) mcp.Tool {
	opts := []mcp.ToolOption{
		mcp.WithDescription(description),
		explainationArgument,
		environmentSourceArgument,
	}
	opts = append(opts, args...)

	return mcp.NewTool(name, opts...)
}

func newEnvironmentTool(name string, description string, args ...mcp.ToolOption) mcp.Tool {
	opts := []mcp.ToolOption{
		mcp.WithDescription(description),
		explainationArgument,
		environmentSourceArgument,
		environmentIDArgument,
	}
	opts = append(opts, args...)

	return mcp.NewTool(name, opts...)
}

//go:build windows

package main

import (
	"os"
	"syscall"
)

// getNotifySignals returns Windows-compatible signals for receiving
func getNotifySignals() []os.Signal {
	// On Windows:
	// - os.Interrupt: Ctrl+C signal (can receive, cannot send to other processes)
	// - syscall.SIGTERM: Termination signal (can receive) (https://pkg.go.dev/os/signal#hdr-Windows)
	// - os.Kill: Not a receivable signal
	return []os.Signal{os.Interrupt, syscall.SIGTERM}
}

//go:build windows

package mcpserver

import (
	"os"
	"syscall"
)

// getNotifySignals returns Windows-compatible signals for MCP server
func getNotifySignals() []os.Signal {
	// On Windows:
	// - os.Interrupt: Ctrl+C signal (can receive, cannot send to other processes)
	// - syscall.SIGTERM: Termination signal (can receive)
	// - os.Kill: Not a receivable signal, used only for Process.Kill()
	return []os.Signal{os.Interrupt, syscall.SIGTERM}
}

//go:build !windows

package main

import (
	"os"
	"syscall"
)

// getNotifySignals returns Unix-compatible signals
func getNotifySignals() []os.Signal {
	return []os.Signal{os.Interrupt, os.Kill, syscall.SIGTERM}
}

//go:build !windows

package mcpserver

import (
	"os"
	"syscall"
)

// getNotifySignals returns Unix-compatible signals for MCP server
func getNotifySignals() []os.Signal {
	return []os.Signal{os.Interrupt, os.Kill, syscall.SIGTERM}
}

//go:build !windows

package main

import (
	"syscall"
)

func execDaggerRun(daggerBin string, args []string, env []string) error {
	return syscall.Exec(daggerBin, args, env)
}

//go:build windows

package main

// On Windows, SIGUSR1 is not available, so we provide a no-op implementation
func setupSignalHandling() {
	// No special signal handling on Windows
}

package rules

import _ "embed"

//go:embed agent.md
var AgentRules string

//go:embed cursor.mdc
var CursorRules string

This code segment functions to: Configure application logging by specifying output destinations based on environmental settings and establishing severity level controls through a multi-writer handler initialization.

```go
[TASK_BEGIN]
func setupLogger() error {
	logFile := os.Getenv("LOG_FILE")
	if logFile != "" {
		dir := filepath.Dir(logFile)
		if err := os.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("failed to create log directory: %w", err)
		}

		f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			return fmt.Errorf("failed to open log file: %w", err)
		}
		logWriter = f
	}

	level := parseLogLevel(os.Getenv("LOG_LEVEL"))
	logger := slog.New(slog.NewTextHandler(logWriter, &slog.HandlerOptions{
		Level: level,
	}))
	slog.SetDefault(logger)

	return nil
}
[TASK_END]
```