226 lines
6 KiB
Go
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")
|
|
}
|
|
}
|