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 ", 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]) + "…" }