summaryrefslogtreecommitdiffstats
path: root/internal/detect
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-15 18:47:15 -0800
committerClawd <ai@clawd.bot>2026-02-15 18:47:15 -0800
commit5b8893550130ad8ffe39a6523a11994757493691 (patch)
tree123b50d2f0747078d3b8d1649be2ada25e2f177a /internal/detect
parentf1d6907f098e1fe66a6b2f9e89abfe70707b53a9 (diff)
feat(v2): add output and detect packages
- internal/output: JSON response types, error codes, exit codes, pretty output - internal/detect: auto-detection of project type (static/docker/binary) - PROGRESS.md: track rebuild progress Foundation for agent-first JSON interface per SPEC.md
Diffstat (limited to 'internal/detect')
-rw-r--r--internal/detect/detect.go105
1 files changed, 105 insertions, 0 deletions
diff --git a/internal/detect/detect.go b/internal/detect/detect.go
new file mode 100644
index 0000000..f3efd22
--- /dev/null
+++ b/internal/detect/detect.go
@@ -0,0 +1,105 @@
1// Package detect provides auto-detection of project types.
2package detect
3
4import (
5 "os"
6 "path/filepath"
7
8 "github.com/bdw/ship/internal/output"
9)
10
11// ProjectType represents the detected deployment type
12type ProjectType string
13
14const (
15 TypeStatic ProjectType = "static"
16 TypeDocker ProjectType = "docker"
17 TypeBinary ProjectType = "binary"
18)
19
20// Result is the detection outcome
21type Result struct {
22 Type ProjectType
23 Path string // Absolute path to deploy
24 Error *output.ErrorResponse
25}
26
27// Detect examines a path and determines how to deploy it.
28// Follows the logic from SPEC.md:
29// - File: must be executable → binary
30// - Directory with Dockerfile → docker
31// - Directory with index.html → static
32// - Go/Node without Dockerfile → error with guidance
33// - Empty or unknown → error
34func Detect(path string) Result {
35 absPath, err := filepath.Abs(path)
36 if err != nil {
37 return Result{Error: output.Err(output.ErrInvalidPath, "cannot resolve path: "+err.Error())}
38 }
39
40 info, err := os.Stat(absPath)
41 if err != nil {
42 if os.IsNotExist(err) {
43 return Result{Error: output.Err(output.ErrInvalidPath, "path does not exist: "+path)}
44 }
45 return Result{Error: output.Err(output.ErrInvalidPath, "cannot access path: "+err.Error())}
46 }
47
48 // File: must be executable binary
49 if !info.IsDir() {
50 return detectFile(absPath, info)
51 }
52
53 // Directory: check contents
54 return detectDirectory(absPath)
55}
56
57func detectFile(path string, info os.FileInfo) Result {
58 // Check if executable
59 if info.Mode()&0111 == 0 {
60 return Result{Error: output.Err(output.ErrUnknownProjectType, "file is not executable")}
61 }
62 return Result{Type: TypeBinary, Path: path}
63}
64
65func detectDirectory(path string) Result {
66 // Check for Dockerfile first (highest priority)
67 if hasFile(path, "Dockerfile") {
68 return Result{Type: TypeDocker, Path: path}
69 }
70
71 // Check for static site
72 if hasFile(path, "index.html") || hasFile(path, "index.htm") {
73 return Result{Type: TypeStatic, Path: path}
74 }
75
76 // Check for Go project without Dockerfile
77 if hasFile(path, "go.mod") {
78 return Result{Error: output.Err(output.ErrUnknownProjectType,
79 "Go project without Dockerfile. Add a Dockerfile or build a binary first.")}
80 }
81
82 // Check for Node project without Dockerfile
83 if hasFile(path, "package.json") {
84 return Result{Error: output.Err(output.ErrUnknownProjectType,
85 "Node project without Dockerfile. Add a Dockerfile.")}
86 }
87
88 // Check if empty
89 entries, err := os.ReadDir(path)
90 if err != nil {
91 return Result{Error: output.Err(output.ErrInvalidPath, "cannot read directory: "+err.Error())}
92 }
93 if len(entries) == 0 {
94 return Result{Error: output.Err(output.ErrInvalidPath, "directory is empty")}
95 }
96
97 // Unknown
98 return Result{Error: output.Err(output.ErrUnknownProjectType,
99 "cannot detect project type. Add a Dockerfile or index.html.")}
100}
101
102func hasFile(dir, name string) bool {
103 _, err := os.Stat(filepath.Join(dir, name))
104 return err == nil
105}