Skip to content

Commit daea8fc

Browse files
committed
feat: add dotfiles, oh-my-zsh, and macOS preferences setup
1 parent 1210de4 commit daea8fc

10 files changed

Lines changed: 575 additions & 18 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
1-
name: Deploy to Cloudflare Pages
1+
name: Deploy to Cloudflare Workers
22

33
on:
44
push:
55
branches:
66
- main
77
paths:
88
- 'website/**'
9+
- 'worker/**'
10+
- 'wrangler.toml'
911
- '.github/workflows/deploy.yml'
1012
workflow_dispatch:
1113

1214
jobs:
1315
deploy:
1416
runs-on: ubuntu-latest
15-
permissions:
16-
contents: read
17-
deployments: write
1817

1918
steps:
2019
- name: Checkout
2120
uses: actions/checkout@v4
2221

23-
- name: Deploy to Cloudflare Pages
22+
- name: Deploy to Cloudflare Workers
2423
uses: cloudflare/wrangler-action@v3
2524
with:
2625
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
2726
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
28-
command: pages deploy website --project-name=openboot --branch=main

internal/dotfiles/dotfiles.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package dotfiles
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"strings"
9+
)
10+
11+
const defaultDotfilesDir = ".dotfiles"
12+
13+
func Clone(repoURL string, dryRun bool) error {
14+
if repoURL == "" {
15+
return nil
16+
}
17+
18+
home, _ := os.UserHomeDir()
19+
dotfilesPath := filepath.Join(home, defaultDotfilesDir)
20+
21+
if _, err := os.Stat(dotfilesPath); err == nil {
22+
fmt.Printf("Dotfiles already exist at %s, skipping clone\n", dotfilesPath)
23+
return nil
24+
}
25+
26+
if dryRun {
27+
fmt.Printf("[DRY-RUN] Would clone %s to %s\n", repoURL, dotfilesPath)
28+
return nil
29+
}
30+
31+
cmd := exec.Command("git", "clone", repoURL, dotfilesPath)
32+
cmd.Stdout = os.Stdout
33+
cmd.Stderr = os.Stderr
34+
return cmd.Run()
35+
}
36+
37+
func Link(dryRun bool) error {
38+
home, _ := os.UserHomeDir()
39+
dotfilesPath := filepath.Join(home, defaultDotfilesDir)
40+
41+
if _, err := os.Stat(dotfilesPath); os.IsNotExist(err) {
42+
return fmt.Errorf("dotfiles directory not found: %s", dotfilesPath)
43+
}
44+
45+
if hasStowPackages(dotfilesPath) {
46+
return linkWithStow(dotfilesPath, dryRun)
47+
}
48+
49+
return linkDirect(dotfilesPath, dryRun)
50+
}
51+
52+
func hasStowPackages(dotfilesPath string) bool {
53+
entries, err := os.ReadDir(dotfilesPath)
54+
if err != nil {
55+
return false
56+
}
57+
58+
for _, entry := range entries {
59+
if entry.IsDir() && !strings.HasPrefix(entry.Name(), ".") {
60+
pkgPath := filepath.Join(dotfilesPath, entry.Name())
61+
subEntries, _ := os.ReadDir(pkgPath)
62+
for _, sub := range subEntries {
63+
if strings.HasPrefix(sub.Name(), ".") {
64+
return true
65+
}
66+
}
67+
}
68+
}
69+
return false
70+
}
71+
72+
func linkWithStow(dotfilesPath string, dryRun bool) error {
73+
entries, err := os.ReadDir(dotfilesPath)
74+
if err != nil {
75+
return err
76+
}
77+
78+
home, _ := os.UserHomeDir()
79+
80+
for _, entry := range entries {
81+
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
82+
continue
83+
}
84+
85+
pkg := entry.Name()
86+
if dryRun {
87+
fmt.Printf("[DRY-RUN] Would stow package: %s\n", pkg)
88+
continue
89+
}
90+
91+
cmd := exec.Command("stow", "-v", "-t", home, pkg)
92+
cmd.Dir = dotfilesPath
93+
cmd.Stdout = os.Stdout
94+
cmd.Stderr = os.Stderr
95+
if err := cmd.Run(); err != nil {
96+
fmt.Printf("Warning: failed to stow %s: %v\n", pkg, err)
97+
}
98+
}
99+
100+
return nil
101+
}
102+
103+
func linkDirect(dotfilesPath string, dryRun bool) error {
104+
home, _ := os.UserHomeDir()
105+
106+
entries, err := os.ReadDir(dotfilesPath)
107+
if err != nil {
108+
return err
109+
}
110+
111+
for _, entry := range entries {
112+
name := entry.Name()
113+
if name == ".git" || name == "README.md" || name == "LICENSE" {
114+
continue
115+
}
116+
117+
src := filepath.Join(dotfilesPath, name)
118+
dst := filepath.Join(home, name)
119+
120+
if dryRun {
121+
fmt.Printf("[DRY-RUN] Would symlink %s -> %s\n", dst, src)
122+
continue
123+
}
124+
125+
if _, err := os.Lstat(dst); err == nil {
126+
backupPath := dst + ".openboot.bak"
127+
if err := os.Rename(dst, backupPath); err != nil {
128+
fmt.Printf("Warning: failed to backup %s: %v\n", dst, err)
129+
continue
130+
}
131+
fmt.Printf("Backed up: %s -> %s\n", dst, backupPath)
132+
}
133+
134+
if err := os.Symlink(src, dst); err != nil {
135+
fmt.Printf("Warning: failed to symlink %s: %v\n", name, err)
136+
} else {
137+
fmt.Printf("Linked: %s -> %s\n", dst, src)
138+
}
139+
}
140+
141+
return nil
142+
}
143+
144+
func GetDotfilesURL() string {
145+
return os.Getenv("OPENBOOT_DOTFILES")
146+
}

internal/installer/installer.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66

77
"github.com/fullstackjam/openboot/internal/brew"
88
"github.com/fullstackjam/openboot/internal/config"
9+
"github.com/fullstackjam/openboot/internal/dotfiles"
10+
"github.com/fullstackjam/openboot/internal/macos"
11+
"github.com/fullstackjam/openboot/internal/shell"
912
"github.com/fullstackjam/openboot/internal/system"
1013
"github.com/fullstackjam/openboot/internal/ui"
1114
)
@@ -44,6 +47,18 @@ func runInstall(cfg *config.Config) error {
4447
return err
4548
}
4649

50+
if err := stepDotfiles(cfg); err != nil {
51+
ui.Error(fmt.Sprintf("Dotfiles setup failed: %v", err))
52+
}
53+
54+
if err := stepShell(cfg); err != nil {
55+
ui.Error(fmt.Sprintf("Shell setup failed: %v", err))
56+
}
57+
58+
if err := stepMacOS(cfg); err != nil {
59+
ui.Error(fmt.Sprintf("macOS configuration failed: %v", err))
60+
}
61+
4762
showCompletion(cfg)
4863
return nil
4964
}
@@ -156,6 +171,151 @@ func stepConfirmAndInstall(cfg *config.Config) error {
156171
return nil
157172
}
158173

174+
func stepDotfiles(cfg *config.Config) error {
175+
if cfg.Dotfiles == "skip" {
176+
return nil
177+
}
178+
179+
ui.Header("Step 4: Dotfiles")
180+
fmt.Println()
181+
182+
dotfilesURL := dotfiles.GetDotfilesURL()
183+
184+
if cfg.Dotfiles == "" && dotfilesURL == "" {
185+
if cfg.Silent || (cfg.DryRun && !system.HasTTY()) {
186+
ui.Muted("Skipping dotfiles (no URL provided)")
187+
fmt.Println()
188+
return nil
189+
}
190+
191+
setup, err := ui.Confirm("Do you have a dotfiles repository to set up?", false)
192+
if err != nil {
193+
return err
194+
}
195+
if !setup {
196+
ui.Muted("Skipping dotfiles setup")
197+
fmt.Println()
198+
return nil
199+
}
200+
201+
dotfilesURL, err = ui.Input("Dotfiles repository URL", "https://github.com/username/dotfiles")
202+
if err != nil {
203+
return err
204+
}
205+
}
206+
207+
if dotfilesURL != "" {
208+
if err := dotfiles.Clone(dotfilesURL, cfg.DryRun); err != nil {
209+
return err
210+
}
211+
}
212+
213+
if cfg.Dotfiles == "link" || cfg.Dotfiles == "" {
214+
if err := dotfiles.Link(cfg.DryRun); err != nil {
215+
return err
216+
}
217+
}
218+
219+
if !cfg.DryRun {
220+
ui.Success("Dotfiles configured")
221+
}
222+
fmt.Println()
223+
return nil
224+
}
225+
226+
func stepShell(cfg *config.Config) error {
227+
if cfg.Shell == "skip" {
228+
return nil
229+
}
230+
231+
ui.Header("Step 5: Shell Configuration")
232+
fmt.Println()
233+
234+
if cfg.Shell == "" {
235+
if cfg.Silent || (cfg.DryRun && !system.HasTTY()) {
236+
cfg.Shell = "install"
237+
} else {
238+
install, err := ui.Confirm("Install Oh-My-Zsh and configure shell?", true)
239+
if err != nil {
240+
return err
241+
}
242+
if !install {
243+
ui.Muted("Skipping shell configuration")
244+
fmt.Println()
245+
return nil
246+
}
247+
cfg.Shell = "install"
248+
}
249+
}
250+
251+
if cfg.Shell == "install" {
252+
if shell.IsOhMyZshInstalled() {
253+
ui.Muted("Oh-My-Zsh already installed")
254+
} else {
255+
if err := shell.InstallOhMyZsh(cfg.DryRun); err != nil {
256+
return fmt.Errorf("failed to install Oh-My-Zsh: %w", err)
257+
}
258+
if !cfg.DryRun {
259+
ui.Success("Oh-My-Zsh installed")
260+
}
261+
}
262+
263+
if err := shell.ConfigureZshrc(cfg.DryRun); err != nil {
264+
return fmt.Errorf("failed to configure .zshrc: %w", err)
265+
}
266+
if !cfg.DryRun {
267+
ui.Success("Shell aliases configured")
268+
}
269+
}
270+
271+
fmt.Println()
272+
return nil
273+
}
274+
275+
func stepMacOS(cfg *config.Config) error {
276+
if cfg.Macos == "skip" {
277+
return nil
278+
}
279+
280+
ui.Header("Step 6: macOS Preferences")
281+
fmt.Println()
282+
283+
if cfg.Macos == "" {
284+
if cfg.Silent || (cfg.DryRun && !system.HasTTY()) {
285+
cfg.Macos = "configure"
286+
} else {
287+
configure, err := ui.Confirm("Apply developer-friendly macOS preferences?", true)
288+
if err != nil {
289+
return err
290+
}
291+
if !configure {
292+
ui.Muted("Skipping macOS preferences")
293+
fmt.Println()
294+
return nil
295+
}
296+
cfg.Macos = "configure"
297+
}
298+
}
299+
300+
if cfg.Macos == "configure" {
301+
if err := macos.CreateScreenshotsDir(cfg.DryRun); err != nil {
302+
ui.Error(fmt.Sprintf("Failed to create Screenshots dir: %v", err))
303+
}
304+
305+
if err := macos.Configure(macos.DefaultPreferences, cfg.DryRun); err != nil {
306+
return err
307+
}
308+
309+
if !cfg.DryRun {
310+
ui.Success("macOS preferences configured")
311+
macos.RestartAffectedApps(cfg.DryRun)
312+
}
313+
}
314+
315+
fmt.Println()
316+
return nil
317+
}
318+
159319
func showCompletion(cfg *config.Config) {
160320
preset, _ := config.GetPreset(cfg.Preset)
161321

0 commit comments

Comments
 (0)