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.

lmata 2026-04-05 16:45 trunk
Commit 1d115cc9ccddbeecca769324c707f7e94e29ff7ecffa73d6b51be7a96cc457ee
1 file changed +183
--- cmd/scuttlectl/main.go
+++ cmd/scuttlectl/main.go
@@ -166,10 +166,54 @@
166166
cmdBackendRename(api, args[2], args[3])
167167
default:
168168
fmt.Fprintf(os.Stderr, "unknown subcommand: backend %s\n", args[1])
169169
os.Exit(1)
170170
}
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
+ }
171215
default:
172216
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
173217
usage()
174218
os.Exit(1)
175219
}
@@ -509,10 +553,12 @@
509553
fmt.Fprintf(tw, "password\t%s\n", creds.Password)
510554
fmt.Fprintf(tw, "server\t%s\n", creds.Server)
511555
tw.Flush()
512556
fmt.Println("\nStore this password — it will not be shown again.")
513557
}
558
+
559
+// --- api-keys ---
514560
515561
func cmdAPIKeyList(api *apiclient.Client, asJSON bool) {
516562
raw, err := api.ListAPIKeys()
517563
die(err)
518564
if asJSON {
@@ -587,10 +633,141 @@
587633
588634
func cmdAPIKeyRevoke(api *apiclient.Client, id string) {
589635
die(api.RevokeAPIKey(id))
590636
fmt.Printf("API key revoked: %s\n", id)
591637
}
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
+}
592769
593770
func usage() {
594771
fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI
595772
596773
Usage:
@@ -625,10 +802,16 @@
625802
admin remove <username> remove admin
626803
admin passwd <username> change admin password (prompts)
627804
api-key list list API keys
628805
api-key create --name <name> --scopes <s1,s2> [--expires 720h]
629806
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
630813
`, version)
631814
}
632815
633816
func printJSON(raw json.RawMessage) {
634817
var buf []byte
635818
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button