add HAKO-VERSION checking
This commit is contained in:
parent
33f45eae02
commit
30a510cb17
6 changed files with 68 additions and 33 deletions
|
@ -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)
|
- Container names follow pattern: `hako-{language}-{project}-{subdirs}` (sanitized, max 60 chars)
|
||||||
- All Docker commands are logged to stderr for transparency
|
- All Docker commands are logged to stderr for transparency
|
||||||
- File syncing respects .gitignore and excludes .git directory for security
|
- 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
|
## Development Commands
|
||||||
|
|
||||||
|
@ -44,4 +45,4 @@ go build -o hako
|
||||||
|
|
||||||
- Base Dockerfile stored in `~/.config/hako/Dockerfile.base`
|
- Base Dockerfile stored in `~/.config/hako/Dockerfile.base`
|
||||||
- Language Dockerfiles in `~/.config/hako/Dockerfile.{lang}`
|
- Language Dockerfiles in `~/.config/hako/Dockerfile.{lang}`
|
||||||
- Mounts Claude config directory/file if present for seamless integration
|
- Mounts Claude config directory/file if present for seamless integration
|
||||||
|
|
43
commands.go
43
commands.go
|
@ -19,6 +19,9 @@ func initCommand(lang string) error {
|
||||||
if err := buildBaseImage(); err != nil {
|
if err := buildBaseImage(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Check base image version
|
||||||
|
checkVersionMismatch("hako-userland")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Building %s language image...\n", lang)
|
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)
|
return fmt.Errorf("image %s not found, run 'hako init %s' first", imageName, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for version mismatch
|
||||||
|
checkVersionMismatch(imageName)
|
||||||
|
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -103,7 +109,24 @@ func downCommand(container string) error {
|
||||||
|
|
||||||
func psCommand() error {
|
func psCommand() error {
|
||||||
fmt.Println("Hako containers:")
|
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 {
|
func syncCommand() error {
|
||||||
|
@ -122,7 +145,7 @@ func syncCommand() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Syncing files from container %s...\n", containerName)
|
fmt.Printf("Syncing files from container %s...\n", containerName)
|
||||||
|
|
||||||
tempDir, err := os.MkdirTemp("", "hako-sync-*")
|
tempDir, err := os.MkdirTemp("", "hako-sync-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -184,12 +207,12 @@ func copyWorkspaceToContainer(containerName string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
untrackedFiles, err := runCommandOutput("git", "ls-files", "--others", "--exclude-standard")
|
untrackedFiles, err := runCommandOutput("git", "ls-files", "--others", "--exclude-standard")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
allFiles := []string{}
|
allFiles := []string{}
|
||||||
if trackedFiles != "" {
|
if trackedFiles != "" {
|
||||||
allFiles = append(allFiles, strings.Split(trackedFiles, "\n")...)
|
allFiles = append(allFiles, strings.Split(trackedFiles, "\n")...)
|
||||||
|
@ -197,17 +220,17 @@ func copyWorkspaceToContainer(containerName string) error {
|
||||||
if untrackedFiles != "" {
|
if untrackedFiles != "" {
|
||||||
allFiles = append(allFiles, strings.Split(untrackedFiles, "\n")...)
|
allFiles = append(allFiles, strings.Split(untrackedFiles, "\n")...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allFiles) == 0 {
|
if len(allFiles) == 0 {
|
||||||
return fmt.Errorf("no files to copy")
|
return fmt.Errorf("no files to copy")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy files one by one to preserve directory structure
|
// Copy files one by one to preserve directory structure
|
||||||
for _, file := range allFiles {
|
for _, file := range allFiles {
|
||||||
if file == "" {
|
if file == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directory structure in container if needed
|
// Create directory structure in container if needed
|
||||||
dir := filepath.Dir(file)
|
dir := filepath.Dir(file)
|
||||||
if dir != "." {
|
if dir != "." {
|
||||||
|
@ -215,12 +238,12 @@ func copyWorkspaceToContainer(containerName string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the file
|
// Copy the file
|
||||||
if err := copyToContainer(containerName, file, "/workspace/"+file); err != nil {
|
if err := copyToContainer(containerName, file, "/workspace/"+file); err != nil {
|
||||||
return fmt.Errorf("failed to copy %s: %v", file, err)
|
return fmt.Errorf("failed to copy %s: %v", file, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
38
docker.go
38
docker.go
|
@ -28,8 +28,8 @@ func ensureConfigDir() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBaseDockerfile() string {
|
func getBaseDockerfile() string {
|
||||||
return fmt.Sprintf(`# HAKO-VERSION=%s
|
return fmt.Sprintf(`FROM debian:trixie-20250610
|
||||||
FROM debian:trixie-20250610
|
LABEL HAKO-VERSION=%s
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
|
@ -57,17 +57,17 @@ CMD ["/usr/bin/fish"]`, hakoVersion)
|
||||||
func getLanguageDockerfile(lang string) string {
|
func getLanguageDockerfile(lang string) string {
|
||||||
switch lang {
|
switch lang {
|
||||||
case "go":
|
case "go":
|
||||||
return fmt.Sprintf(`# HAKO-VERSION=%s
|
return fmt.Sprintf(`FROM hako-userland
|
||||||
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 -
|
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}"
|
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||||
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
CMD ["/bin/bash"]`, hakoVersion)
|
CMD ["/usr/bin/fish"]`, hakoVersion)
|
||||||
case "py", "python":
|
case "py", "python":
|
||||||
return fmt.Sprintf(`# HAKO-VERSION=%s
|
return fmt.Sprintf(`FROM hako-userland
|
||||||
FROM hako-userland
|
LABEL HAKO-VERSION=%s
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
|
@ -78,7 +78,7 @@ RUN apt-get update && apt-get install -y \
|
||||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||||
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
CMD ["/bin/bash"]`, hakoVersion)
|
CMD ["/usr/bin/fish"]`, hakoVersion)
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -148,7 +148,6 @@ func createContainer(containerName, imageName, workspaceDir string) error {
|
||||||
// Mount workspace
|
// Mount workspace
|
||||||
args = append(args, "-v", workspaceDir+":/workspace")
|
args = append(args, "-v", workspaceDir+":/workspace")
|
||||||
|
|
||||||
|
|
||||||
args = append(args, "-it", imageName)
|
args = append(args, "-it", imageName)
|
||||||
|
|
||||||
if err := runCommand("docker", args...); err != nil {
|
if err := runCommand("docker", args...); err != nil {
|
||||||
|
@ -206,3 +205,24 @@ func copyFromContainer(containerName, srcPath, destPath string) error {
|
||||||
func listHakoContainers() error {
|
func listHakoContainers() error {
|
||||||
return runCommand("docker", "ps", "-a", "--filter", "name=hako-", "--format", "table {{.Names}}\\t{{.Status}}\\t{{.Image}}")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
4
git.go
4
git.go
|
@ -41,7 +41,7 @@ func getProjectContainerName(lang string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
projectName := filepath.Base(gitRoot)
|
projectName := filepath.Base(gitRoot)
|
||||||
|
|
||||||
var pathComponents []string
|
var pathComponents []string
|
||||||
if relPath != "." {
|
if relPath != "." {
|
||||||
pathComponents = strings.Split(relPath, string(os.PathSeparator))
|
pathComponents = strings.Split(relPath, string(os.PathSeparator))
|
||||||
|
@ -65,4 +65,4 @@ func sanitizeName(name string) string {
|
||||||
result = strings.ReplaceAll(result, ".", "_")
|
result = strings.ReplaceAll(result, ".", "_")
|
||||||
result = strings.ReplaceAll(result, "-", "_")
|
result = strings.ReplaceAll(result, "-", "_")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
2
main.go
2
main.go
|
@ -96,4 +96,4 @@ func main() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
utils.go
9
utils.go
|
@ -24,12 +24,3 @@ func runCommandOutput(name string, args ...string) (string, error) {
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(output)), nil
|
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()
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue