chore: publish pxmon v0.2.0

This commit is contained in:
2026-06-16 21:52:10 +04:00
commit 6b703db02b
69 changed files with 25886 additions and 0 deletions
+286
View File
@@ -0,0 +1,286 @@
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 <name> /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 <name> Alias for 'cluster use <name>'")
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
}