diff --git a/CLAUDE.md b/CLAUDE.md index 4f8bc13..52d4600 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,8 @@ Hako is a Docker container management tool designed specifically for Claude Code - Container names follow pattern: `hako-{language}-{project}-{subdirs}` (sanitized, max 60 chars) - All Docker commands are logged to stderr for transparency - File syncing respects .gitignore and excludes .git directory for security -- Images are versioned (HAKO-VERSION=1) for upgrade management +- Images are versioned (HAKO-VERSION=1, as a Docker label) for upgrade management + - NOTE: changes to the base Dockerfiles should involve bumping the hako version ## Development Commands @@ -44,4 +45,4 @@ go build -o hako - Base Dockerfile stored in `~/.config/hako/Dockerfile.base` - Language Dockerfiles in `~/.config/hako/Dockerfile.{lang}` -- Mounts Claude config directory/file if present for seamless integration \ No newline at end of file +- Mounts Claude config directory/file if present for seamless integration diff --git a/commands.go b/commands.go index d061eac..41d17f1 100644 --- a/commands.go +++ b/commands.go @@ -19,6 +19,9 @@ func initCommand(lang string) error { if err := buildBaseImage(); err != nil { return err } + } else { + // Check base image version + checkVersionMismatch("hako-userland") } fmt.Printf("Building %s language image...\n", lang) @@ -36,6 +39,9 @@ func upCommand(lang string) error { return fmt.Errorf("image %s not found, run 'hako init %s' first", imageName, lang) } + // Check for version mismatch + checkVersionMismatch(imageName) + cwd, err := os.Getwd() if err != nil { return err @@ -103,7 +109,24 @@ func downCommand(container string) error { func psCommand() error { fmt.Println("Hako containers:") - return listHakoContainers() + if err := listHakoContainers(); err != nil { + return err + } + + // Check for version mismatches in running containers + output, err := runCommandOutput("docker", "ps", "-a", "--filter", "name=hako-", "--format", "{{.Image}}") + if err == nil { + images := strings.Split(strings.TrimSpace(output), "\n") + checkedImages := make(map[string]bool) + for _, image := range images { + if image != "" && !checkedImages[image] { + checkVersionMismatch(image) + checkedImages[image] = true + } + } + } + + return nil } func syncCommand() error { @@ -122,7 +145,7 @@ func syncCommand() error { } fmt.Printf("Syncing files from container %s...\n", containerName) - + tempDir, err := os.MkdirTemp("", "hako-sync-*") if err != nil { return err @@ -184,12 +207,12 @@ func copyWorkspaceToContainer(containerName string) error { if err != nil { return err } - + untrackedFiles, err := runCommandOutput("git", "ls-files", "--others", "--exclude-standard") if err != nil { return err } - + allFiles := []string{} if trackedFiles != "" { allFiles = append(allFiles, strings.Split(trackedFiles, "\n")...) @@ -197,17 +220,17 @@ func copyWorkspaceToContainer(containerName string) error { if untrackedFiles != "" { allFiles = append(allFiles, strings.Split(untrackedFiles, "\n")...) } - + if len(allFiles) == 0 { return fmt.Errorf("no files to copy") } - + // Copy files one by one to preserve directory structure for _, file := range allFiles { if file == "" { continue } - + // Create directory structure in container if needed dir := filepath.Dir(file) if dir != "." { @@ -215,12 +238,12 @@ func copyWorkspaceToContainer(containerName string) error { return err } } - + // Copy the file if err := copyToContainer(containerName, file, "/workspace/"+file); err != nil { return fmt.Errorf("failed to copy %s: %v", file, err) } } - + return nil -} \ No newline at end of file +} diff --git a/docker.go b/docker.go index 4aef59a..6faea18 100644 --- a/docker.go +++ b/docker.go @@ -28,8 +28,8 @@ func ensureConfigDir() error { } func getBaseDockerfile() string { - return fmt.Sprintf(`# HAKO-VERSION=%s -FROM debian:trixie-20250610 + return fmt.Sprintf(`FROM debian:trixie-20250610 +LABEL HAKO-VERSION=%s RUN apt-get update && apt-get install -y \ curl \ @@ -57,17 +57,17 @@ CMD ["/usr/bin/fish"]`, hakoVersion) func getLanguageDockerfile(lang string) string { switch lang { case "go": - return fmt.Sprintf(`# HAKO-VERSION=%s -FROM hako-userland + 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 ["/bin/bash"]`, hakoVersion) +CMD ["/usr/bin/fish"]`, hakoVersion) case "py", "python": - return fmt.Sprintf(`# HAKO-VERSION=%s -FROM hako-userland + return fmt.Sprintf(`FROM hako-userland +LABEL HAKO-VERSION=%s RUN apt-get update && apt-get install -y \ python3 \ @@ -78,7 +78,7 @@ RUN apt-get update && apt-get install -y \ RUN ln -s /usr/bin/python3 /usr/bin/python WORKDIR /workspace -CMD ["/bin/bash"]`, hakoVersion) +CMD ["/usr/bin/fish"]`, hakoVersion) default: return "" } @@ -148,7 +148,6 @@ func createContainer(containerName, imageName, workspaceDir string) error { // Mount workspace args = append(args, "-v", workspaceDir+":/workspace") - args = append(args, "-it", imageName) if err := runCommand("docker", args...); err != nil { @@ -206,3 +205,24 @@ func copyFromContainer(containerName, srcPath, destPath string) error { 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") + } +} diff --git a/git.go b/git.go index b98a706..537267d 100644 --- a/git.go +++ b/git.go @@ -41,7 +41,7 @@ func getProjectContainerName(lang string) (string, error) { } projectName := filepath.Base(gitRoot) - + var pathComponents []string if relPath != "." { pathComponents = strings.Split(relPath, string(os.PathSeparator)) @@ -65,4 +65,4 @@ func sanitizeName(name string) string { result = strings.ReplaceAll(result, ".", "_") result = strings.ReplaceAll(result, "-", "_") return result -} \ No newline at end of file +} diff --git a/main.go b/main.go index 05ecf82..86f4f83 100644 --- a/main.go +++ b/main.go @@ -96,4 +96,4 @@ func main() { if err := rootCmd.Execute(); err != nil { os.Exit(1) } -} \ No newline at end of file +} diff --git a/utils.go b/utils.go index ca62b74..65508f9 100644 --- a/utils.go +++ b/utils.go @@ -24,12 +24,3 @@ func runCommandOutput(name string, args ...string) (string, error) { } return strings.TrimSpace(string(output)), nil } - -func runCommandWithInput(name string, input string, args ...string) error { - cmd := exec.Command(name, args...) - fmt.Fprintf(os.Stderr, "+ %s %s\n", name, strings.Join(args, " ")) - cmd.Stdin = strings.NewReader(input) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} \ No newline at end of file