chore: publish pxmon v0.2.0
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user