initial commit (second try)
This commit is contained in:
parent
a62cf65376
commit
ed3c0eae14
7 changed files with 656 additions and 0 deletions
226
commands.go
Normal file
226
commands.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initCommand(lang string) error {
|
||||||
|
if lang == "" {
|
||||||
|
fmt.Println("Building base hako image...")
|
||||||
|
return buildBaseImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !imageExists("hako-userland") {
|
||||||
|
fmt.Println("Base image not found, building it first...")
|
||||||
|
if err := buildBaseImage(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Building %s language image...\n", lang)
|
||||||
|
return buildLanguageImage(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upCommand(lang string) error {
|
||||||
|
containerName, err := getProjectContainerName(lang)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageName := fmt.Sprintf("hako-userland-%s", lang)
|
||||||
|
if !imageExists(imageName) {
|
||||||
|
return fmt.Errorf("image %s not found, run 'hako init %s' first", imageName, lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerExists(containerName) {
|
||||||
|
if !containerIsRunning(containerName) {
|
||||||
|
fmt.Printf("Starting existing container %s...\n", containerName)
|
||||||
|
if err := startContainer(containerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Container %s is already running\n", containerName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Creating new container %s...\n", containerName)
|
||||||
|
if err := createContainer(containerName, imageName, cwd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := startContainer(containerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Copying workspace to container...\n")
|
||||||
|
if err := copyWorkspaceToContainer(containerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Entering container %s...\n", containerName)
|
||||||
|
return execContainer(containerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downCommand(container string) error {
|
||||||
|
var containerName string
|
||||||
|
|
||||||
|
if container != "" {
|
||||||
|
containerName = container
|
||||||
|
} else {
|
||||||
|
// Find any hako container for current project
|
||||||
|
output, err := runCommandOutput("docker", "ps", "-a", "--filter", "name=hako-", "--format", "{{.Names}}")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
containers := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
|
if len(containers) == 0 || containers[0] == "" {
|
||||||
|
return fmt.Errorf("no hako containers found for current project")
|
||||||
|
}
|
||||||
|
containerName = containers[0] // Use first found container
|
||||||
|
}
|
||||||
|
|
||||||
|
if !containerExists(containerName) {
|
||||||
|
return fmt.Errorf("container %s does not exist", containerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Stopping container %s...\n", containerName)
|
||||||
|
if containerIsRunning(containerName) {
|
||||||
|
if err := stopContainer(containerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Removing container %s...\n", containerName)
|
||||||
|
return removeContainer(containerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func psCommand() error {
|
||||||
|
fmt.Println("Hako containers:")
|
||||||
|
return listHakoContainers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncCommand() error {
|
||||||
|
output, err := runCommandOutput("docker", "ps", "-a", "--filter", "name=hako-", "--format", "{{.Names}}")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
containers := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
|
if len(containers) == 0 || containers[0] == "" {
|
||||||
|
return fmt.Errorf("no hako containers found for current project")
|
||||||
|
}
|
||||||
|
containerName := containers[0] // Use first found container
|
||||||
|
|
||||||
|
if !containerExists(containerName) {
|
||||||
|
return fmt.Errorf("container %s does not exist", containerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Syncing files from container %s...\n", containerName)
|
||||||
|
|
||||||
|
tempDir, err := os.MkdirTemp("", "hako-sync-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
if err := copyFromContainer(containerName, "/workspace/.", tempDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncFiles(tempDir, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncFiles(srcDir, destDir string) error {
|
||||||
|
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(srcDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if relPath == ".git" || relPath == ".gitignore" {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(filepath.Base(relPath), ".git") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
destPath := filepath.Join(destDir, relPath)
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return os.MkdirAll(destPath, info.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, srcFile)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyWorkspaceToContainer(containerName string) error {
|
||||||
|
// Get all files that should be copied (tracked + untracked, excluding ignored)
|
||||||
|
trackedFiles, err := runCommandOutput("git", "ls-files")
|
||||||
|
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")...)
|
||||||
|
}
|
||||||
|
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 != "." {
|
||||||
|
if err := runCommand("docker", "exec", containerName, "mkdir", "-p", "/workspace/"+dir); err != nil {
|
||||||
|
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
|
||||||
|
}
|
208
docker.go
Normal file
208
docker.go
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
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(`# HAKO-VERSION=%s
|
||||||
|
FROM debian:bookworm
|
||||||
|
|
||||||
|
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(`# HAKO-VERSION=%s
|
||||||
|
FROM hako-userland
|
||||||
|
|
||||||
|
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)
|
||||||
|
case "py", "python":
|
||||||
|
return fmt.Sprintf(`# HAKO-VERSION=%s
|
||||||
|
FROM hako-userland
|
||||||
|
|
||||||
|
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 ["/bin/bash"]`, 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 := runCommand("docker", "image", "inspect", imageName)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerExists(containerName string) bool {
|
||||||
|
err := runCommand("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}
|
||||||
|
|
||||||
|
// Mount workspace
|
||||||
|
args = append(args, "-v", workspaceDir+":/workspace")
|
||||||
|
|
||||||
|
|
||||||
|
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}}")
|
||||||
|
}
|
68
git.go
Normal file
68
git.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isGitRepository() bool {
|
||||||
|
_, err := os.Stat(".git")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGitRootPath() (string, error) {
|
||||||
|
output, err := runCommandOutput("git", "rev-parse", "--show-toplevel")
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("not in a git repository")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProjectContainerName(lang string) (string, error) {
|
||||||
|
if !isGitRepository() {
|
||||||
|
return "", errors.New("must be run inside a git repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
gitRoot, err := getGitRootPath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(gitRoot, cwd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectName := filepath.Base(gitRoot)
|
||||||
|
|
||||||
|
var pathComponents []string
|
||||||
|
if relPath != "." {
|
||||||
|
pathComponents = strings.Split(relPath, string(os.PathSeparator))
|
||||||
|
}
|
||||||
|
|
||||||
|
containerName := "hako-" + lang + "-" + sanitizeName(projectName)
|
||||||
|
for _, component := range pathComponents {
|
||||||
|
containerName += "-" + sanitizeName(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(containerName) > 60 {
|
||||||
|
return "", errors.New("container name too long, try running from a shallower directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeName(name string) string {
|
||||||
|
result := strings.ToLower(name)
|
||||||
|
result = strings.ReplaceAll(result, " ", "_")
|
||||||
|
result = strings.ReplaceAll(result, ".", "_")
|
||||||
|
result = strings.ReplaceAll(result, "-", "_")
|
||||||
|
return result
|
||||||
|
}
|
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module hako
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/spf13/cobra v1.8.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
)
|
10
go.sum
Normal file
10
go.sum
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
99
main.go
Normal file
99
main.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "hako",
|
||||||
|
Short: "Docker container management for Claude Code",
|
||||||
|
Long: "A tool that manages Docker containers providing sandboxed environments for Claude Code",
|
||||||
|
}
|
||||||
|
|
||||||
|
var initCmd = &cobra.Command{
|
||||||
|
Use: "init [language]",
|
||||||
|
Short: "Initialize Docker images",
|
||||||
|
Long: "Build base Docker image or language-specific variants",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var lang string
|
||||||
|
if len(args) > 0 {
|
||||||
|
lang = args[0]
|
||||||
|
}
|
||||||
|
if err := initCommand(lang); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var upCmd = &cobra.Command{
|
||||||
|
Use: "up <language>",
|
||||||
|
Short: "Start and enter a container",
|
||||||
|
Long: "Create or start a container for the current project and drop into shell",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := upCommand(args[0]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var downCmd = &cobra.Command{
|
||||||
|
Use: "down [container]",
|
||||||
|
Short: "Stop a container",
|
||||||
|
Long: "Stop and remove a container",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var container string
|
||||||
|
if len(args) > 0 {
|
||||||
|
container = args[0]
|
||||||
|
}
|
||||||
|
if err := downCommand(container); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var psCmd = &cobra.Command{
|
||||||
|
Use: "ps",
|
||||||
|
Short: "List running containers",
|
||||||
|
Long: "Show all running hako containers",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := psCommand(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncCmd = &cobra.Command{
|
||||||
|
Use: "sync",
|
||||||
|
Short: "Sync files from container",
|
||||||
|
Long: "Copy files from container workspace to working directory",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := syncCommand(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(initCmd)
|
||||||
|
rootCmd.AddCommand(upCmd)
|
||||||
|
rootCmd.AddCommand(downCmd)
|
||||||
|
rootCmd.AddCommand(psCmd)
|
||||||
|
rootCmd.AddCommand(syncCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
35
utils.go
Normal file
35
utils.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runCommand(name string, args ...string) error {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
fmt.Fprintf(os.Stderr, "+ %s %s\n", name, strings.Join(args, " "))
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommandOutput(name string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
fmt.Fprintf(os.Stderr, "+ %s %s\n", name, strings.Join(args, " "))
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
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