aboutsummaryrefslogtreecommitdiffstats
path: root/internal/state/state.go
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-04-18 14:40:17 -0700
committerClawd <ai@clawd.bot>2026-04-18 14:40:17 -0700
commit778bef5ee6941056e06326d1eaaa6956d7307a85 (patch)
tree23b85f32fb69f85078b3debec08c1353694def6f /internal/state/state.go
parenteb76b1f6e1697ef170fc45d25e81b21679ea7b0d (diff)
Remove Go implementation — ship is skills-only nowmain
The skills/ directory fully replaces the old Go CLI. Drop all Go source, build files, planning docs, and the stale SECURITY.md (which described the old git-user push-deploy model that no longer exists). Trim .gitignore to match the new tree.
Diffstat (limited to 'internal/state/state.go')
-rw-r--r--internal/state/state.go106
1 files changed, 0 insertions, 106 deletions
diff --git a/internal/state/state.go b/internal/state/state.go
deleted file mode 100644
index 9b06179..0000000
--- a/internal/state/state.go
+++ /dev/null
@@ -1,106 +0,0 @@
1package state
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "regexp"
9)
10
11// State represents the local ship configuration
12type State struct {
13 DefaultHost string `json:"default_host,omitempty"`
14 Hosts map[string]*Host `json:"hosts"`
15}
16
17// Host represents configuration for a single VPS
18type Host struct {
19 BaseDomain string `json:"base_domain,omitempty"`
20 GitSetup bool `json:"git_setup,omitempty"`
21}
22
23var validName = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
24
25// ValidateName checks that a name is safe for use in shell commands,
26// file paths, systemd units, and DNS labels.
27func ValidateName(name string) error {
28 if !validName.MatchString(name) {
29 return fmt.Errorf("invalid name %q: must start with a lowercase letter, contain only lowercase letters, digits, and hyphens, and be 1-63 characters", name)
30 }
31 return nil
32}
33
34// Load reads state from ~/.config/ship/state.json
35func Load() (*State, error) {
36 path := statePath()
37
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 if state.Hosts == nil {
55 state.Hosts = make(map[string]*Host)
56 }
57
58 return &state, nil
59}
60
61// Save writes state to ~/.config/ship/state.json
62func (s *State) Save() error {
63 path := statePath()
64
65 dir := filepath.Dir(path)
66 if err := os.MkdirAll(dir, 0755); err != nil {
67 return fmt.Errorf("failed to create config directory: %w", err)
68 }
69
70 data, err := json.MarshalIndent(s, "", " ")
71 if err != nil {
72 return fmt.Errorf("failed to marshal state: %w", err)
73 }
74
75 if err := os.WriteFile(path, data, 0600); err != nil {
76 return fmt.Errorf("failed to write state file: %w", err)
77 }
78
79 return nil
80}
81
82// GetHost returns the host config, creating it if it doesn't exist
83func (s *State) GetHost(host string) *Host {
84 if s.Hosts[host] == nil {
85 s.Hosts[host] = &Host{}
86 }
87 return s.Hosts[host]
88}
89
90// GetDefaultHost returns the default host, or empty string if not set
91func (s *State) GetDefaultHost() string {
92 return s.DefaultHost
93}
94
95// SetDefaultHost sets the default host
96func (s *State) SetDefaultHost(host string) {
97 s.DefaultHost = host
98}
99
100func statePath() string {
101 home, err := os.UserHomeDir()
102 if err != nil {
103 return ".ship-state.json"
104 }
105 return filepath.Join(home, ".config", "ship", "state.json")
106}