313 lines
8.6 KiB
Go
313 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"hugin/cli/internal/api"
|
|
"hugin/cli/internal/bundle"
|
|
"hugin/cli/internal/config"
|
|
"hugin/cli/internal/keystore"
|
|
"hugin/cli/internal/localmcp"
|
|
"hugin/cli/internal/lock"
|
|
"hugin/cli/internal/pkgdir"
|
|
"hugin/cli/internal/reconcile"
|
|
)
|
|
|
|
// Lock-Datei von hugin try; hugin.lock bleibt dabei unberührt (→ KONZEPT.md 11.1).
|
|
const trialLock = localmcp.TrialLock
|
|
|
|
func newCommand() *cobra.Command {
|
|
var folder string
|
|
cmd := &cobra.Command{
|
|
Use: "new <kind> <id>",
|
|
Short: "legt einen Paketordner mit Manifest-Gerüst an (Art: skill, rules, command, subagent, mcp, plugin)",
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
target, err := pkgdir.New(folder, args[0], args[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s angelegt. hugin.yaml ausfüllen, dann: hugin validate %s\n", target, target)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&folder, "in", ".", "Ordner, in dem der Paketordner entsteht")
|
|
return cmd
|
|
}
|
|
|
|
// authorClient: Server aus --server, sonst aus .hugin.yaml, sonst HUGIN_SERVER.
|
|
func authorClient(server string) (*api.Client, error) {
|
|
if server == "" {
|
|
if k, err := config.Load(repoFolder); err == nil {
|
|
server = k.Server
|
|
}
|
|
}
|
|
if server == "" {
|
|
server = os.Getenv("HUGIN_SERVER")
|
|
}
|
|
if server == "" {
|
|
return nil, errors.New("kein Server: --server angeben, HUGIN_SERVER setzen oder in einem Repo mit .hugin.yaml arbeiten")
|
|
}
|
|
key, err := keystore.Get(server)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return api.New(server, key)
|
|
}
|
|
|
|
func folderArg(args []string) string {
|
|
if len(args) == 1 {
|
|
return args[0]
|
|
}
|
|
return "."
|
|
}
|
|
|
|
// printFindings druckt Linter-Befunde; true, wenn ein Fehler dabei ist.
|
|
func printFindings(findings []api.Finding) bool {
|
|
errs := false
|
|
for _, b := range findings {
|
|
location := ""
|
|
if b.File != "" {
|
|
location = " (" + b.File + ")"
|
|
}
|
|
fmt.Printf(" %-7s %s %s%s\n", b.Severity, b.Id, b.Text, location)
|
|
errs = errs || b.Severity == "error"
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// linterError macht aus einer 422-Antwort mit Befunden eine lesbare Ausgabe.
|
|
func linterError(err error) error {
|
|
var f *api.APIError
|
|
if errors.As(err, &f) && len(f.Findings) > 0 {
|
|
fmt.Println("Der Linter hat das Paket abgelehnt:")
|
|
printFindings(f.Findings)
|
|
return findingError{"Paket nicht angenommen"}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func validateCommand() *cobra.Command {
|
|
var server string
|
|
cmd := &cobra.Command{
|
|
Use: "validate [folder]",
|
|
Short: "prüft einen Paketordner über den Server und zeigt, was in jedem Werkzeug ankommt; speichert nichts",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
client, err := authorClient(server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := pkgdir.Zip(folderArg(args))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := client.Validate(raw)
|
|
if err != nil {
|
|
return linterError(err)
|
|
}
|
|
if len(p.Findings) > 0 {
|
|
fmt.Println("Befunde:")
|
|
}
|
|
errs := printFindings(p.Findings)
|
|
fmt.Printf("Risiko: %s\n", p.Risk)
|
|
if p.Compatibility != nil {
|
|
fmt.Printf("Kompatibilität: %s\n", *p.Compatibility)
|
|
}
|
|
toolNames := make([]string, 0, len(p.Preview))
|
|
for w := range p.Preview {
|
|
toolNames = append(toolNames, w)
|
|
}
|
|
sort.Strings(toolNames)
|
|
for _, w := range toolNames {
|
|
fmt.Printf("\n%s:\n", w)
|
|
for _, op := range p.Preview[w] {
|
|
fmt.Printf(" %-10v %v\n", op["kind"], target(op))
|
|
}
|
|
}
|
|
if errs {
|
|
return findingError{"Paket hat Fehler"}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&server, "server", "", "Adresse von Hugin (sonst aus .hugin.yaml oder HUGIN_SERVER)")
|
|
return cmd
|
|
}
|
|
|
|
func target(op map[string]any) string {
|
|
if path, ok := op["path"]; ok {
|
|
return fmt.Sprint(path)
|
|
}
|
|
text := fmt.Sprint(op["file"])
|
|
if s, ok := op["key"]; ok {
|
|
text += fmt.Sprintf(" %v", s)
|
|
}
|
|
if b, ok := op["block"]; ok {
|
|
text += fmt.Sprintf(" Block %v", b)
|
|
}
|
|
return text
|
|
}
|
|
|
|
func tryCommand() *cobra.Command {
|
|
var server string
|
|
var remove, overwrite bool
|
|
cmd := &cobra.Command{
|
|
Use: "try [folder]",
|
|
Short: "installiert einen Entwurf zum Ausprobieren ins Repo, ohne hugin.lock zu ändern (--remove räumt auf)",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
lockPath := filepath.Join(repoFolder, trialLock)
|
|
old, err := lock.LoadFrom(lockPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b := bundle.Empty()
|
|
if !remove {
|
|
client, err := authorClient(server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := pkgdir.Zip(folderArg(args))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
k, _ := config.Load(repoFolder)
|
|
detected, _ := detectTools(k)
|
|
details := map[string]string{}
|
|
for _, w := range detected {
|
|
details[w.name] = w.version
|
|
}
|
|
zip, err := client.Trial(raw, details)
|
|
if err != nil {
|
|
return linterError(err)
|
|
}
|
|
if b, err = bundle.FromZip(zip); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
plan, err := reconcile.MakePlan(repoFolder, b, old, reconcile.Options{Overwrite: overwrite})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range plan.Findings {
|
|
fmt.Fprintln(os.Stderr, "Befund:", f)
|
|
}
|
|
if plan.Open() {
|
|
return findingError{"nichts geändert; mit --overwrite setzt try die Dateien trotzdem"}
|
|
}
|
|
if err := plan.RunWithLock(repoFolder, lockPath); err != nil {
|
|
return err
|
|
}
|
|
if !remove {
|
|
home, err := localmcp.Home()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := install(b, home); err != nil {
|
|
return fmt.Errorf("Probe geschrieben, aber lokaler Server nicht installiert: %w", err)
|
|
}
|
|
}
|
|
if remove {
|
|
os.Remove(lockPath)
|
|
fmt.Printf("Probe entfernt: %d Dateien gelöscht.\n", len(plan.Delete))
|
|
return nil
|
|
}
|
|
fmt.Printf("Probe installiert: %d Dateien geschrieben (%d neu).\n", len(plan.Write), len(plan.New))
|
|
for _, rel := range plan.New {
|
|
fmt.Println(" ", rel)
|
|
}
|
|
fmt.Println("Jetzt in Claude Code oder OpenCode ausprobieren. Aufräumen mit: hugin try --remove")
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&server, "server", "", "Adresse von Hugin (sonst aus .hugin.yaml oder HUGIN_SERVER)")
|
|
cmd.Flags().BoolVar(&remove, "remove", false, "Dateien der letzten Probe wieder entfernen")
|
|
cmd.Flags().BoolVar(&overwrite, "overwrite", false, "vorhandene Dateien überschreiben")
|
|
return cmd
|
|
}
|
|
|
|
func uploadCommand() *cobra.Command {
|
|
var server string
|
|
var submit bool
|
|
cmd := &cobra.Command{
|
|
Use: "upload [folder]",
|
|
Short: "lädt einen Paketordner als Entwurf hoch (mit --submit gleich zur Prüfung)",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
client, err := authorClient(server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := pkgdir.Zip(folderArg(args))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v, err := client.Upload(raw)
|
|
if err != nil {
|
|
return linterError(err)
|
|
}
|
|
printFindings(v.Linter)
|
|
fmt.Printf("Entwurf %s %s gespeichert (Risiko %s).\n", v.Package, v.Version, v.Risk)
|
|
if submit {
|
|
if v, err = client.Submit(v.Package, v.Version); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Eingereicht: %s %s ist jetzt %s. Eine zweite Person prüft.\n", v.Package, v.Version, v.Status)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&server, "server", "", "Adresse von Hugin (sonst aus .hugin.yaml oder HUGIN_SERVER)")
|
|
cmd.Flags().BoolVar(&submit, "submit", false, "nach dem Hochladen zur Prüfung einreichen")
|
|
return cmd
|
|
}
|
|
|
|
func searchCommand() *cobra.Command {
|
|
var server, kind string
|
|
cmd := &cobra.Command{
|
|
Use: "search [text]",
|
|
Short: "sucht im Katalog in ID und Beschreibung (für Teams und Autoren)",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
client, err := authorClient(server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
text := ""
|
|
if len(args) == 1 {
|
|
text = args[0]
|
|
}
|
|
matches, err := client.Search(text, kind)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(matches) == 0 {
|
|
fmt.Println("Keine Pakete gefunden.")
|
|
return nil
|
|
}
|
|
for _, t := range matches {
|
|
fmt.Printf("%-32s %-9s %-8s %s\n", t.ID, t.Kind, t.Version, shorten(t.Description, 80))
|
|
}
|
|
fmt.Printf("\n%d Paket(e). Abonnieren: in .hugin.yaml unter packages eintragen, dann hugin sync.\n", len(matches))
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&server, "server", "", "Adresse von Hugin (sonst aus .hugin.yaml oder HUGIN_SERVER)")
|
|
cmd.Flags().StringVar(&kind, "kind", "", "nur diese Art: skill, rules, command, subagent, mcp, plugin")
|
|
return cmd
|
|
}
|
|
|
|
func shorten(text string, n int) string {
|
|
r := []rune(text)
|
|
if len(r) <= n {
|
|
return text
|
|
}
|
|
return string(r[:n-1]) + "…"
|
|
}
|