log commands to stderr

This commit is contained in:
Luna 2025-06-10 19:09:37 -03:00
parent 051121c79e
commit d0159c2ba4
10 changed files with 112 additions and 89 deletions

View file

@ -3,6 +3,7 @@ package docker
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -11,6 +12,12 @@ import (
"l4.pm/hako/pkg/config" "l4.pm/hako/pkg/config"
) )
// loggedCommand creates a command and logs it to stderr
func loggedCommand(name string, args ...string) *exec.Cmd {
log.Printf("exec: %s %s", name, strings.Join(args, " "))
return exec.Command(name, args...)
}
// Client wraps Docker operations. // Client wraps Docker operations.
type Client struct { type Client struct {
ctx context.Context ctx context.Context
@ -19,7 +26,7 @@ type Client struct {
// NewClient creates a new Docker client. // NewClient creates a new Docker client.
func NewClient() (*Client, error) { func NewClient() (*Client, error) {
// Check if Docker is available // Check if Docker is available
cmd := exec.Command("docker", "version", "--format", "{{.Server.Version}}") cmd := loggedCommand("docker", "version", "--format", "{{.Server.Version}}")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("docker is not available: %w", err) return nil, fmt.Errorf("docker is not available: %w", err)
} }
@ -39,7 +46,7 @@ func (c *Client) BuildBaseImage(cfg *config.Config) error {
} }
// Build the base image // Build the base image
cmd := exec.Command("docker", "build", cmd := loggedCommand("docker", "build",
"-f", dockerfile, "-f", dockerfile,
"-t", "hako-userland", "-t", "hako-userland",
filepath.Dir(dockerfile)) filepath.Dir(dockerfile))
@ -72,7 +79,7 @@ func (c *Client) BuildLanguageImage(language string, cfg *config.Config) error {
imageName := fmt.Sprintf("hako-userland-%s", language) imageName := fmt.Sprintf("hako-userland-%s", language)
// Build the language image // Build the language image
cmd := exec.Command("docker", "build", cmd := loggedCommand("docker", "build",
"-f", dockerfile, "-f", dockerfile,
"-t", imageName, "-t", imageName,
filepath.Dir(dockerfile)) filepath.Dir(dockerfile))
@ -111,7 +118,7 @@ func (c *Client) StartShell(containerName, language string, syncer interface{ To
fmt.Printf("Starting existing container: %s\n", containerName) fmt.Printf("Starting existing container: %s\n", containerName)
// Start the container if it's stopped // Start the container if it's stopped
cmd := exec.Command("docker", "start", containerName) cmd := loggedCommand("docker", "start", containerName)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to start container: %w", err) return fmt.Errorf("failed to start container: %w", err)
} }
@ -121,7 +128,7 @@ func (c *Client) StartShell(containerName, language string, syncer interface{ To
// Get current user info for running container as host user // Get current user info for running container as host user
uid := os.Getenv("UID") uid := os.Getenv("UID")
if uid == "" { if uid == "" {
cmd := exec.Command("id", "-u") cmd := loggedCommand("id", "-u")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user ID: %w", err) return fmt.Errorf("failed to get user ID: %w", err)
@ -131,7 +138,7 @@ func (c *Client) StartShell(containerName, language string, syncer interface{ To
gid := os.Getenv("GID") gid := os.Getenv("GID")
if gid == "" { if gid == "" {
cmd := exec.Command("id", "-g") cmd := loggedCommand("id", "-g")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("failed to get group ID: %w", err) return fmt.Errorf("failed to get group ID: %w", err)
@ -153,19 +160,19 @@ func (c *Client) StartShell(containerName, language string, syncer interface{ To
// Mount Claude config file if it exists // Mount Claude config file if it exists
claudeConfigPath := filepath.Join(homeDir, ".claude.json") claudeConfigPath := filepath.Join(homeDir, ".claude.json")
if _, err := os.Stat(claudeConfigPath); err == nil { if _, err := os.Stat(claudeConfigPath); err == nil {
args = append(args, "-v", fmt.Sprintf("%s:/home/user/.claude.json:ro", claudeConfigPath)) args = append(args, "-v", fmt.Sprintf("%s:/workspace/.claude.json:ro", claudeConfigPath))
} }
// Mount Claude directory if it exists // Mount Claude directory if it exists
claudeDirPath := filepath.Join(homeDir, ".claude") claudeDirPath := filepath.Join(homeDir, ".claude")
if _, err := os.Stat(claudeDirPath); err == nil { if _, err := os.Stat(claudeDirPath); err == nil {
args = append(args, "-v", fmt.Sprintf("%s:/home/user/.claude", claudeDirPath)) args = append(args, "-v", fmt.Sprintf("%s:/workspace/.claude", claudeDirPath))
} }
args = append(args, imageName, "/bin/bash") args = append(args, imageName, "/bin/bash")
// Create and start the container // Create and start the container
cmd := exec.Command("docker", args...) cmd := loggedCommand("docker", args...)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create container: %w", err) return fmt.Errorf("failed to create container: %w", err)
@ -179,7 +186,7 @@ func (c *Client) StartShell(containerName, language string, syncer interface{ To
// Execute interactive shell // Execute interactive shell
fmt.Printf("Dropping into shell in container: %s\n", containerName) fmt.Printf("Dropping into shell in container: %s\n", containerName)
cmd := exec.Command("docker", "exec", "-it", containerName, "/bin/bash") cmd := loggedCommand("docker", "exec", "-it", containerName, "/bin/bash")
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -194,7 +201,7 @@ func (c *Client) GetCurrentContainer(path string) (string, error) {
sanitized := sanitizeContainerName(path) sanitized := sanitizeContainerName(path)
// Try to find a container that matches this path pattern // Try to find a container that matches this path pattern
cmd := exec.Command("docker", "ps", "-a", "--format", "{{.Names}}") cmd := loggedCommand("docker", "ps", "-a", "--format", "{{.Names}}")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to list containers: %w", err) return "", fmt.Errorf("failed to list containers: %w", err)
@ -212,7 +219,7 @@ func (c *Client) GetCurrentContainer(path string) (string, error) {
// ListContainers returns all running hako containers. // ListContainers returns all running hako containers.
func (c *Client) ListContainers() ([]string, error) { func (c *Client) ListContainers() ([]string, error) {
cmd := exec.Command("docker", "ps", "--filter", "name=hako-", "--format", "{{.Names}}") cmd := loggedCommand("docker", "ps", "--filter", "name=hako-", "--format", "{{.Names}}")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err) return nil, fmt.Errorf("failed to list containers: %w", err)
@ -229,13 +236,13 @@ func (c *Client) ListContainers() ([]string, error) {
// StopContainer stops and removes a container. // StopContainer stops and removes a container.
func (c *Client) StopContainer(containerName string) error { func (c *Client) StopContainer(containerName string) error {
// Stop the container // Stop the container
cmd := exec.Command("docker", "stop", containerName) cmd := loggedCommand("docker", "stop", containerName)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to stop container: %w", err) return fmt.Errorf("failed to stop container: %w", err)
} }
// Remove the container // Remove the container
cmd = exec.Command("docker", "rm", containerName) cmd = loggedCommand("docker", "rm", containerName)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to remove container: %w", err) return fmt.Errorf("failed to remove container: %w", err)
} }
@ -246,14 +253,14 @@ func (c *Client) StopContainer(containerName string) error {
// imageExists checks if a Docker image exists locally. // imageExists checks if a Docker image exists locally.
func (c *Client) imageExists(imageName string) bool { func (c *Client) imageExists(imageName string) bool {
cmd := exec.Command("docker", "images", "-q", imageName) cmd := loggedCommand("docker", "images", "-q", imageName)
output, err := cmd.Output() output, err := cmd.Output()
return err == nil && len(strings.TrimSpace(string(output))) > 0 return err == nil && len(strings.TrimSpace(string(output))) > 0
} }
// containerExists checks if a container exists. // containerExists checks if a container exists.
func (c *Client) containerExists(containerName string) bool { func (c *Client) containerExists(containerName string) bool {
cmd := exec.Command("docker", "ps", "-a", "-q", "-f", fmt.Sprintf("name=%s", containerName)) cmd := loggedCommand("docker", "ps", "-a", "-q", "-f", fmt.Sprintf("name=%s", containerName))
output, err := cmd.Output() output, err := cmd.Output()
return err == nil && len(strings.TrimSpace(string(output))) > 0 return err == nil && len(strings.TrimSpace(string(output))) > 0
} }

View file

@ -2,12 +2,27 @@ package git
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
) )
// loggedCommand creates a command and logs it to stderr
func loggedCommand(name string, args ...string) *exec.Cmd {
log.Printf("exec: %s %s", name, strings.Join(args, " "))
return exec.Command(name, args...)
}
// loggedCommandWithDir creates a command with a working directory and logs it
func loggedCommandWithDir(dir, name string, args ...string) *exec.Cmd {
log.Printf("exec (in %s): %s %s", dir, name, strings.Join(args, " "))
cmd := exec.Command(name, args...)
cmd.Dir = dir
return cmd
}
// Repo represents a git repository. // Repo represents a git repository.
type Repo struct { type Repo struct {
workDir string workDir string
@ -22,8 +37,7 @@ func NewRepo() (*Repo, error) {
} }
// Check if we're inside a git repository // Check if we're inside a git repository
cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree") cmd := loggedCommandWithDir(workDir, "git", "rev-parse", "--is-inside-work-tree")
cmd.Dir = workDir
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("not inside a git repository") return nil, fmt.Errorf("not inside a git repository")
@ -34,8 +48,7 @@ func NewRepo() (*Repo, error) {
} }
// Get the repository root directory // Get the repository root directory
cmd = exec.Command("git", "rev-parse", "--show-toplevel") cmd = loggedCommandWithDir(workDir, "git", "rev-parse", "--show-toplevel")
cmd.Dir = workDir
output, err = cmd.Output() output, err = cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get git root directory: %w", err) return nil, fmt.Errorf("failed to get git root directory: %w", err)
@ -61,8 +74,7 @@ func (r *Repo) Root() string {
// RelativePath returns the current directory relative to the git root. // RelativePath returns the current directory relative to the git root.
func (r *Repo) RelativePath() (string, error) { func (r *Repo) RelativePath() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-prefix") cmd := loggedCommandWithDir(r.workDir, "git", "rev-parse", "--show-prefix")
cmd.Dir = r.workDir
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get relative path: %w", err) return "", fmt.Errorf("failed to get relative path: %w", err)
@ -73,8 +85,7 @@ func (r *Repo) RelativePath() (string, error) {
// ListFiles returns all tracked files in the repository. // ListFiles returns all tracked files in the repository.
func (r *Repo) ListFiles() ([]string, error) { func (r *Repo) ListFiles() ([]string, error) {
cmd := exec.Command("git", "ls-files") cmd := loggedCommandWithDir(r.rootDir, "git", "ls-files")
cmd.Dir = r.rootDir
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list git files: %w", err) return nil, fmt.Errorf("failed to list git files: %w", err)
@ -90,8 +101,7 @@ func (r *Repo) ListFiles() ([]string, error) {
// ListUntrackedFiles returns all untracked files that are not ignored. // ListUntrackedFiles returns all untracked files that are not ignored.
func (r *Repo) ListUntrackedFiles() ([]string, error) { func (r *Repo) ListUntrackedFiles() ([]string, error) {
cmd := exec.Command("git", "ls-files", "--others", "--exclude-standard") cmd := loggedCommandWithDir(r.rootDir, "git", "ls-files", "--others", "--exclude-standard")
cmd.Dir = r.rootDir
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list untracked files: %w", err) return nil, fmt.Errorf("failed to list untracked files: %w", err)
@ -127,8 +137,7 @@ func (r *Repo) ListAllFiles() ([]string, error) {
// IsIgnored checks if a file path is ignored by git. // IsIgnored checks if a file path is ignored by git.
func (r *Repo) IsIgnored(path string) (bool, error) { func (r *Repo) IsIgnored(path string) (bool, error) {
cmd := exec.Command("git", "check-ignore", path) cmd := loggedCommandWithDir(r.rootDir, "git", "check-ignore", path)
cmd.Dir = r.rootDir
err := cmd.Run() err := cmd.Run()
if err == nil { if err == nil {

View file

@ -2,6 +2,7 @@ package sync
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -10,6 +11,12 @@ import (
"l4.pm/hako/pkg/git" "l4.pm/hako/pkg/git"
) )
// loggedCommand creates a command and logs it to stderr
func loggedCommand(name string, args ...string) *exec.Cmd {
log.Printf("exec: %s %s", name, strings.Join(args, " "))
return exec.Command(name, args...)
}
// Syncer handles file synchronization between host and containers. // Syncer handles file synchronization between host and containers.
type Syncer struct { type Syncer struct {
repo *git.Repo repo *git.Repo
@ -35,7 +42,7 @@ func (s *Syncer) ToContainer(containerName string) error {
} }
// Create workspace directory in container // Create workspace directory in container
cmd := exec.Command("docker", "exec", containerName, "mkdir", "-p", "/workspace") cmd := loggedCommand("docker", "exec", containerName, "mkdir", "-p", "/workspace")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create workspace directory in container: %w", err) return fmt.Errorf("failed to create workspace directory in container: %w", err)
} }
@ -48,13 +55,13 @@ func (s *Syncer) ToContainer(containerName string) error {
// Ensure directory exists in container // Ensure directory exists in container
dir := filepath.Dir(file) dir := filepath.Dir(file)
if dir != "." { if dir != "." {
cmd := exec.Command("docker", "exec", containerName, "mkdir", "-p", fmt.Sprintf("/workspace/%s", dir)) cmd := loggedCommand("docker", "exec", containerName, "mkdir", "-p", fmt.Sprintf("/workspace/%s", dir))
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create directory %s in container: %w", dir, err) return fmt.Errorf("failed to create directory %s in container: %w", dir, err)
} }
} }
cmd := exec.Command("docker", "cp", srcPath, dstPath) cmd := loggedCommand("docker", "cp", srcPath, dstPath)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to copy %s to container: %w", file, err) return fmt.Errorf("failed to copy %s to container: %w", file, err)
} }
@ -67,7 +74,7 @@ func (s *Syncer) ToContainer(containerName string) error {
// FromContainer copies files from container back to host. // FromContainer copies files from container back to host.
func (s *Syncer) FromContainer(containerName string) error { func (s *Syncer) FromContainer(containerName string) error {
// Get list of files in container workspace // Get list of files in container workspace
cmd := exec.Command("docker", "exec", containerName, "find", "/workspace", "-type", "f", "-not", "-path", "/workspace/.git/*") cmd := loggedCommand("docker", "exec", containerName, "find", "/workspace", "-type", "f", "-not", "-path", "/workspace/.git/*")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("failed to list files in container: %w", err) return fmt.Errorf("failed to list files in container: %w", err)
@ -106,7 +113,7 @@ func (s *Syncer) FromContainer(containerName string) error {
return fmt.Errorf("failed to create directory %s: %w", dir, err) return fmt.Errorf("failed to create directory %s: %w", dir, err)
} }
cmd := exec.Command("docker", "cp", srcPath, dstPath) cmd := loggedCommand("docker", "cp", srcPath, dstPath)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to copy %s from container: %w", relPath, err) return fmt.Errorf("failed to copy %s from container: %w", relPath, err)
} }