feat(cli): proxdeploy in Go

Befehle login, servers, distros, profiles, presets, vm, lxc, template list|build, batch, status, logs --follow, cancel. Renderer im Aufbau der Vorgabe (zeichengenau per Golden-File), 24-bit/256/ohne Farbe, --ascii, --quiet, --json, --dry-run; live im Terminal, zeilenweise ohne. SSE mit Wiederverbinden und Polling als Rückfall. Typen aus openapi.json per oapi-codegen; Release-Builds per cli/bauen.sh.

Refs #013

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 15:08:41 +02:00
co-authored by Claude Opus 5.5
parent 9fdced62db
commit bd4bca5399
36 changed files with 5960 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
package befehle
import (
"bufio"
"fmt"
"net/url"
"os"
"strings"
"github.com/spf13/cobra"
"golang.org/x/term"
"git.wilde-server.de/florian.wilde/proxdeploy/cli/internal/api"
"git.wilde-server.de/florian.wilde/proxdeploy/cli/internal/konfig"
)
func (u *umgebung) loginBefehl() *cobra.Command {
var token, benutzer, name, server string
var tage int
befehl := &cobra.Command{
Use: "login <url>",
Short: "An einer ProxDeploy-Instanz anmelden und das API-Token speichern",
Long: "Fragt Benutzername und Passwort ab, legt ein API-Token an und speichert es in\n" +
"~/.config/proxdeploy/config.toml (Rechte 0600). Mit --token wird ein vorhandenes\n" +
"Token (Web-GUI → Einstellungen → API-Tokens) geprüft und gespeichert.",
Example: " proxdeploy login https://proxdeploy.example\n proxdeploy login https://proxdeploy.labor --instance labor --token pd_…",
Args: func(_ *cobra.Command, argumente []string) error {
if len(argumente) != 1 {
return bedienfehler("URL fehlt: proxdeploy login <url>")
}
return nil
},
RunE: func(cmd *cobra.Command, argumente []string) error {
adresse := strings.TrimRight(strings.TrimSpace(argumente[0]), "/")
if !strings.Contains(adresse, "://") {
adresse = "https://" + adresse
}
geprueft, err := url.Parse(adresse)
if err != nil || geprueft.Host == "" || (geprueft.Scheme != "https" && geprueft.Scheme != "http") {
return bedienfehler("keine gültige URL: %s", argumente[0])
}
ctx := cmd.Context()
c := api.Neu(adresse, "", Version)
if token == "" {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return bedienfehler("ohne Terminal kein Passwort-Dialog – --token angeben")
}
leser := bufio.NewReader(os.Stdin)
if benutzer == "" {
fmt.Fprint(u.fehler, "Benutzername: ")
zeile, err := leser.ReadString('\n')
if err != nil {
return bedienfehler("Eingabe abgebrochen")
}
benutzer = strings.TrimSpace(zeile)
}
fmt.Fprint(u.fehler, "Passwort: ")
passwort, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(u.fehler)
if err != nil {
return bedienfehler("Eingabe abgebrochen")
}
if name == "" {
rechner, _ := os.Hostname()
name = "cli@" + rechner
}
anmeldung := api.TokenAnmeldung{Benutzername: benutzer, Passwort: string(passwort), Name: name}
if tage > 0 {
anmeldung.Tage = &tage
}
neu, err := c.TokenAnmelden(ctx, anmeldung)
if err != nil {
return err
}
token = neu.Token
}
c.Token = token
ich, err := c.Ich(ctx)
if err != nil {
return err
}
datei, pfad, err := konfig.Lesen()
if err != nil {
return err
}
instanz := konfig.Instanz{URL: adresse, Token: token, Server: server}
if alt, ok := datei.Instanzen[u.opt.instanz]; ok && server == "" {
instanz.Server = alt.Server
} else if u.opt.instanz == "" && server == "" {
instanz.Server = datei.Server
}
datei.Setzen(u.opt.instanz, instanz)
if err := konfig.Schreiben(pfad, datei); err != nil {
return fmt.Errorf("Konfiguration nicht geschrieben: %w", err)
}
if u.opt.json {
return u.jsonAusgeben(map[string]any{"benutzername": ich.Benutzername, "rolle": ich.Rolle, "version": ich.Version, "konfiguration": pfad})
}
s := u.stil()
symbol := "✓"
if s.ASCII {
symbol = "[OK]"
}
rolle := map[api.IchRolle]string{api.ADMIN: "Admin", api.OPERATOR: "Operator"}[ich.Rolle]
fmt.Fprintf(u.aus, "%s Angemeldet als %s %s\n", s.Teal(symbol), s.Fett(ich.Benutzername),
s.Muted(fmt.Sprintf("(%s) · ProxDeploy v%s · Token in %s", rolle, ich.Version, pfad)))
return nil
},
}
f := befehl.Flags()
f.StringVar(&token, "token", "", "vorhandenes API-Token (pd_…) statt Passwort")
f.StringVar(&benutzer, "user", "", "Benutzername (sonst Abfrage)")
f.StringVar(&name, "name", "", "Name des Tokens (Standard: cli@<rechner>)")
f.IntVar(&tage, "days", 0, "Token läuft nach so vielen Tagen ab (Standard: unbefristet)")
f.StringVar(&server, "default-server", "", "Standard für --server speichern")
return befehl
}