1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
// 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
}
|