summaryrefslogtreecommitdiffstats
path: root/internal/state
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2025-12-28 09:21:08 -0800
committerbndw <ben@bdw.to>2025-12-28 09:21:08 -0800
commit13c2f9cffa624fdf498f3b61fab9d809b92e026e (patch)
tree4b25205ccd05e9e887376c10edb2f4069ea1d9d4 /internal/state
init
Diffstat (limited to 'internal/state')
-rw-r--r--internal/state/state.go146
1 files changed, 146 insertions, 0 deletions
diff --git a/internal/state/state.go b/internal/state/state.go
new file mode 100644
index 0000000..c7c7dd4
--- /dev/null
+++ b/internal/state/state.go
@@ -0,0 +1,146 @@
1package state
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8)
9
10// State represents the entire local deployment state
11type State struct {
12 Hosts map[string]*Host `json:"hosts"`
13}
14
15// Host represents deployment state for a single VPS
16type Host struct {
17 NextPort int `json:"next_port"`
18 Apps map[string]*App `json:"apps"`
19}
20
21// App represents a deployed application or static site
22type App struct {
23 Type string `json:"type"` // "app" or "static"
24 Domain string `json:"domain"`
25 Port int `json:"port,omitempty"` // only for type="app"
26 Env map[string]string `json:"env,omitempty"` // only for type="app"
27}
28
29const (
30 startPort = 8001
31)
32
33// Load reads state from ~/.config/deploy/state.json
34func Load() (*State, error) {
35 path := statePath()
36
37 // If file doesn't exist, return empty state
38 if _, err := os.Stat(path); os.IsNotExist(err) {
39 return &State{
40 Hosts: make(map[string]*Host),
41 }, nil
42 }
43
44 data, err := os.ReadFile(path)
45 if err != nil {
46 return nil, fmt.Errorf("failed to read state file: %w", err)
47 }
48
49 var state State
50 if err := json.Unmarshal(data, &state); err != nil {
51 return nil, fmt.Errorf("failed to parse state file: %w", err)
52 }
53
54 // Initialize maps if nil
55 if state.Hosts == nil {
56 state.Hosts = make(map[string]*Host)
57 }
58
59 return &state, nil
60}
61
62// Save writes state to ~/.config/deploy/state.json
63func (s *State) Save() error {
64 path := statePath()
65
66 // Ensure directory exists
67 dir := filepath.Dir(path)
68 if err := os.MkdirAll(dir, 0755); err != nil {
69 return fmt.Errorf("failed to create config directory: %w", err)
70 }
71
72 data, err := json.MarshalIndent(s, "", " ")
73 if err != nil {
74 return fmt.Errorf("failed to marshal state: %w", err)
75 }
76
77 if err := os.WriteFile(path, data, 0600); err != nil {
78 return fmt.Errorf("failed to write state file: %w", err)
79 }
80
81 return nil
82}
83
84// GetHost returns the host state, creating it if it doesn't exist
85func (s *State) GetHost(host string) *Host {
86 if s.Hosts[host] == nil {
87 s.Hosts[host] = &Host{
88 NextPort: startPort,
89 Apps: make(map[string]*App),
90 }
91 }
92 if s.Hosts[host].Apps == nil {
93 s.Hosts[host].Apps = make(map[string]*App)
94 }
95 return s.Hosts[host]
96}
97
98// AllocatePort returns the next available port for a host
99func (s *State) AllocatePort(host string) int {
100 h := s.GetHost(host)
101 port := h.NextPort
102 h.NextPort++
103 return port
104}
105
106// AddApp adds or updates an app in the state
107func (s *State) AddApp(host, name string, app *App) {
108 h := s.GetHost(host)
109 h.Apps[name] = app
110}
111
112// RemoveApp removes an app from the state
113func (s *State) RemoveApp(host, name string) error {
114 h := s.GetHost(host)
115 if _, exists := h.Apps[name]; !exists {
116 return fmt.Errorf("app %s not found", name)
117 }
118 delete(h.Apps, name)
119 return nil
120}
121
122// GetApp returns an app from the state
123func (s *State) GetApp(host, name string) (*App, error) {
124 h := s.GetHost(host)
125 app, exists := h.Apps[name]
126 if !exists {
127 return nil, fmt.Errorf("app %s not found", name)
128 }
129 return app, nil
130}
131
132// ListApps returns all apps for a host
133func (s *State) ListApps(host string) map[string]*App {
134 h := s.GetHost(host)
135 return h.Apps
136}
137
138// statePath returns the path to the state file
139func statePath() string {
140 home, err := os.UserHomeDir()
141 if err != nil {
142 // Fallback to current directory (should rarely happen)
143 return ".deploy-state.json"
144 }
145 return filepath.Join(home, ".config", "deploy", "state.json")
146}