hako/docker.go
2025-06-10 22:16:16 -03:00

226 lines
6 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
const hakoVersion = "1"
func getConfigDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := filepath.Join(homeDir, ".config", "hako")
return configDir, nil
}
func ensureConfigDir() error {
configDir, err := getConfigDir()
if err != nil {
return err
}
return os.MkdirAll(configDir, 0755)
}
func getBaseDockerfile() string {
return fmt.Sprintf(`FROM debian:trixie-20250610
LABEL HAKO-VERSION=%s
RUN apt-get update && apt-get install -y \
curl \
git \
build-essential \
ca-certificates \
fish \
&& rm -rf /var/lib/apt/lists/*
# Install Node.js LTS
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y nodejs
# Install Claude Code
RUN npm install -g @anthropic-ai/claude-code
# configure some things
RUN mkdir /workspace
RUN git config --global --add safe.directory /workspace
WORKDIR /workspace
CMD ["/usr/bin/fish"]`, hakoVersion)
}
func getLanguageDockerfile(lang string) string {
switch lang {
case "go":
return fmt.Sprintf(`FROM hako-userland
LABEL HAKO-VERSION=%s
RUN curl -fsSL https://go.dev/dl/go1.21.5.linux-amd64.tar.gz | tar -C /usr/local -xzf -
ENV PATH="/usr/local/go/bin:${PATH}"
WORKDIR /workspace
CMD ["/usr/bin/fish"]`, hakoVersion)
case "py", "python":
return fmt.Sprintf(`FROM hako-userland
LABEL HAKO-VERSION=%s
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
RUN ln -s /usr/bin/python3 /usr/bin/python
WORKDIR /workspace
CMD ["/usr/bin/fish"]`, hakoVersion)
default:
return ""
}
}
func buildBaseImage() error {
if err := ensureConfigDir(); err != nil {
return err
}
configDir, _ := getConfigDir()
dockerfilePath := filepath.Join(configDir, "Dockerfile.base")
dockerfile := getBaseDockerfile()
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
return err
}
fmt.Printf("Building base image hako-userland...\n")
return runCommand("docker", "build", "-f", dockerfilePath, "-t", "hako-userland", ".")
}
func buildLanguageImage(lang string) error {
dockerfile := getLanguageDockerfile(lang)
if dockerfile == "" {
return fmt.Errorf("unsupported language: %s", lang)
}
configDir, err := getConfigDir()
if err != nil {
return err
}
dockerfilePath := filepath.Join(configDir, fmt.Sprintf("Dockerfile.%s", lang))
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
return err
}
imageName := fmt.Sprintf("hako-userland-%s", lang)
fmt.Printf("Building %s image %s...\n", lang, imageName)
return runCommand("docker", "build", "-f", dockerfilePath, "-t", imageName, ".")
}
func imageExists(imageName string) bool {
err := runCommandSilent("docker", "image", "inspect", imageName)
return err == nil
}
func containerExists(containerName string) bool {
err := runCommandSilent("docker", "container", "inspect", containerName)
return err == nil
}
func containerIsRunning(containerName string) bool {
output, err := runCommandOutput("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
return err == nil && strings.TrimSpace(output) == "true"
}
func createContainer(containerName, imageName, workspaceDir string) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return err
}
args := []string{"create", "--name", containerName}
// No mounting - workspace is isolated and copied separately
args = append(args, "-it", imageName)
if err := runCommand("docker", args...); err != nil {
return err
}
// Copy Claude config directory if it exists
claudeDir := filepath.Join(homeDir, ".claude")
if _, err := os.Stat(claudeDir); err == nil {
if err := copyToContainer(containerName, claudeDir, "/root/.claude"); err != nil {
return fmt.Errorf("failed to copy Claude config directory: %w", err)
}
}
// Copy Claude config file if it exists
claudeFile := filepath.Join(homeDir, ".claude.json")
if _, err := os.Stat(claudeFile); err == nil {
if err := copyToContainer(containerName, claudeFile, "/root/.claude.json"); err != nil {
return fmt.Errorf("failed to copy Claude config file: %w", err)
}
}
return nil
}
func startContainer(containerName string) error {
return runCommand("docker", "start", containerName)
}
func stopContainer(containerName string) error {
return runCommand("docker", "stop", containerName)
}
func removeContainer(containerName string) error {
return runCommand("docker", "rm", containerName)
}
func execContainer(containerName string) error {
cmd := exec.Command("docker", "exec", "-it", containerName, "/usr/bin/fish")
fmt.Fprintf(os.Stderr, "+ %s %s\n", "docker", strings.Join([]string{"exec", "-it", containerName, "/usr/bin/fish"}, " "))
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func copyToContainer(containerName, srcPath, destPath string) error {
return runCommand("docker", "cp", srcPath, containerName+":"+destPath)
}
func copyFromContainer(containerName, srcPath, destPath string) error {
return runCommand("docker", "cp", containerName+":"+srcPath, destPath)
}
func listHakoContainers() error {
return runCommand("docker", "ps", "-a", "--filter", "name=hako-", "--format", "table {{.Names}}\\t{{.Status}}\\t{{.Image}}")
}
func getImageVersion(imageName string) (string, error) {
output, err := runCommandOutput("docker", "image", "inspect", "-f", "{{index .Config.Labels \"HAKO-VERSION\"}}", imageName)
if err != nil {
return "", err
}
return strings.TrimSpace(output), nil
}
func checkVersionMismatch(imageName string) {
imageVersion, err := getImageVersion(imageName)
if err != nil {
// If we can't get the version, don't warn (might be old image without version)
return
}
if imageVersion != "" && imageVersion != hakoVersion {
fmt.Printf("\033[33mWarning: Image %s has version %s but hako executable is version %s\033[0m\n", imageName, imageVersion, hakoVersion)
fmt.Printf("\033[33mConsider running 'hako init' to rebuild with the current version\033[0m\n")
}
}