summaryrefslogtreecommitdiffstats
path: root/internal/detect/detect.go
blob: f3efd22417507a7f8636e7298be1580756626e0a (plain)
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
}