// Package detect provides auto-detection of project types. package detect import ( "os" "path/filepath" "github.com/bdw/ship/internal/output" ) // ProjectType represents the detected deployment type type ProjectType string const ( TypeStatic ProjectType = "static" TypeDocker ProjectType = "docker" TypeBinary ProjectType = "binary" ) // Result is the detection outcome type Result struct { Type ProjectType Path string // Absolute path to deploy Error *output.ErrorResponse } // Detect examines a path and determines how to deploy it. // Follows the logic from SPEC.md: // - File: must be executable → binary // - Directory with Dockerfile → docker // - Directory with index.html → static // - Go/Node without Dockerfile → error with guidance // - Empty or unknown → error func Detect(path string) Result { absPath, err := filepath.Abs(path) if err != nil { return Result{Error: output.Err(output.ErrInvalidPath, "cannot resolve path: "+err.Error())} } info, err := os.Stat(absPath) if err != nil { if os.IsNotExist(err) { return Result{Error: output.Err(output.ErrInvalidPath, "path does not exist: "+path)} } return Result{Error: output.Err(output.ErrInvalidPath, "cannot access path: "+err.Error())} } // File: must be executable binary if !info.IsDir() { return detectFile(absPath, info) } // Directory: check contents return detectDirectory(absPath) } func detectFile(path string, info os.FileInfo) Result { // Check if executable if info.Mode()&0111 == 0 { return Result{Error: output.Err(output.ErrUnknownProjectType, "file is not executable")} } return Result{Type: TypeBinary, Path: path} } func detectDirectory(path string) Result { // Check for Dockerfile first (highest priority) if hasFile(path, "Dockerfile") { return Result{Type: TypeDocker, Path: path} } // Check for static site if hasFile(path, "index.html") || hasFile(path, "index.htm") { return Result{Type: TypeStatic, Path: path} } // Check for Go project without Dockerfile if hasFile(path, "go.mod") { return Result{Error: output.Err(output.ErrUnknownProjectType, "Go project without Dockerfile. Add a Dockerfile or build a binary first.")} } // Check for Node project without Dockerfile if hasFile(path, "package.json") { return Result{Error: output.Err(output.ErrUnknownProjectType, "Node project without Dockerfile. Add a Dockerfile.")} } // Check if empty entries, err := os.ReadDir(path) if err != nil { return Result{Error: output.Err(output.ErrInvalidPath, "cannot read directory: "+err.Error())} } if len(entries) == 0 { return Result{Error: output.Err(output.ErrInvalidPath, "directory is empty")} } // Unknown return Result{Error: output.Err(output.ErrUnknownProjectType, "cannot detect project type. Add a Dockerfile or index.html.")} } func hasFile(dir, name string) bool { _, err := os.Stat(filepath.Join(dir, name)) return err == nil }