package cli import ( "bufio" "errors" "fmt" "io" "os" "strings" "unicode" "pxmon/internal/cluster" ) type shellStatus struct { ActiveCluster string ClusterCount int LoadError error } func (a *App) runShell(configPath string, jsonOut bool) int { status := a.loadShellStatus(configPath) a.printShellBanner(status, configPath, jsonOut) history := make([]string, 0, 64) reader := bufio.NewReader(os.Stdin) for { status = a.loadShellStatus(configPath) fmt.Fprint(a.out, status.prompt()) line, err := reader.ReadString('\n') if err != nil { if errors.Is(err, io.EOF) { fmt.Fprintln(a.out) return 0 } fmt.Fprintf(a.err, "shell input error: %v\n", err) return 1 } line = strings.TrimSpace(line) if line == "" { continue } history = append(history, line) args, parseErr := parseShellArgs(line) if parseErr != nil { fmt.Fprintf(a.err, "parse error: %v\n", parseErr) continue } if len(args) == 0 { continue } if strings.EqualFold(args[0], "pxmon") || strings.EqualFold(args[0], "pxmon") { args = args[1:] } if len(args) == 0 { continue } switch shellToken(args[0]) { case "exit", "quit", "q", ":q": return 0 case "clear", "cls": fmt.Fprint(a.out, "\x1b[2J\x1b[H") continue case "history": for i, h := range history { fmt.Fprintf(a.out, "%3d %s\n", i+1, h) } continue case "help", "?": a.printShellHelp() continue case "status", "dashboard": a.printShellStatus(status, configPath, jsonOut) continue } cmd := expandShellCommand(args) code := a.runRootCommand(cmd, configPath, jsonOut, false) if code != 0 { fmt.Fprintf(a.err, "[exit %d]\n", code) } } } func (a *App) loadShellStatus(configPath string) shellStatus { status := shellStatus{} store, err := cluster.NewStore(configPath) if err != nil { status.LoadError = err return status } svc := cluster.NewService(store) clusters, _, err := svc.List() if err != nil { status.LoadError = err return status } status.ClusterCount = len(clusters) current, err := svc.Current() if err == nil { status.ActiveCluster = current.Name return status } if !errors.Is(err, cluster.ErrNoActiveCluster) { status.LoadError = err } return status } func (s shellStatus) prompt() string { active := strings.TrimSpace(s.ActiveCluster) if active == "" { active = "none" } active = strings.ReplaceAll(active, "]", "_") return fmt.Sprintf("pxmon[%s]> ", active) } func (a *App) printShellBanner(status shellStatus, configPath string, jsonOut bool) { fmt.Fprintln(a.out, "+----------------------------------------------------------------+") fmt.Fprintln(a.out, "| pxmon interactive shell |") fmt.Fprintln(a.out, "| slash: /help /clusters /use /stats [name] /network |") fmt.Fprintln(a.out, "| exit: /quit or /q |") fmt.Fprintln(a.out, "+----------------------------------------------------------------+") a.printShellStatus(status, configPath, jsonOut) fmt.Fprintln(a.out) } func (a *App) printShellStatus(status shellStatus, configPath string, jsonOut bool) { active := strings.TrimSpace(status.ActiveCluster) if active == "" { active = "none" } jsonState := "off" if jsonOut { jsonState = "on" } fmt.Fprintf(a.out, "Clusters: %d | Active: %s | JSON: %s\n", status.ClusterCount, active, jsonState) if strings.TrimSpace(configPath) != "" { fmt.Fprintf(a.out, "Config: %s\n", configPath) } if status.LoadError != nil { fmt.Fprintf(a.out, "Status error: %v\n", status.LoadError) } } func (a *App) printShellHelp() { fmt.Fprintln(a.out, "Shell commands:") fmt.Fprintln(a.out, " help Show this help") fmt.Fprintln(a.out, " status Show shell status") fmt.Fprintln(a.out, " history Show command history") fmt.Fprintln(a.out, " clear Clear screen") fmt.Fprintln(a.out, " exit | quit | q Exit shell") fmt.Fprintln(a.out) fmt.Fprintln(a.out, "Shortcuts:") fmt.Fprintln(a.out, " /clusters Alias for 'cluster list'") fmt.Fprintln(a.out, " /stats [name] Alias for 'cluster stats [name]'") fmt.Fprintln(a.out, " /monitor [name] Alias for 'cluster stats [name]'") fmt.Fprintln(a.out, " /network [name] Open network-focused TUI") fmt.Fprintln(a.out, " /connect ... Alias for 'cluster connect ...'") fmt.Fprintln(a.out, " /use Alias for 'cluster use '") fmt.Fprintln(a.out, " /alert ... Alias for 'cluster alert ...'") fmt.Fprintln(a.out) fmt.Fprintln(a.out, "Examples:") fmt.Fprintln(a.out, " /clusters") fmt.Fprintln(a.out, " /connect --name eu-1 --host 10.0.0.10 --user root --auth key --key-path ~/.ssh/id_ed25519") fmt.Fprintln(a.out, " /bootstrap eu-1") fmt.Fprintln(a.out, " /stats eu-1") fmt.Fprintln(a.out, " /network eu-1") fmt.Fprintln(a.out, " /alert set eu-1 --net-mbps 300 --ram 90 --disk 90") fmt.Fprintln(a.out, " /alert set eu-1 --net-sustain-enabled=true --net-sustain-mbps 500 --net-sustain-mins 60 --net-sustain-include net0 --net-sustain-exclude backup") } func shellToken(v string) string { token := strings.TrimSpace(strings.ToLower(v)) if strings.HasPrefix(token, "/") { token = strings.TrimPrefix(token, "/") } return token } func expandShellCommand(args []string) []string { if len(args) == 0 { return args } out := append([]string(nil), args...) cmd := shellToken(out[0]) if cmd == "" { return out } out[0] = cmd switch cmd { case "clusters", "nodes": return append([]string{"cluster", "list"}, out[1:]...) case "stats", "monitor", "watch": return append([]string{"cluster", "stats"}, out[1:]...) case "alerts": return append([]string{"cluster", "alert"}, out[1:]...) } if isClusterShortcut(cmd) { return append([]string{"cluster", cmd}, out[1:]...) } return out } func isClusterShortcut(cmd string) bool { switch cmd { case "connect", "add", "list", "ls", "show", "get", "current", "use", "ping", "check", "bootstrap", "disconnect", "remove", "rm", "alert": return true default: return false } } func parseShellArgs(line string) ([]string, error) { line = strings.TrimSpace(line) if line == "" { return nil, nil } args := make([]string, 0, 8) var current strings.Builder var quote rune escaped := false tokenStarted := false flush := func() { if tokenStarted { args = append(args, current.String()) current.Reset() tokenStarted = false } } for _, r := range line { switch { case escaped: current.WriteRune(r) escaped = false tokenStarted = true case r == '\\': escaped = true tokenStarted = true case quote != 0: if r == quote { quote = 0 } else { current.WriteRune(r) } tokenStarted = true case r == '\'' || r == '"': quote = r tokenStarted = true case unicode.IsSpace(r): flush() default: current.WriteRune(r) tokenStarted = true } } if escaped { return nil, errors.New("unterminated escape at end of command") } if quote != 0 { return nil, errors.New("unterminated quoted string") } flush() return args, nil }