ScuttleBot
fix: restore topology/config/bot commands lost in #104 merge The merge of main into feature/104-scuttlectl-topology incorrectly resolved conflicts by dropping the topology, config, and bot command handlers from scuttlectl main.go. This restores all three command groups while keeping the api-key commands from main.
Commit
1d115cc9ccddbeecca769324c707f7e94e29ff7ecffa73d6b51be7a96cc457ee
Parent
a1cd907f8751b86…
1 file changed
+183
+183
| --- cmd/scuttlectl/main.go | ||
| +++ cmd/scuttlectl/main.go | ||
| @@ -166,10 +166,54 @@ | ||
| 166 | 166 | cmdBackendRename(api, args[2], args[3]) |
| 167 | 167 | default: |
| 168 | 168 | fmt.Fprintf(os.Stderr, "unknown subcommand: backend %s\n", args[1]) |
| 169 | 169 | os.Exit(1) |
| 170 | 170 | } |
| 171 | + case "topology", "topo": | |
| 172 | + if len(args) < 2 { | |
| 173 | + fmt.Fprintf(os.Stderr, "usage: scuttlectl topology <list|provision|drop>\n") | |
| 174 | + os.Exit(1) | |
| 175 | + } | |
| 176 | + switch args[1] { | |
| 177 | + case "list", "show": | |
| 178 | + cmdTopologyList(api, *jsonFlag) | |
| 179 | + case "provision", "create": | |
| 180 | + requireArgs(args, 3, "scuttlectl topology provision #channel") | |
| 181 | + cmdTopologyProvision(api, args[2], *jsonFlag) | |
| 182 | + case "drop", "rm": | |
| 183 | + requireArgs(args, 3, "scuttlectl topology drop #channel") | |
| 184 | + cmdTopologyDrop(api, args[2]) | |
| 185 | + default: | |
| 186 | + fmt.Fprintf(os.Stderr, "unknown subcommand: topology %s\n", args[1]) | |
| 187 | + os.Exit(1) | |
| 188 | + } | |
| 189 | + case "config": | |
| 190 | + if len(args) < 2 { | |
| 191 | + fmt.Fprintf(os.Stderr, "usage: scuttlectl config <show|history>\n") | |
| 192 | + os.Exit(1) | |
| 193 | + } | |
| 194 | + switch args[1] { | |
| 195 | + case "show", "get": | |
| 196 | + cmdConfigShow(api, *jsonFlag) | |
| 197 | + case "history": | |
| 198 | + cmdConfigHistory(api, *jsonFlag) | |
| 199 | + default: | |
| 200 | + fmt.Fprintf(os.Stderr, "unknown subcommand: config %s\n", args[1]) | |
| 201 | + os.Exit(1) | |
| 202 | + } | |
| 203 | + case "bot", "bots": | |
| 204 | + if len(args) < 2 { | |
| 205 | + fmt.Fprintf(os.Stderr, "usage: scuttlectl bot <list>\n") | |
| 206 | + os.Exit(1) | |
| 207 | + } | |
| 208 | + switch args[1] { | |
| 209 | + case "list": | |
| 210 | + cmdBotList(api, *jsonFlag) | |
| 211 | + default: | |
| 212 | + fmt.Fprintf(os.Stderr, "unknown subcommand: bot %s\n", args[1]) | |
| 213 | + os.Exit(1) | |
| 214 | + } | |
| 171 | 215 | default: |
| 172 | 216 | fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) |
| 173 | 217 | usage() |
| 174 | 218 | os.Exit(1) |
| 175 | 219 | } |
| @@ -509,10 +553,12 @@ | ||
| 509 | 553 | fmt.Fprintf(tw, "password\t%s\n", creds.Password) |
| 510 | 554 | fmt.Fprintf(tw, "server\t%s\n", creds.Server) |
| 511 | 555 | tw.Flush() |
| 512 | 556 | fmt.Println("\nStore this password — it will not be shown again.") |
| 513 | 557 | } |
| 558 | + | |
| 559 | +// --- api-keys --- | |
| 514 | 560 | |
| 515 | 561 | func cmdAPIKeyList(api *apiclient.Client, asJSON bool) { |
| 516 | 562 | raw, err := api.ListAPIKeys() |
| 517 | 563 | die(err) |
| 518 | 564 | if asJSON { |
| @@ -587,10 +633,141 @@ | ||
| 587 | 633 | |
| 588 | 634 | func cmdAPIKeyRevoke(api *apiclient.Client, id string) { |
| 589 | 635 | die(api.RevokeAPIKey(id)) |
| 590 | 636 | fmt.Printf("API key revoked: %s\n", id) |
| 591 | 637 | } |
| 638 | + | |
| 639 | +// --- topology --- | |
| 640 | + | |
| 641 | +func cmdTopologyList(api *apiclient.Client, asJSON bool) { | |
| 642 | + raw, err := api.GetTopology() | |
| 643 | + die(err) | |
| 644 | + if asJSON { | |
| 645 | + printJSON(raw) | |
| 646 | + return | |
| 647 | + } | |
| 648 | + var data struct { | |
| 649 | + StaticChannels []string `json:"static_channels"` | |
| 650 | + Types []struct { | |
| 651 | + Name string `json:"name"` | |
| 652 | + Prefix string `json:"prefix"` | |
| 653 | + Autojoin []string `json:"autojoin"` | |
| 654 | + Ephemeral bool `json:"ephemeral"` | |
| 655 | + TTL int64 `json:"ttl_seconds"` | |
| 656 | + } `json:"types"` | |
| 657 | + } | |
| 658 | + must(json.Unmarshal(raw, &data)) | |
| 659 | + | |
| 660 | + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) | |
| 661 | + fmt.Fprintln(tw, "STATIC CHANNELS") | |
| 662 | + for _, ch := range data.StaticChannels { | |
| 663 | + fmt.Fprintf(tw, " %s\n", ch) | |
| 664 | + } | |
| 665 | + if len(data.Types) > 0 { | |
| 666 | + fmt.Fprintln(tw, "\nCHANNEL TYPES") | |
| 667 | + fmt.Fprintln(tw, " NAME\tPREFIX\tAUTOJOIN\tEPHEMERAL\tTTL") | |
| 668 | + for _, t := range data.Types { | |
| 669 | + ttl := "—" | |
| 670 | + if t.TTL > 0 { | |
| 671 | + ttl = fmt.Sprintf("%dh", t.TTL/3600) | |
| 672 | + } | |
| 673 | + eph := "no" | |
| 674 | + if t.Ephemeral { | |
| 675 | + eph = "yes" | |
| 676 | + } | |
| 677 | + fmt.Fprintf(tw, " %s\t#%s*\t%s\t%s\t%s\n", t.Name, t.Prefix, strings.Join(t.Autojoin, ","), eph, ttl) | |
| 678 | + } | |
| 679 | + } | |
| 680 | + tw.Flush() | |
| 681 | +} | |
| 682 | + | |
| 683 | +func cmdTopologyProvision(api *apiclient.Client, channel string, asJSON bool) { | |
| 684 | + if !strings.HasPrefix(channel, "#") { | |
| 685 | + channel = "#" + channel | |
| 686 | + } | |
| 687 | + raw, err := api.ProvisionChannel(channel) | |
| 688 | + die(err) | |
| 689 | + if asJSON { | |
| 690 | + printJSON(raw) | |
| 691 | + return | |
| 692 | + } | |
| 693 | + fmt.Printf("Channel provisioned: %s\n", channel) | |
| 694 | +} | |
| 695 | + | |
| 696 | +func cmdTopologyDrop(api *apiclient.Client, channel string) { | |
| 697 | + if !strings.HasPrefix(channel, "#") { | |
| 698 | + channel = "#" + channel | |
| 699 | + } | |
| 700 | + die(api.DropChannel(channel)) | |
| 701 | + fmt.Printf("Channel dropped: %s\n", channel) | |
| 702 | +} | |
| 703 | + | |
| 704 | +// --- config --- | |
| 705 | + | |
| 706 | +func cmdConfigShow(api *apiclient.Client, asJSON bool) { | |
| 707 | + raw, err := api.GetConfig() | |
| 708 | + die(err) | |
| 709 | + printJSON(raw) // always JSON — config is a complex nested object | |
| 710 | +} | |
| 711 | + | |
| 712 | +func cmdConfigHistory(api *apiclient.Client, asJSON bool) { | |
| 713 | + raw, err := api.GetConfigHistory() | |
| 714 | + die(err) | |
| 715 | + if asJSON { | |
| 716 | + printJSON(raw) | |
| 717 | + return | |
| 718 | + } | |
| 719 | + var data struct { | |
| 720 | + Entries []struct { | |
| 721 | + Filename string `json:"filename"` | |
| 722 | + At string `json:"at"` | |
| 723 | + } `json:"entries"` | |
| 724 | + } | |
| 725 | + must(json.Unmarshal(raw, &data)) | |
| 726 | + if len(data.Entries) == 0 { | |
| 727 | + fmt.Println("no config history") | |
| 728 | + return | |
| 729 | + } | |
| 730 | + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) | |
| 731 | + fmt.Fprintln(tw, "SNAPSHOT\tTIME") | |
| 732 | + for _, e := range data.Entries { | |
| 733 | + fmt.Fprintf(tw, "%s\t%s\n", e.Filename, e.At) | |
| 734 | + } | |
| 735 | + tw.Flush() | |
| 736 | +} | |
| 737 | + | |
| 738 | +// --- bots --- | |
| 739 | + | |
| 740 | +func cmdBotList(api *apiclient.Client, asJSON bool) { | |
| 741 | + raw, err := api.GetSettings() | |
| 742 | + die(err) | |
| 743 | + if asJSON { | |
| 744 | + printJSON(raw) | |
| 745 | + return | |
| 746 | + } | |
| 747 | + var data struct { | |
| 748 | + Policies struct { | |
| 749 | + Behaviors []struct { | |
| 750 | + ID string `json:"id"` | |
| 751 | + Name string `json:"name"` | |
| 752 | + Nick string `json:"nick"` | |
| 753 | + Enabled bool `json:"enabled"` | |
| 754 | + } `json:"behaviors"` | |
| 755 | + } `json:"policies"` | |
| 756 | + } | |
| 757 | + must(json.Unmarshal(raw, &data)) | |
| 758 | + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) | |
| 759 | + fmt.Fprintln(tw, "BOT\tNICK\tSTATUS") | |
| 760 | + for _, b := range data.Policies.Behaviors { | |
| 761 | + status := "disabled" | |
| 762 | + if b.Enabled { | |
| 763 | + status = "enabled" | |
| 764 | + } | |
| 765 | + fmt.Fprintf(tw, "%s\t%s\t%s\n", b.Name, b.Nick, status) | |
| 766 | + } | |
| 767 | + tw.Flush() | |
| 768 | +} | |
| 592 | 769 | |
| 593 | 770 | func usage() { |
| 594 | 771 | fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI |
| 595 | 772 | |
| 596 | 773 | Usage: |
| @@ -625,10 +802,16 @@ | ||
| 625 | 802 | admin remove <username> remove admin |
| 626 | 803 | admin passwd <username> change admin password (prompts) |
| 627 | 804 | api-key list list API keys |
| 628 | 805 | api-key create --name <name> --scopes <s1,s2> [--expires 720h] |
| 629 | 806 | api-key revoke <id> revoke an API key |
| 807 | + topology list show topology (static channels, types) | |
| 808 | + topology provision #channel provision a new channel via ChanServ | |
| 809 | + topology drop #channel drop a channel | |
| 810 | + config show dump current config (JSON) | |
| 811 | + config history show config change history | |
| 812 | + bot list show system bot status | |
| 630 | 813 | `, version) |
| 631 | 814 | } |
| 632 | 815 | |
| 633 | 816 | func printJSON(raw json.RawMessage) { |
| 634 | 817 | var buf []byte |
| 635 | 818 |
| --- cmd/scuttlectl/main.go | |
| +++ cmd/scuttlectl/main.go | |
| @@ -166,10 +166,54 @@ | |
| 166 | cmdBackendRename(api, args[2], args[3]) |
| 167 | default: |
| 168 | fmt.Fprintf(os.Stderr, "unknown subcommand: backend %s\n", args[1]) |
| 169 | os.Exit(1) |
| 170 | } |
| 171 | default: |
| 172 | fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) |
| 173 | usage() |
| 174 | os.Exit(1) |
| 175 | } |
| @@ -509,10 +553,12 @@ | |
| 509 | fmt.Fprintf(tw, "password\t%s\n", creds.Password) |
| 510 | fmt.Fprintf(tw, "server\t%s\n", creds.Server) |
| 511 | tw.Flush() |
| 512 | fmt.Println("\nStore this password — it will not be shown again.") |
| 513 | } |
| 514 | |
| 515 | func cmdAPIKeyList(api *apiclient.Client, asJSON bool) { |
| 516 | raw, err := api.ListAPIKeys() |
| 517 | die(err) |
| 518 | if asJSON { |
| @@ -587,10 +633,141 @@ | |
| 587 | |
| 588 | func cmdAPIKeyRevoke(api *apiclient.Client, id string) { |
| 589 | die(api.RevokeAPIKey(id)) |
| 590 | fmt.Printf("API key revoked: %s\n", id) |
| 591 | } |
| 592 | |
| 593 | func usage() { |
| 594 | fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI |
| 595 | |
| 596 | Usage: |
| @@ -625,10 +802,16 @@ | |
| 625 | admin remove <username> remove admin |
| 626 | admin passwd <username> change admin password (prompts) |
| 627 | api-key list list API keys |
| 628 | api-key create --name <name> --scopes <s1,s2> [--expires 720h] |
| 629 | api-key revoke <id> revoke an API key |
| 630 | `, version) |
| 631 | } |
| 632 | |
| 633 | func printJSON(raw json.RawMessage) { |
| 634 | var buf []byte |
| 635 |
| --- cmd/scuttlectl/main.go | |
| +++ cmd/scuttlectl/main.go | |
| @@ -166,10 +166,54 @@ | |
| 166 | cmdBackendRename(api, args[2], args[3]) |
| 167 | default: |
| 168 | fmt.Fprintf(os.Stderr, "unknown subcommand: backend %s\n", args[1]) |
| 169 | os.Exit(1) |
| 170 | } |
| 171 | case "topology", "topo": |
| 172 | if len(args) < 2 { |
| 173 | fmt.Fprintf(os.Stderr, "usage: scuttlectl topology <list|provision|drop>\n") |
| 174 | os.Exit(1) |
| 175 | } |
| 176 | switch args[1] { |
| 177 | case "list", "show": |
| 178 | cmdTopologyList(api, *jsonFlag) |
| 179 | case "provision", "create": |
| 180 | requireArgs(args, 3, "scuttlectl topology provision #channel") |
| 181 | cmdTopologyProvision(api, args[2], *jsonFlag) |
| 182 | case "drop", "rm": |
| 183 | requireArgs(args, 3, "scuttlectl topology drop #channel") |
| 184 | cmdTopologyDrop(api, args[2]) |
| 185 | default: |
| 186 | fmt.Fprintf(os.Stderr, "unknown subcommand: topology %s\n", args[1]) |
| 187 | os.Exit(1) |
| 188 | } |
| 189 | case "config": |
| 190 | if len(args) < 2 { |
| 191 | fmt.Fprintf(os.Stderr, "usage: scuttlectl config <show|history>\n") |
| 192 | os.Exit(1) |
| 193 | } |
| 194 | switch args[1] { |
| 195 | case "show", "get": |
| 196 | cmdConfigShow(api, *jsonFlag) |
| 197 | case "history": |
| 198 | cmdConfigHistory(api, *jsonFlag) |
| 199 | default: |
| 200 | fmt.Fprintf(os.Stderr, "unknown subcommand: config %s\n", args[1]) |
| 201 | os.Exit(1) |
| 202 | } |
| 203 | case "bot", "bots": |
| 204 | if len(args) < 2 { |
| 205 | fmt.Fprintf(os.Stderr, "usage: scuttlectl bot <list>\n") |
| 206 | os.Exit(1) |
| 207 | } |
| 208 | switch args[1] { |
| 209 | case "list": |
| 210 | cmdBotList(api, *jsonFlag) |
| 211 | default: |
| 212 | fmt.Fprintf(os.Stderr, "unknown subcommand: bot %s\n", args[1]) |
| 213 | os.Exit(1) |
| 214 | } |
| 215 | default: |
| 216 | fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) |
| 217 | usage() |
| 218 | os.Exit(1) |
| 219 | } |
| @@ -509,10 +553,12 @@ | |
| 553 | fmt.Fprintf(tw, "password\t%s\n", creds.Password) |
| 554 | fmt.Fprintf(tw, "server\t%s\n", creds.Server) |
| 555 | tw.Flush() |
| 556 | fmt.Println("\nStore this password — it will not be shown again.") |
| 557 | } |
| 558 | |
| 559 | // --- api-keys --- |
| 560 | |
| 561 | func cmdAPIKeyList(api *apiclient.Client, asJSON bool) { |
| 562 | raw, err := api.ListAPIKeys() |
| 563 | die(err) |
| 564 | if asJSON { |
| @@ -587,10 +633,141 @@ | |
| 633 | |
| 634 | func cmdAPIKeyRevoke(api *apiclient.Client, id string) { |
| 635 | die(api.RevokeAPIKey(id)) |
| 636 | fmt.Printf("API key revoked: %s\n", id) |
| 637 | } |
| 638 | |
| 639 | // --- topology --- |
| 640 | |
| 641 | func cmdTopologyList(api *apiclient.Client, asJSON bool) { |
| 642 | raw, err := api.GetTopology() |
| 643 | die(err) |
| 644 | if asJSON { |
| 645 | printJSON(raw) |
| 646 | return |
| 647 | } |
| 648 | var data struct { |
| 649 | StaticChannels []string `json:"static_channels"` |
| 650 | Types []struct { |
| 651 | Name string `json:"name"` |
| 652 | Prefix string `json:"prefix"` |
| 653 | Autojoin []string `json:"autojoin"` |
| 654 | Ephemeral bool `json:"ephemeral"` |
| 655 | TTL int64 `json:"ttl_seconds"` |
| 656 | } `json:"types"` |
| 657 | } |
| 658 | must(json.Unmarshal(raw, &data)) |
| 659 | |
| 660 | tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) |
| 661 | fmt.Fprintln(tw, "STATIC CHANNELS") |
| 662 | for _, ch := range data.StaticChannels { |
| 663 | fmt.Fprintf(tw, " %s\n", ch) |
| 664 | } |
| 665 | if len(data.Types) > 0 { |
| 666 | fmt.Fprintln(tw, "\nCHANNEL TYPES") |
| 667 | fmt.Fprintln(tw, " NAME\tPREFIX\tAUTOJOIN\tEPHEMERAL\tTTL") |
| 668 | for _, t := range data.Types { |
| 669 | ttl := "—" |
| 670 | if t.TTL > 0 { |
| 671 | ttl = fmt.Sprintf("%dh", t.TTL/3600) |
| 672 | } |
| 673 | eph := "no" |
| 674 | if t.Ephemeral { |
| 675 | eph = "yes" |
| 676 | } |
| 677 | fmt.Fprintf(tw, " %s\t#%s*\t%s\t%s\t%s\n", t.Name, t.Prefix, strings.Join(t.Autojoin, ","), eph, ttl) |
| 678 | } |
| 679 | } |
| 680 | tw.Flush() |
| 681 | } |
| 682 | |
| 683 | func cmdTopologyProvision(api *apiclient.Client, channel string, asJSON bool) { |
| 684 | if !strings.HasPrefix(channel, "#") { |
| 685 | channel = "#" + channel |
| 686 | } |
| 687 | raw, err := api.ProvisionChannel(channel) |
| 688 | die(err) |
| 689 | if asJSON { |
| 690 | printJSON(raw) |
| 691 | return |
| 692 | } |
| 693 | fmt.Printf("Channel provisioned: %s\n", channel) |
| 694 | } |
| 695 | |
| 696 | func cmdTopologyDrop(api *apiclient.Client, channel string) { |
| 697 | if !strings.HasPrefix(channel, "#") { |
| 698 | channel = "#" + channel |
| 699 | } |
| 700 | die(api.DropChannel(channel)) |
| 701 | fmt.Printf("Channel dropped: %s\n", channel) |
| 702 | } |
| 703 | |
| 704 | // --- config --- |
| 705 | |
| 706 | func cmdConfigShow(api *apiclient.Client, asJSON bool) { |
| 707 | raw, err := api.GetConfig() |
| 708 | die(err) |
| 709 | printJSON(raw) // always JSON — config is a complex nested object |
| 710 | } |
| 711 | |
| 712 | func cmdConfigHistory(api *apiclient.Client, asJSON bool) { |
| 713 | raw, err := api.GetConfigHistory() |
| 714 | die(err) |
| 715 | if asJSON { |
| 716 | printJSON(raw) |
| 717 | return |
| 718 | } |
| 719 | var data struct { |
| 720 | Entries []struct { |
| 721 | Filename string `json:"filename"` |
| 722 | At string `json:"at"` |
| 723 | } `json:"entries"` |
| 724 | } |
| 725 | must(json.Unmarshal(raw, &data)) |
| 726 | if len(data.Entries) == 0 { |
| 727 | fmt.Println("no config history") |
| 728 | return |
| 729 | } |
| 730 | tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) |
| 731 | fmt.Fprintln(tw, "SNAPSHOT\tTIME") |
| 732 | for _, e := range data.Entries { |
| 733 | fmt.Fprintf(tw, "%s\t%s\n", e.Filename, e.At) |
| 734 | } |
| 735 | tw.Flush() |
| 736 | } |
| 737 | |
| 738 | // --- bots --- |
| 739 | |
| 740 | func cmdBotList(api *apiclient.Client, asJSON bool) { |
| 741 | raw, err := api.GetSettings() |
| 742 | die(err) |
| 743 | if asJSON { |
| 744 | printJSON(raw) |
| 745 | return |
| 746 | } |
| 747 | var data struct { |
| 748 | Policies struct { |
| 749 | Behaviors []struct { |
| 750 | ID string `json:"id"` |
| 751 | Name string `json:"name"` |
| 752 | Nick string `json:"nick"` |
| 753 | Enabled bool `json:"enabled"` |
| 754 | } `json:"behaviors"` |
| 755 | } `json:"policies"` |
| 756 | } |
| 757 | must(json.Unmarshal(raw, &data)) |
| 758 | tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) |
| 759 | fmt.Fprintln(tw, "BOT\tNICK\tSTATUS") |
| 760 | for _, b := range data.Policies.Behaviors { |
| 761 | status := "disabled" |
| 762 | if b.Enabled { |
| 763 | status = "enabled" |
| 764 | } |
| 765 | fmt.Fprintf(tw, "%s\t%s\t%s\n", b.Name, b.Nick, status) |
| 766 | } |
| 767 | tw.Flush() |
| 768 | } |
| 769 | |
| 770 | func usage() { |
| 771 | fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI |
| 772 | |
| 773 | Usage: |
| @@ -625,10 +802,16 @@ | |
| 802 | admin remove <username> remove admin |
| 803 | admin passwd <username> change admin password (prompts) |
| 804 | api-key list list API keys |
| 805 | api-key create --name <name> --scopes <s1,s2> [--expires 720h] |
| 806 | api-key revoke <id> revoke an API key |
| 807 | topology list show topology (static channels, types) |
| 808 | topology provision #channel provision a new channel via ChanServ |
| 809 | topology drop #channel drop a channel |
| 810 | config show dump current config (JSON) |
| 811 | config history show config change history |
| 812 | bot list show system bot status |
| 813 | `, version) |
| 814 | } |
| 815 | |
| 816 | func printJSON(raw json.RawMessage) { |
| 817 | var buf []byte |
| 818 |