Files
osmedeus/pkg/cli/uninstall.go
T
j3ssie d5b5564149 feat: add queue system for delayed task execution
- Add QueuePoller for dual-source (DB + Redis) task polling with concurrent execution
- Add worker queue subcommands (list, new, run) for managing queued tasks
- Add uninstall command to safely remove Osmedeus installation
- Add input validation to prevent command injection via dangerous shell characters
- Add queue run support via --queue and --queue-run flags in osmedeus run
- Add queue polling to server with --no-queue-polling flag to disable it
- Support queuing tasks from both CLI and API with deduplication across sources
2026-02-15 18:13:57 +07:00

218 lines
6.7 KiB
Go

package cli
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/spf13/cobra"
)
var (
uninstallClean bool
)
// uninstallCmd represents the uninstall command
var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Remove Osmedeus installation (base folder, workspaces, and binary)",
Long: UsageUninstall(),
RunE: runUninstall,
}
func init() {
uninstallCmd.Flags().BoolVar(&uninstallClean, "clean", false, "also remove workspaces data (~/workspaces-osmedeus)")
}
func runUninstall(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to determine home directory: %w", err)
}
// Resolve folders to remove
baseFolderPath := filepath.Join(homeDir, "osmedeus-base")
dotOsmPath := filepath.Join(homeDir, ".osmedeus")
workspacesPath := filepath.Join(homeDir, "workspaces-osmedeus")
// If config is loaded, use its actual paths
cfg := config.Get()
if cfg != nil {
if cfg.BaseFolder != "" {
baseFolderPath = cfg.BaseFolder
}
if ws := cfg.GetWorkspacesDir(); ws != "" {
workspacesPath = ws
}
}
// Find osmedeus binaries in PATH (up to 3)
binaries := findOsmedeusBinaries(3)
// ── Print warning ──────────────────────────────────────────────
fmt.Println()
fmt.Println(terminal.BoldRed("╔════════════════════════════════════════════════════════════════╗"))
fmt.Println(terminal.BoldRed("║ WARNING: This will PERMANENTLY remove Osmedeus ║"))
fmt.Println(terminal.BoldRed("╚════════════════════════════════════════════════════════════════╝"))
fmt.Println()
fmt.Println(terminal.BoldRed("The following will be DELETED:"))
fmt.Println()
printFolderLine(printer, baseFolderPath, "base folder (settings, workflows, binaries)")
printFolderLine(printer, dotOsmPath, "initialization marker")
if uninstallClean {
printFolderLine(printer, workspacesPath, "workspaces data (scan results)")
}
if len(binaries) > 0 {
for _, bin := range binaries {
fmt.Printf(" %s %s %s\n", terminal.Red("✘"), terminal.Yellow(bin), terminal.Gray("osmedeus binary"))
}
} else {
fmt.Printf(" %s %s\n", terminal.Gray("○"), terminal.Gray("no osmedeus binary found in PATH"))
}
fmt.Println()
if !uninstallClean {
printer.Info("Workspaces folder %s will be kept (use %s to remove it too)",
terminal.Yellow(workspacesPath), terminal.Cyan("--clean"))
}
fmt.Println()
// ── Require --force ────────────────────────────────────────────
if !globalForce {
printer.Warning("This operation is IRREVERSIBLE!")
printer.Warning("Use %s %s to confirm deletion", terminal.Cyan("--force"), terminal.Cyan("--clean"))
fmt.Println()
if !confirmPrompt(terminal.BoldRed("Are you sure you want to uninstall Osmedeus?")) {
printer.Info("Uninstall cancelled.")
return nil
}
fmt.Println()
}
// ── Perform removal ────────────────────────────────────────────
removeFolderIfExists(printer, baseFolderPath, "base folder")
removeFolderIfExists(printer, dotOsmPath, ".osmedeus marker")
if uninstallClean {
removeFolderIfExists(printer, workspacesPath, "workspaces")
}
// Remove binaries (up to 3)
for _, bin := range binaries {
if err := os.Remove(bin); err != nil {
printer.Warning("Failed to remove binary %s: %s", terminal.Yellow(bin), err)
if runtime.GOOS != "windows" {
printer.Info("Try: %s", terminal.Cyan(fmt.Sprintf("sudo rm %s", bin)))
}
} else {
printer.Success("Removed binary: %s", terminal.Yellow(bin))
}
}
fmt.Println()
printer.Success("Osmedeus has been uninstalled.")
if !uninstallClean {
printer.Info("Note: Workspaces folder was preserved at %s", terminal.Yellow(workspacesPath))
printer.Info("To remove it manually: %s", terminal.Cyan(fmt.Sprintf("rm -rf %s", workspacesPath)))
}
return nil
}
// findOsmedeusBinaries searches PATH for osmedeus binaries, returning up to maxCount unique paths.
func findOsmedeusBinaries(maxCount int) []string {
var results []string
seen := make(map[string]bool)
for i := 0; i < maxCount; i++ {
path, err := exec.LookPath("osmedeus")
if err != nil {
break
}
// Resolve symlinks to get the real path
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
resolved = path
}
absPath, err := filepath.Abs(resolved)
if err != nil {
absPath = resolved
}
if seen[absPath] {
break // No more unique binaries to find
}
seen[absPath] = true
results = append(results, absPath)
// Temporarily remove the directory from PATH so LookPath finds the next one
dir := filepath.Dir(absPath)
removeDirFromPath(dir)
}
return results
}
// removeDirFromPath removes a directory from the PATH environment variable.
func removeDirFromPath(dir string) {
pathSep := string(os.PathListSeparator)
paths := filepath.SplitList(os.Getenv("PATH"))
var filtered []string
for _, p := range paths {
if p != dir {
filtered = append(filtered, p)
}
}
_ = os.Setenv("PATH", joinPaths(filtered, pathSep))
}
// joinPaths joins path segments with the given separator.
func joinPaths(paths []string, sep string) string {
result := ""
for i, p := range paths {
if i > 0 {
result += sep
}
result += p
}
return result
}
// printFolderLine prints a folder entry with exists/missing indicator.
func printFolderLine(printer *terminal.Printer, path, description string) {
if _, err := os.Stat(path); err == nil {
fmt.Printf(" %s %s %s\n", terminal.Red("✘"), terminal.Yellow(path), terminal.Gray(description))
} else {
fmt.Printf(" %s %s %s\n", terminal.Gray("○"), terminal.Gray(path), terminal.Gray("(not found)"))
}
}
// removeFolderIfExists removes a folder and prints the result.
func removeFolderIfExists(printer *terminal.Printer, path, label string) {
if _, err := os.Stat(path); os.IsNotExist(err) {
printer.Info("Skipped %s (not found): %s", label, terminal.Gray(path))
return
}
if err := os.RemoveAll(path); err != nil {
printer.Warning("Failed to remove %s: %s", label, err)
if runtime.GOOS != "windows" {
printer.Info("Try: %s", terminal.Cyan(fmt.Sprintf("sudo rm -rf %s", path)))
}
} else {
printer.Success("Removed %s: %s", label, terminal.Yellow(path))
}
}