summaryrefslogtreecommitdiffstats
path: root/internal/state/state.go
blob: 324fd34991a5b3bd3330f544e58021818abc8864 (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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package state

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
)

// State represents the entire local deployment state
type State struct {
	DefaultHost string           `json:"default_host,omitempty"`
	Hosts       map[string]*Host `json:"hosts"`
}

// Host represents deployment state for a single VPS
type Host struct {
	NextPort   int             `json:"next_port"`
	BaseDomain string          `json:"base_domain,omitempty"`
	GitSetup   bool            `json:"git_setup,omitempty"`
	Apps       map[string]*App `json:"apps"`
}

// App represents a deployed application or static site
type App struct {
	Type   string            `json:"type"`             // "app", "static", "git-app", or "git-static"
	Domain string            `json:"domain"`
	Port   int               `json:"port,omitempty"`   // only for type="app" or "git-app"
	Repo   string            `json:"repo,omitempty"`   // only for git types, e.g. "/srv/git/foo.git"
	Env    map[string]string `json:"env,omitempty"`    // only for type="app" or "git-app"
	Args   string            `json:"args,omitempty"`   // only for type="app"
	Files  []string          `json:"files,omitempty"`  // only for type="app"
	Memory string            `json:"memory,omitempty"` // only for type="app"
	CPU    string            `json:"cpu,omitempty"`    // only for type="app"
}

const (
	startPort = 8001
)

// Load reads state from ~/.config/ship/state.json
func Load() (*State, error) {
	path := statePath()

	// If file doesn't exist, return empty state
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return &State{
			Hosts: make(map[string]*Host),
		}, nil
	}

	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read state file: %w", err)
	}

	var state State
	if err := json.Unmarshal(data, &state); err != nil {
		return nil, fmt.Errorf("failed to parse state file: %w", err)
	}

	// Initialize maps if nil
	if state.Hosts == nil {
		state.Hosts = make(map[string]*Host)
	}

	return &state, nil
}

// Save writes state to ~/.config/ship/state.json
func (s *State) Save() error {
	path := statePath()

	// Ensure directory exists
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	data, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal state: %w", err)
	}

	if err := os.WriteFile(path, data, 0600); err != nil {
		return fmt.Errorf("failed to write state file: %w", err)
	}

	return nil
}

// GetHost returns the host state, creating it if it doesn't exist
func (s *State) GetHost(host string) *Host {
	if s.Hosts[host] == nil {
		s.Hosts[host] = &Host{
			NextPort: startPort,
			Apps:     make(map[string]*App),
		}
	}
	if s.Hosts[host].Apps == nil {
		s.Hosts[host].Apps = make(map[string]*App)
	}
	return s.Hosts[host]
}

// AllocatePort returns the next available port for a host
func (s *State) AllocatePort(host string) int {
	h := s.GetHost(host)
	port := h.NextPort
	h.NextPort++
	return port
}

// AddApp adds or updates an app in the state
func (s *State) AddApp(host, name string, app *App) {
	h := s.GetHost(host)
	h.Apps[name] = app
}

// RemoveApp removes an app from the state
func (s *State) RemoveApp(host, name string) error {
	h := s.GetHost(host)
	if _, exists := h.Apps[name]; !exists {
		return fmt.Errorf("app %s not found", name)
	}
	delete(h.Apps, name)
	return nil
}

// GetApp returns an app from the state
func (s *State) GetApp(host, name string) (*App, error) {
	h := s.GetHost(host)
	app, exists := h.Apps[name]
	if !exists {
		return nil, fmt.Errorf("app %s not found", name)
	}
	return app, nil
}

// ListApps returns all apps for a host
func (s *State) ListApps(host string) map[string]*App {
	h := s.GetHost(host)
	return h.Apps
}

// GetDefaultHost returns the default host, or empty string if not set
func (s *State) GetDefaultHost() string {
	return s.DefaultHost
}

// SetDefaultHost sets the default host
func (s *State) SetDefaultHost(host string) {
	s.DefaultHost = host
}

// statePath returns the path to the state file
func statePath() string {
	home, err := os.UserHomeDir()
	if err != nil {
		// Fallback to current directory (should rarely happen)
		return ".ship-state.json"
	}
	return filepath.Join(home, ".config", "ship", "state.json")
}