diff options
| author | hachem <im@hachem.wtf> | 2026-08-29 04:35:17 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-29 04:35:17 +0200 |
| commit | 6d44318d728da84e5018bbbdbf69dc3053ffe133 (patch) | |
| tree | af59f1ecb98720b7f975f97c372d17daff5be419 /internal | |
| parent | 2c22479b452b1fad9bc29c2a544088b662189d83 (diff) | |
add: merge command
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/commands.go | 5 | ||||
| -rw-r--r-- | internal/cli/merge.go | 247 |
2 files changed, 252 insertions, 0 deletions
diff --git a/internal/cli/commands.go b/internal/cli/commands.go index f66ff62..fc620c6 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -30,6 +30,11 @@ func defaultCommands() []command { run: runGenerate, }, { + name: "merge", + desc: "Back up the existing server and merge the generated files into it", + run: runMerge, + }, + { name: "help", desc: "Show available commands", run: func(m *model) tea.Cmd { diff --git a/internal/cli/merge.go b/internal/cli/merge.go new file mode 100644 index 0000000..fbae597 --- /dev/null +++ b/internal/cli/merge.go @@ -0,0 +1,247 @@ +package cli + +import ( + "archive/zip" + "bufio" + "compress/flate" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/hachem-wtf/benson/internal/config" +) + +func runMerge(m *model) tea.Cmd { + m.busy = true + ch := make(chan tea.Msg, 256) + m.events = ch + cfg := m.cfg + go mergeWorker(cfg, ch) + return tea.Batch(m.spin.Tick, waitForEvent(ch)) +} + +func mergeWorker(cfg *config.Config, ch chan<- tea.Msg) { + emit := func(s string) { ch <- streamMsg{s} } + done := func(s string) { ch <- streamDoneMsg{s} } + + if cfg.ServerLocation == "" || cfg.BackupLocation == "" { + done(errorStyle.Render("server_location and backup_location must be set in config.")) + return + } + if _, err := os.Stat(serverDir); err != nil { + done(errorStyle.Render("No generated files found. Run 'generate' first.")) + return + } + + emit(headingStyle.Render("Merging generated server")) + + if cfg.MergePreCommand != "" { + emit(mutedStyle.Render("Running pre-merge command…")) + if err := runShellCommand(cfg.MergePreCommand, emit); err != nil { + emit(warnStyle.Render(" pre-command exited: " + err.Error())) + } + } + + if _, err := os.Stat(cfg.ServerLocation); err == nil { + emit(mutedStyle.Render("Backing up " + cfg.ServerLocation + "…")) + name, err := createBackup(cfg.ServerLocation, cfg.BackupLocation) + if err != nil { + done(errorStyle.Render("Failed to create backup: " + err.Error())) + return + } + emit(" " + successStyle.Render("✓ ") + name) + } else { + emit(mutedStyle.Render("Server location doesn't exist, creating it…")) + if err := os.MkdirAll(cfg.ServerLocation, 0o755); err != nil { + done(errorStyle.Render("Failed to create server location: " + err.Error())) + return + } + } + + emit(mutedStyle.Render("Moving generated files into place…")) + entries, err := os.ReadDir(serverDir) + if err != nil { + done(errorStyle.Render("Failed to read generated directory: " + err.Error())) + return + } + for _, e := range entries { + src := filepath.Join(serverDir, e.Name()) + dst := filepath.Join(cfg.ServerLocation, e.Name()) + + os.RemoveAll(dst) + if err := movePath(src, dst); err != nil { + done(errorStyle.Render("Failed to move " + e.Name() + ": " + err.Error())) + return + } + emit(" " + successStyle.Render("moved ") + e.Name()) + } + os.RemoveAll(serverDir) + emit(successStyle.Render("Merge complete.")) + + if cfg.MergePostCommand != "" { + emit(mutedStyle.Render("Running post-merge command…")) + if err := runShellCommand(cfg.MergePostCommand, emit); err != nil { + emit(warnStyle.Render(" post-command exited: " + err.Error())) + } + } + + done("") +} + +func createBackup(sourceDir, backupDir string) (string, error) { + if err := os.MkdirAll(backupDir, 0o755); err != nil { + return "", err + } + + name := "backup_" + time.Now().Format("2006-01-02_15-04-05") + ".zip" + f, err := os.Create(filepath.Join(backupDir, name)) + if err != nil { + return "", err + } + defer f.Close() + + zw := zip.NewWriter(f) + zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) { + return flate.NewWriter(out, flate.BestCompression) + }) + + walkErr := filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + + rel, err := filepath.Rel(sourceDir, path) + if err != nil { + return err + } + + w, err := zw.Create(filepath.ToSlash(rel)) + if err != nil { + return err + } + + src, err := os.Open(path) + if err != nil { + return err + } + defer src.Close() + + _, err = io.Copy(w, src) + return err + }) + if walkErr != nil { + zw.Close() + return "", walkErr + } + + if err := zw.Close(); err != nil { + return "", err + } + return name, nil +} + +func runShellCommand(command string, emit func(string)) error { + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/C", command) + } else { + cmd = exec.Command("sh", "-c", command) + } + + pr, pw, err := os.Pipe() + if err != nil { + return err + } + cmd.Stdout = pw + cmd.Stderr = pw + + if err := cmd.Start(); err != nil { + pw.Close() + return err + } + pw.Close() + + scanner := bufio.NewScanner(pr) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for scanner.Scan() { + emit(mutedStyle.Render(" │ " + scanner.Text())) + } + return cmd.Wait() +} + +func movePath(src, dst string) error { + if err := os.Rename(src, dst); err == nil { + return nil + } + + info, err := os.Stat(src) + if err != nil { + return err + } + + if info.IsDir() { + if err := copyDir(src, dst); err != nil { + return err + } + } else if err := copyFile(src, dst, info.Mode()); err != nil { + return err + } + return os.RemoveAll(src) +} + +func copyDir(src, dst string) error { + if err := os.MkdirAll(dst, 0o755); err != nil { + return err + } + + entries, err := os.ReadDir(src) + if err != nil { + return err + } + for _, e := range entries { + s := filepath.Join(src, e.Name()) + d := filepath.Join(dst, e.Name()) + + if e.IsDir() { + if err := copyDir(s, d); err != nil { + return err + } + continue + } + + info, err := e.Info() + if err != nil { + return err + } + if err := copyFile(s, d, info.Mode()); err != nil { + return err + } + } + return nil +} + +func copyFile(src, dst string, mode os.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} |
