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
+131
View File
@@ -0,0 +1,131 @@
// Package konfig liest und schreibt ~/.config/proxdeploy/config.toml.
//
// url = "https://proxdeploy.example"
// token = "pd_…"
// server = "rz1" # optional: Standard für --server
//
// [instances.labor] # weitere Instanzen, Auswahl mit --instance labor
// url = "https://proxdeploy.labor"
// token = "pd_…"
//
// PROXDEPLOY_URL, PROXDEPLOY_TOKEN und PROXDEPLOY_INSTANCE gehen der Datei vor.
package konfig
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"github.com/BurntSushi/toml"
)
type Instanz struct {
URL string `toml:"url"`
Token string `toml:"token"`
Server string `toml:"server,omitempty"`
}
type Datei struct {
Instanz
Instanzen map[string]Instanz `toml:"instances,omitempty"`
}
// Pfad: $XDG_CONFIG_HOME/proxdeploy/config.toml, sonst ~/.config/…; unter Windows %AppData%.
func Pfad() (string, error) {
if eigener := os.Getenv("PROXDEPLOY_CONFIG"); eigener != "" {
return eigener, nil
}
basis := os.Getenv("XDG_CONFIG_HOME")
if basis == "" {
if runtime.GOOS == "windows" {
var err error
if basis, err = os.UserConfigDir(); err != nil {
return "", err
}
} else {
heim, err := os.UserHomeDir()
if err != nil {
return "", err
}
basis = filepath.Join(heim, ".config")
}
}
return filepath.Join(basis, "proxdeploy", "config.toml"), nil
}
// Lesen liefert die Datei oder eine leere, wenn es sie noch nicht gibt.
func Lesen() (Datei, string, error) {
pfad, err := Pfad()
if err != nil {
return Datei{}, "", err
}
var d Datei
if _, err := toml.DecodeFile(pfad, &d); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return Datei{}, pfad, nil
}
return Datei{}, pfad, fmt.Errorf("%s: %w", pfad, err)
}
return d, pfad, nil
}
// Waehlen: die gewünschte Instanz (leer = Hauptabschnitt), überlagert von der Umgebung.
func (d Datei) Waehlen(name string) (Instanz, error) {
if name == "" {
name = os.Getenv("PROXDEPLOY_INSTANCE")
}
i := d.Instanz
if name != "" {
gefunden, ok := d.Instanzen[name]
if !ok {
return Instanz{}, fmt.Errorf("Instanz %q steht nicht in der Konfiguration", name)
}
i = gefunden
}
if url := os.Getenv("PROXDEPLOY_URL"); url != "" {
i.URL = url
}
if token := os.Getenv("PROXDEPLOY_TOKEN"); token != "" {
i.Token = token
}
return i, nil
}
// Setzen trägt eine Instanz ein (leer = Hauptabschnitt).
func (d *Datei) Setzen(name string, i Instanz) {
if name == "" {
d.Instanz = i
return
}
if d.Instanzen == nil {
d.Instanzen = map[string]Instanz{}
}
d.Instanzen[name] = i
}
// Schreiben legt die Datei mit Rechten 0600 an – sie enthält das Token.
func Schreiben(pfad string, d Datei) error {
if err := os.MkdirAll(filepath.Dir(pfad), 0o700); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(pfad), ".config-*.toml")
if err != nil {
return err
}
defer os.Remove(tmp.Name())
if err := tmp.Chmod(0o600); err != nil {
tmp.Close()
return err
}
if err := toml.NewEncoder(tmp).Encode(d); err != nil {
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return os.Rename(tmp.Name(), pfad)
}