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") } }