ScuttleBot

scuttlebot / cmd / scuttlectl / main.go
Source Blame History 665 lines
4c4aa94… lmata 1 // scuttlectl is the CLI for observing and managing a scuttlebot instance.
4c4aa94… lmata 2 //
4c4aa94… lmata 3 // Usage:
4c4aa94… lmata 4 //
4c4aa94… lmata 5 // scuttlectl [--url URL] [--token TOKEN] [--json] <command> [args]
4c4aa94… lmata 6 //
4c4aa94… lmata 7 // Environment variables:
4c4aa94… lmata 8 //
4c4aa94… lmata 9 // SCUTTLEBOT_URL API base URL (default: http://localhost:8080)
4c4aa94… lmata 10 // SCUTTLEBOT_TOKEN API bearer token
cadb504… lmata 11 package main
cadb504… lmata 12
4c4aa94… lmata 13 import (
4c4aa94… lmata 14 "encoding/json"
4c4aa94… lmata 15 "flag"
4c4aa94… lmata 16 "fmt"
4c4aa94… lmata 17 "os"
4c4aa94… lmata 18 "strings"
4c4aa94… lmata 19 "text/tabwriter"
4c4aa94… lmata 20
4c4aa94… lmata 21 "github.com/conflicthq/scuttlebot/cmd/scuttlectl/internal/apiclient"
4c4aa94… lmata 22 )
cadb504… lmata 23
cadb504… lmata 24 var version = "dev"
cadb504… lmata 25
cadb504… lmata 26 func main() {
4c4aa94… lmata 27 // Global flags.
4c4aa94… lmata 28 urlFlag := flag.String("url", envOr("SCUTTLEBOT_URL", "http://localhost:8080"), "scuttlebot API base URL")
4c4aa94… lmata 29 tokenFlag := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "API bearer token")
4c4aa94… lmata 30 jsonFlag := flag.Bool("json", false, "output raw JSON")
4c4aa94… lmata 31 versionFlag := flag.Bool("version", false, "print version and exit")
4c4aa94… lmata 32 flag.Usage = usage
4c4aa94… lmata 33 flag.Parse()
4c4aa94… lmata 34
4c4aa94… lmata 35 if *versionFlag {
4c4aa94… lmata 36 fmt.Println(version)
4c4aa94… lmata 37 return
4c4aa94… lmata 38 }
4c4aa94… lmata 39
4c4aa94… lmata 40 args := flag.Args()
4c4aa94… lmata 41 if len(args) == 0 {
4c4aa94… lmata 42 usage()
4c4aa94… lmata 43 os.Exit(1)
4c4aa94… lmata 44 }
4c4aa94… lmata 45
5ac549c… lmata 46 switch args[0] {
5ac549c… lmata 47 case "setup":
5ac549c… lmata 48 cfgPath := "scuttlebot.yaml"
5ac549c… lmata 49 if len(args) > 1 {
5ac549c… lmata 50 cfgPath = args[1]
5ac549c… lmata 51 }
5ac549c… lmata 52 cmdSetup(cfgPath)
5ac549c… lmata 53 return
5ac549c… lmata 54 }
5ac549c… lmata 55
4c4aa94… lmata 56 if *tokenFlag == "" {
4c4aa94… lmata 57 fmt.Fprintln(os.Stderr, "error: API token required (set SCUTTLEBOT_TOKEN or use --token)")
4c4aa94… lmata 58 os.Exit(1)
4c4aa94… lmata 59 }
4c4aa94… lmata 60
4c4aa94… lmata 61 api := apiclient.New(*urlFlag, *tokenFlag)
4c4aa94… lmata 62
4c4aa94… lmata 63 switch args[0] {
4c4aa94… lmata 64 case "status":
4c4aa94… lmata 65 cmdStatus(api, *jsonFlag)
4c4aa94… lmata 66 case "agents", "agent":
4c4aa94… lmata 67 if len(args) < 2 {
4c4aa94… lmata 68 fmt.Fprintf(os.Stderr, "usage: scuttlectl %s <subcommand>\n", args[0])
4c4aa94… lmata 69 os.Exit(1)
4c4aa94… lmata 70 }
4c4aa94… lmata 71 switch args[1] {
4c4aa94… lmata 72 case "list":
4c4aa94… lmata 73 cmdAgentList(api, *jsonFlag)
4c4aa94… lmata 74 case "get":
4c4aa94… lmata 75 requireArgs(args, 3, "scuttlectl agent get <nick>")
4c4aa94… lmata 76 cmdAgentGet(api, args[2], *jsonFlag)
4c4aa94… lmata 77 case "register":
4c4aa94… lmata 78 requireArgs(args, 3, "scuttlectl agent register <nick> [--type worker] [--channels #a,#b]")
4c4aa94… lmata 79 cmdAgentRegister(api, args[2:], *jsonFlag)
4c4aa94… lmata 80 case "revoke":
4c4aa94… lmata 81 requireArgs(args, 3, "scuttlectl agent revoke <nick>")
4c4aa94… lmata 82 cmdAgentRevoke(api, args[2])
5ac549c… lmata 83 case "delete":
5ac549c… lmata 84 requireArgs(args, 3, "scuttlectl agent delete <nick>")
5ac549c… lmata 85 cmdAgentDelete(api, args[2])
4c4aa94… lmata 86 case "rotate":
4c4aa94… lmata 87 requireArgs(args, 3, "scuttlectl agent rotate <nick>")
4c4aa94… lmata 88 cmdAgentRotate(api, args[2], *jsonFlag)
4c4aa94… lmata 89 default:
4c4aa94… lmata 90 fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n", args[1])
4c4aa94… lmata 91 os.Exit(1)
4c4aa94… lmata 92 }
5ac549c… lmata 93 case "admin":
5ac549c… lmata 94 if len(args) < 2 {
5ac549c… lmata 95 fmt.Fprintf(os.Stderr, "usage: scuttlectl admin <subcommand>\n")
5ac549c… lmata 96 os.Exit(1)
5ac549c… lmata 97 }
5ac549c… lmata 98 switch args[1] {
5ac549c… lmata 99 case "list":
5ac549c… lmata 100 cmdAdminList(api, *jsonFlag)
5ac549c… lmata 101 case "add":
5ac549c… lmata 102 requireArgs(args, 3, "scuttlectl admin add <username>")
5ac549c… lmata 103 cmdAdminAdd(api, args[2])
5ac549c… lmata 104 case "remove":
5ac549c… lmata 105 requireArgs(args, 3, "scuttlectl admin remove <username>")
5ac549c… lmata 106 cmdAdminRemove(api, args[2])
5ac549c… lmata 107 case "passwd":
5ac549c… lmata 108 requireArgs(args, 3, "scuttlectl admin passwd <username>")
5ac549c… lmata 109 cmdAdminPasswd(api, args[2])
5ac549c… lmata 110 default:
5ac549c… lmata 111 fmt.Fprintf(os.Stderr, "unknown subcommand: admin %s\n", args[1])
5ac549c… lmata 112 os.Exit(1)
5ac549c… lmata 113 }
68677f9… noreply 114 case "api-key", "api-keys":
68677f9… noreply 115 if len(args) < 2 {
68677f9… noreply 116 fmt.Fprintf(os.Stderr, "usage: scuttlectl api-key <list|create|revoke>\n")
68677f9… noreply 117 os.Exit(1)
68677f9… noreply 118 }
68677f9… noreply 119 switch args[1] {
68677f9… noreply 120 case "list":
68677f9… noreply 121 cmdAPIKeyList(api, *jsonFlag)
68677f9… noreply 122 case "create":
68677f9… noreply 123 requireArgs(args, 3, "scuttlectl api-key create --name <name> --scopes <scope1,scope2>")
68677f9… noreply 124 cmdAPIKeyCreate(api, args[2:], *jsonFlag)
68677f9… noreply 125 case "revoke":
68677f9… noreply 126 requireArgs(args, 3, "scuttlectl api-key revoke <id>")
68677f9… noreply 127 cmdAPIKeyRevoke(api, args[2])
68677f9… noreply 128 default:
68677f9… noreply 129 fmt.Fprintf(os.Stderr, "unknown subcommand: api-key %s\n", args[1])
68677f9… noreply 130 os.Exit(1)
68677f9… noreply 131 }
5ac549c… lmata 132 case "channels", "channel":
5ac549c… lmata 133 if len(args) < 2 {
5ac549c… lmata 134 fmt.Fprintf(os.Stderr, "usage: scuttlectl channels <list|users <channel>>\n")
5ac549c… lmata 135 os.Exit(1)
5ac549c… lmata 136 }
5ac549c… lmata 137 switch args[1] {
5ac549c… lmata 138 case "list":
5ac549c… lmata 139 cmdChannelList(api, *jsonFlag)
5ac549c… lmata 140 case "users":
5ac549c… lmata 141 requireArgs(args, 3, "scuttlectl channels users <channel>")
5ac549c… lmata 142 cmdChannelUsers(api, args[2], *jsonFlag)
5ac549c… lmata 143 case "delete", "rm":
5ac549c… lmata 144 requireArgs(args, 3, "scuttlectl channels delete <channel>")
5ac549c… lmata 145 cmdChannelDelete(api, args[2])
5ac549c… lmata 146 default:
5ac549c… lmata 147 fmt.Fprintf(os.Stderr, "unknown subcommand: channels %s\n", args[1])
5ac549c… lmata 148 os.Exit(1)
5ac549c… lmata 149 }
5ac549c… lmata 150 case "backend", "backends":
5ac549c… lmata 151 if len(args) < 2 {
73ef90f… lmata 152 fmt.Fprintf(os.Stderr, "usage: scuttlectl backend <list|get|delete|rename> [args]\n")
4c4aa94… lmata 153 os.Exit(1)
4c4aa94… lmata 154 }
5ac549c… lmata 155 switch args[1] {
73ef90f… lmata 156 case "list":
73ef90f… lmata 157 cmdBackendList(api, *jsonFlag)
73ef90f… lmata 158 case "get":
73ef90f… lmata 159 requireArgs(args, 3, "scuttlectl backend get <name>")
73ef90f… lmata 160 cmdBackendGet(api, args[2], *jsonFlag)
73ef90f… lmata 161 case "delete", "rm":
73ef90f… lmata 162 requireArgs(args, 3, "scuttlectl backend delete <name>")
73ef90f… lmata 163 cmdBackendDelete(api, args[2])
5ac549c… lmata 164 case "rename":
5ac549c… lmata 165 requireArgs(args, 4, "scuttlectl backend rename <old-name> <new-name>")
5ac549c… lmata 166 cmdBackendRename(api, args[2], args[3])
5ac549c… lmata 167 default:
5ac549c… lmata 168 fmt.Fprintf(os.Stderr, "unknown subcommand: backend %s\n", args[1])
5ac549c… lmata 169 os.Exit(1)
5ac549c… lmata 170 }
4c4aa94… lmata 171 default:
4c4aa94… lmata 172 fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
4c4aa94… lmata 173 usage()
4c4aa94… lmata 174 os.Exit(1)
4c4aa94… lmata 175 }
4c4aa94… lmata 176 }
4c4aa94… lmata 177
4c4aa94… lmata 178 func cmdStatus(api *apiclient.Client, asJSON bool) {
4c4aa94… lmata 179 raw, err := api.Status()
4c4aa94… lmata 180 die(err)
4c4aa94… lmata 181 if asJSON {
4c4aa94… lmata 182 printJSON(raw)
4c4aa94… lmata 183 return
4c4aa94… lmata 184 }
4c4aa94… lmata 185
4c4aa94… lmata 186 var s struct {
4c4aa94… lmata 187 Status string `json:"status"`
4c4aa94… lmata 188 Uptime string `json:"uptime"`
4c4aa94… lmata 189 Agents int `json:"agents"`
4c4aa94… lmata 190 Started string `json:"started"`
4c4aa94… lmata 191 }
4c4aa94… lmata 192 must(json.Unmarshal(raw, &s))
4c4aa94… lmata 193
4c4aa94… lmata 194 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
4c4aa94… lmata 195 fmt.Fprintf(tw, "status\t%s\n", s.Status)
4c4aa94… lmata 196 fmt.Fprintf(tw, "uptime\t%s\n", s.Uptime)
4c4aa94… lmata 197 fmt.Fprintf(tw, "agents\t%d\n", s.Agents)
4c4aa94… lmata 198 fmt.Fprintf(tw, "started\t%s\n", s.Started)
4c4aa94… lmata 199 tw.Flush()
4c4aa94… lmata 200 }
4c4aa94… lmata 201
4c4aa94… lmata 202 func cmdAgentList(api *apiclient.Client, asJSON bool) {
4c4aa94… lmata 203 raw, err := api.ListAgents()
4c4aa94… lmata 204 die(err)
4c4aa94… lmata 205 if asJSON {
4c4aa94… lmata 206 printJSON(raw)
4c4aa94… lmata 207 return
4c4aa94… lmata 208 }
4c4aa94… lmata 209
4c4aa94… lmata 210 var body struct {
4c4aa94… lmata 211 Agents []struct {
4c4aa94… lmata 212 Nick string `json:"nick"`
4c4aa94… lmata 213 Type string `json:"type"`
4c4aa94… lmata 214 Channels []string `json:"channels"`
4c4aa94… lmata 215 Revoked bool `json:"revoked"`
4c4aa94… lmata 216 } `json:"agents"`
4c4aa94… lmata 217 }
4c4aa94… lmata 218 must(json.Unmarshal(raw, &body))
4c4aa94… lmata 219
4c4aa94… lmata 220 if len(body.Agents) == 0 {
4c4aa94… lmata 221 fmt.Println("no agents registered")
4c4aa94… lmata 222 return
4c4aa94… lmata 223 }
4c4aa94… lmata 224
4c4aa94… lmata 225 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
4c4aa94… lmata 226 fmt.Fprintln(tw, "NICK\tTYPE\tCHANNELS\tSTATUS")
4c4aa94… lmata 227 for _, a := range body.Agents {
4c4aa94… lmata 228 status := "active"
4c4aa94… lmata 229 if a.Revoked {
4c4aa94… lmata 230 status = "revoked"
4c4aa94… lmata 231 }
4c4aa94… lmata 232 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", a.Nick, a.Type, strings.Join(a.Channels, ","), status)
4c4aa94… lmata 233 }
4c4aa94… lmata 234 tw.Flush()
4c4aa94… lmata 235 }
4c4aa94… lmata 236
4c4aa94… lmata 237 func cmdAgentGet(api *apiclient.Client, nick string, asJSON bool) {
4c4aa94… lmata 238 raw, err := api.GetAgent(nick)
4c4aa94… lmata 239 die(err)
4c4aa94… lmata 240 if asJSON {
4c4aa94… lmata 241 printJSON(raw)
4c4aa94… lmata 242 return
4c4aa94… lmata 243 }
4c4aa94… lmata 244
4c4aa94… lmata 245 var a struct {
4c4aa94… lmata 246 Nick string `json:"nick"`
4c4aa94… lmata 247 Type string `json:"type"`
4c4aa94… lmata 248 Channels []string `json:"channels"`
4c4aa94… lmata 249 Revoked bool `json:"revoked"`
4c4aa94… lmata 250 }
4c4aa94… lmata 251 must(json.Unmarshal(raw, &a))
4c4aa94… lmata 252
4c4aa94… lmata 253 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
4c4aa94… lmata 254 status := "active"
4c4aa94… lmata 255 if a.Revoked {
4c4aa94… lmata 256 status = "revoked"
4c4aa94… lmata 257 }
4c4aa94… lmata 258 fmt.Fprintf(tw, "nick\t%s\n", a.Nick)
4c4aa94… lmata 259 fmt.Fprintf(tw, "type\t%s\n", a.Type)
4c4aa94… lmata 260 fmt.Fprintf(tw, "channels\t%s\n", strings.Join(a.Channels, ", "))
4c4aa94… lmata 261 fmt.Fprintf(tw, "status\t%s\n", status)
4c4aa94… lmata 262 tw.Flush()
4c4aa94… lmata 263 }
4c4aa94… lmata 264
4c4aa94… lmata 265 func cmdAgentRegister(api *apiclient.Client, args []string, asJSON bool) {
4c4aa94… lmata 266 nick := args[0]
4c4aa94… lmata 267 var channels []string
4c4aa94… lmata 268
4c4aa94… lmata 269 // Parse optional --type and --channels from remaining args.
4c4aa94… lmata 270 fs := flag.NewFlagSet("agent register", flag.ExitOnError)
4c4aa94… lmata 271 typeFlag := fs.String("type", "worker", "agent type (worker, orchestrator, observer)")
4c4aa94… lmata 272 channelsFlag := fs.String("channels", "", "comma-separated list of channels to join")
4c4aa94… lmata 273 _ = fs.Parse(args[1:])
0e244d2… lmata 274 agentType := *typeFlag
4c4aa94… lmata 275 if *channelsFlag != "" {
4c4aa94… lmata 276 for _, ch := range strings.Split(*channelsFlag, ",") {
4c4aa94… lmata 277 if ch = strings.TrimSpace(ch); ch != "" {
4c4aa94… lmata 278 channels = append(channels, ch)
4c4aa94… lmata 279 }
4c4aa94… lmata 280 }
4c4aa94… lmata 281 }
4c4aa94… lmata 282
4c4aa94… lmata 283 raw, err := api.RegisterAgent(nick, agentType, channels)
4c4aa94… lmata 284 die(err)
4c4aa94… lmata 285 if asJSON {
4c4aa94… lmata 286 printJSON(raw)
4c4aa94… lmata 287 return
4c4aa94… lmata 288 }
4c4aa94… lmata 289
4c4aa94… lmata 290 var body struct {
4c4aa94… lmata 291 Credentials struct {
4c4aa94… lmata 292 Nick string `json:"nick"`
4c4aa94… lmata 293 Password string `json:"password"`
4c4aa94… lmata 294 Server string `json:"server"`
4c4aa94… lmata 295 } `json:"credentials"`
4c4aa94… lmata 296 Payload struct {
4c4aa94… lmata 297 Token string `json:"token"`
4c4aa94… lmata 298 Signature string `json:"signature"`
4c4aa94… lmata 299 } `json:"payload"`
4c4aa94… lmata 300 }
4c4aa94… lmata 301 must(json.Unmarshal(raw, &body))
4c4aa94… lmata 302
4c4aa94… lmata 303 fmt.Printf("Agent registered: %s\n\n", nick)
4c4aa94… lmata 304 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
4c4aa94… lmata 305 fmt.Fprintln(tw, "CREDENTIAL\tVALUE")
4c4aa94… lmata 306 fmt.Fprintf(tw, "nick\t%s\n", body.Credentials.Nick)
4c4aa94… lmata 307 fmt.Fprintf(tw, "password\t%s\n", body.Credentials.Password)
4c4aa94… lmata 308 fmt.Fprintf(tw, "server\t%s\n", body.Credentials.Server)
4c4aa94… lmata 309 tw.Flush()
4c4aa94… lmata 310 fmt.Println("\nStore these credentials — the password will not be shown again.")
4c4aa94… lmata 311 }
4c4aa94… lmata 312
5ac549c… lmata 313 func cmdAdminList(api *apiclient.Client, asJSON bool) {
5ac549c… lmata 314 raw, err := api.ListAdmins()
5ac549c… lmata 315 die(err)
5ac549c… lmata 316 if asJSON {
5ac549c… lmata 317 printJSON(raw)
5ac549c… lmata 318 return
5ac549c… lmata 319 }
5ac549c… lmata 320
5ac549c… lmata 321 var body struct {
5ac549c… lmata 322 Admins []struct {
5ac549c… lmata 323 Username string `json:"username"`
5ac549c… lmata 324 Created string `json:"created"`
5ac549c… lmata 325 } `json:"admins"`
5ac549c… lmata 326 }
5ac549c… lmata 327 must(json.Unmarshal(raw, &body))
5ac549c… lmata 328
5ac549c… lmata 329 if len(body.Admins) == 0 {
5ac549c… lmata 330 fmt.Println("no admin accounts")
5ac549c… lmata 331 return
5ac549c… lmata 332 }
5ac549c… lmata 333
5ac549c… lmata 334 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
5ac549c… lmata 335 fmt.Fprintln(tw, "USERNAME\tCREATED")
5ac549c… lmata 336 for _, a := range body.Admins {
5ac549c… lmata 337 fmt.Fprintf(tw, "%s\t%s\n", a.Username, a.Created)
5ac549c… lmata 338 }
5ac549c… lmata 339 tw.Flush()
5ac549c… lmata 340 }
5ac549c… lmata 341
5ac549c… lmata 342 func cmdAdminAdd(api *apiclient.Client, username string) {
5ac549c… lmata 343 pass := promptPassword()
5ac549c… lmata 344 _, err := api.AddAdmin(username, pass)
5ac549c… lmata 345 die(err)
5ac549c… lmata 346 fmt.Printf("Admin added: %s\n", username)
5ac549c… lmata 347 }
5ac549c… lmata 348
5ac549c… lmata 349 func cmdAdminRemove(api *apiclient.Client, username string) {
5ac549c… lmata 350 die(api.RemoveAdmin(username))
5ac549c… lmata 351 fmt.Printf("Admin removed: %s\n", username)
5ac549c… lmata 352 }
5ac549c… lmata 353
5ac549c… lmata 354 func cmdAdminPasswd(api *apiclient.Client, username string) {
5ac549c… lmata 355 pass := promptPassword()
5ac549c… lmata 356 die(api.SetAdminPassword(username, pass))
5ac549c… lmata 357 fmt.Printf("Password updated for: %s\n", username)
5ac549c… lmata 358 }
5ac549c… lmata 359
5ac549c… lmata 360 func promptPassword() string {
5ac549c… lmata 361 fmt.Fprint(os.Stderr, "password: ")
5ac549c… lmata 362 var pass string
f7eb47b… lmata 363 _, _ = fmt.Scanln(&pass)
5ac549c… lmata 364 return pass
5ac549c… lmata 365 }
5ac549c… lmata 366
4c4aa94… lmata 367 func cmdAgentRevoke(api *apiclient.Client, nick string) {
4c4aa94… lmata 368 die(api.RevokeAgent(nick))
4c4aa94… lmata 369 fmt.Printf("Agent revoked: %s\n", nick)
5ac549c… lmata 370 }
5ac549c… lmata 371
5ac549c… lmata 372 func cmdAgentDelete(api *apiclient.Client, nick string) {
5ac549c… lmata 373 die(api.DeleteAgent(nick))
5ac549c… lmata 374 fmt.Printf("Agent deleted: %s\n", nick)
5ac549c… lmata 375 }
5ac549c… lmata 376
5ac549c… lmata 377 func cmdChannelList(api *apiclient.Client, asJSON bool) {
5ac549c… lmata 378 raw, err := api.ListChannels()
5ac549c… lmata 379 die(err)
5ac549c… lmata 380 if asJSON {
5ac549c… lmata 381 printJSON(raw)
5ac549c… lmata 382 return
5ac549c… lmata 383 }
5ac549c… lmata 384 var body struct {
5ac549c… lmata 385 Channels []string `json:"channels"`
5ac549c… lmata 386 }
5ac549c… lmata 387 must(json.Unmarshal(raw, &body))
5ac549c… lmata 388 if len(body.Channels) == 0 {
5ac549c… lmata 389 fmt.Println("no channels")
5ac549c… lmata 390 return
5ac549c… lmata 391 }
5ac549c… lmata 392 for _, ch := range body.Channels {
5ac549c… lmata 393 fmt.Println(ch)
5ac549c… lmata 394 }
5ac549c… lmata 395 }
5ac549c… lmata 396
5ac549c… lmata 397 func cmdChannelUsers(api *apiclient.Client, channel string, asJSON bool) {
5ac549c… lmata 398 raw, err := api.ChannelUsers(channel)
5ac549c… lmata 399 die(err)
5ac549c… lmata 400 if asJSON {
5ac549c… lmata 401 printJSON(raw)
5ac549c… lmata 402 return
5ac549c… lmata 403 }
5ac549c… lmata 404 var body struct {
5ac549c… lmata 405 Users []string `json:"users"`
5ac549c… lmata 406 }
5ac549c… lmata 407 must(json.Unmarshal(raw, &body))
5ac549c… lmata 408 if len(body.Users) == 0 {
5ac549c… lmata 409 fmt.Printf("no users in %s\n", channel)
5ac549c… lmata 410 return
5ac549c… lmata 411 }
5ac549c… lmata 412 for _, u := range body.Users {
5ac549c… lmata 413 fmt.Println(u)
5ac549c… lmata 414 }
5ac549c… lmata 415 }
5ac549c… lmata 416
5ac549c… lmata 417 func cmdChannelDelete(api *apiclient.Client, channel string) {
5ac549c… lmata 418 die(api.DeleteChannel(channel))
5ac549c… lmata 419 fmt.Printf("Channel deleted: #%s\n", strings.TrimPrefix(channel, "#"))
5ac549c… lmata 420 }
5ac549c… lmata 421
73ef90f… lmata 422 func cmdBackendList(api *apiclient.Client, asJSON bool) {
73ef90f… lmata 423 raw, err := api.ListLLMBackends()
73ef90f… lmata 424 die(err)
73ef90f… lmata 425 if asJSON {
73ef90f… lmata 426 printJSON(raw)
73ef90f… lmata 427 return
73ef90f… lmata 428 }
73ef90f… lmata 429 var body struct {
73ef90f… lmata 430 Backends []struct {
73ef90f… lmata 431 Name string `json:"name"`
73ef90f… lmata 432 Provider string `json:"provider"`
73ef90f… lmata 433 } `json:"backends"`
73ef90f… lmata 434 }
73ef90f… lmata 435 must(json.Unmarshal(raw, &body))
73ef90f… lmata 436 if len(body.Backends) == 0 {
73ef90f… lmata 437 fmt.Println("no backends")
73ef90f… lmata 438 return
73ef90f… lmata 439 }
73ef90f… lmata 440 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
73ef90f… lmata 441 fmt.Fprintln(tw, "NAME\tPROVIDER")
73ef90f… lmata 442 for _, b := range body.Backends {
73ef90f… lmata 443 fmt.Fprintf(tw, "%s\t%s\n", b.Name, b.Provider)
73ef90f… lmata 444 }
73ef90f… lmata 445 tw.Flush()
73ef90f… lmata 446 }
73ef90f… lmata 447
73ef90f… lmata 448 func cmdBackendGet(api *apiclient.Client, name string, asJSON bool) {
73ef90f… lmata 449 raw, err := api.GetLLMBackend(name)
73ef90f… lmata 450 die(err)
73ef90f… lmata 451 if asJSON {
73ef90f… lmata 452 printJSON(raw)
73ef90f… lmata 453 return
73ef90f… lmata 454 }
73ef90f… lmata 455 var b struct {
73ef90f… lmata 456 Name string `json:"name"`
73ef90f… lmata 457 Provider string `json:"provider"`
73ef90f… lmata 458 Model string `json:"model"`
73ef90f… lmata 459 BaseURL string `json:"base_url,omitempty"`
73ef90f… lmata 460 }
73ef90f… lmata 461 must(json.Unmarshal(raw, &b))
73ef90f… lmata 462 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
73ef90f… lmata 463 fmt.Fprintf(tw, "name\t%s\n", b.Name)
73ef90f… lmata 464 fmt.Fprintf(tw, "provider\t%s\n", b.Provider)
73ef90f… lmata 465 fmt.Fprintf(tw, "model\t%s\n", b.Model)
73ef90f… lmata 466 if b.BaseURL != "" {
73ef90f… lmata 467 fmt.Fprintf(tw, "base_url\t%s\n", b.BaseURL)
73ef90f… lmata 468 }
73ef90f… lmata 469 tw.Flush()
73ef90f… lmata 470 }
73ef90f… lmata 471
73ef90f… lmata 472 func cmdBackendDelete(api *apiclient.Client, name string) {
73ef90f… lmata 473 die(api.DeleteLLMBackend(name))
73ef90f… lmata 474 fmt.Printf("Backend deleted: %s\n", name)
73ef90f… lmata 475 }
73ef90f… lmata 476
5ac549c… lmata 477 func cmdBackendRename(api *apiclient.Client, oldName, newName string) {
5ac549c… lmata 478 raw, err := api.GetLLMBackend(oldName)
5ac549c… lmata 479 die(err)
5ac549c… lmata 480
5ac549c… lmata 481 var cfg map[string]any
5ac549c… lmata 482 must(json.Unmarshal(raw, &cfg))
5ac549c… lmata 483 cfg["name"] = newName
5ac549c… lmata 484
5ac549c… lmata 485 die(api.CreateLLMBackend(cfg))
5ac549c… lmata 486 die(api.DeleteLLMBackend(oldName))
5ac549c… lmata 487 fmt.Printf("Backend renamed: %s → %s\n", oldName, newName)
4c4aa94… lmata 488 }
4c4aa94… lmata 489
4c4aa94… lmata 490 func cmdAgentRotate(api *apiclient.Client, nick string, asJSON bool) {
4c4aa94… lmata 491 raw, err := api.RotateAgent(nick)
4c4aa94… lmata 492 die(err)
4c4aa94… lmata 493 if asJSON {
4c4aa94… lmata 494 printJSON(raw)
4c4aa94… lmata 495 return
4c4aa94… lmata 496 }
4c4aa94… lmata 497
4c4aa94… lmata 498 var creds struct {
4c4aa94… lmata 499 Nick string `json:"nick"`
4c4aa94… lmata 500 Password string `json:"password"`
4c4aa94… lmata 501 Server string `json:"server"`
4c4aa94… lmata 502 }
4c4aa94… lmata 503 must(json.Unmarshal(raw, &creds))
4c4aa94… lmata 504
4c4aa94… lmata 505 fmt.Printf("Credentials rotated for: %s\n\n", nick)
4c4aa94… lmata 506 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
4c4aa94… lmata 507 fmt.Fprintln(tw, "CREDENTIAL\tVALUE")
4c4aa94… lmata 508 fmt.Fprintf(tw, "nick\t%s\n", creds.Nick)
4c4aa94… lmata 509 fmt.Fprintf(tw, "password\t%s\n", creds.Password)
4c4aa94… lmata 510 fmt.Fprintf(tw, "server\t%s\n", creds.Server)
4c4aa94… lmata 511 tw.Flush()
4c4aa94… lmata 512 fmt.Println("\nStore this password — it will not be shown again.")
4c4aa94… lmata 513 }
4c4aa94… lmata 514
68677f9… noreply 515 func cmdAPIKeyList(api *apiclient.Client, asJSON bool) {
68677f9… noreply 516 raw, err := api.ListAPIKeys()
68677f9… noreply 517 die(err)
68677f9… noreply 518 if asJSON {
68677f9… noreply 519 printJSON(raw)
68677f9… noreply 520 return
68677f9… noreply 521 }
68677f9… noreply 522
68677f9… noreply 523 var keys []struct {
68677f9… noreply 524 ID string `json:"id"`
68677f9… noreply 525 Name string `json:"name"`
68677f9… noreply 526 Scopes []string `json:"scopes"`
68677f9… noreply 527 CreatedAt string `json:"created_at"`
68677f9… noreply 528 LastUsed *string `json:"last_used"`
68677f9… noreply 529 ExpiresAt *string `json:"expires_at"`
68677f9… noreply 530 Active bool `json:"active"`
68677f9… noreply 531 }
68677f9… noreply 532 must(json.Unmarshal(raw, &keys))
68677f9… noreply 533
68677f9… noreply 534 if len(keys) == 0 {
68677f9… noreply 535 fmt.Println("no API keys")
68677f9… noreply 536 return
68677f9… noreply 537 }
68677f9… noreply 538
68677f9… noreply 539 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
68677f9… noreply 540 fmt.Fprintln(tw, "ID\tNAME\tSCOPES\tACTIVE\tLAST USED")
68677f9… noreply 541 for _, k := range keys {
68677f9… noreply 542 lastUsed := "-"
68677f9… noreply 543 if k.LastUsed != nil {
68677f9… noreply 544 lastUsed = *k.LastUsed
68677f9… noreply 545 }
68677f9… noreply 546 status := "yes"
68677f9… noreply 547 if !k.Active {
68677f9… noreply 548 status = "revoked"
68677f9… noreply 549 }
68677f9… noreply 550 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", k.ID, k.Name, strings.Join(k.Scopes, ","), status, lastUsed)
68677f9… noreply 551 }
68677f9… noreply 552 tw.Flush()
68677f9… noreply 553 }
68677f9… noreply 554
68677f9… noreply 555 func cmdAPIKeyCreate(api *apiclient.Client, args []string, asJSON bool) {
68677f9… noreply 556 fs := flag.NewFlagSet("api-key create", flag.ExitOnError)
68677f9… noreply 557 nameFlag := fs.String("name", "", "key name (required)")
68677f9… noreply 558 scopesFlag := fs.String("scopes", "", "comma-separated scopes (required)")
68677f9… noreply 559 expiresFlag := fs.String("expires", "", "expiry duration (e.g. 720h for 30 days)")
68677f9… noreply 560 _ = fs.Parse(args)
68677f9… noreply 561
68677f9… noreply 562 if *nameFlag == "" || *scopesFlag == "" {
68677f9… noreply 563 fmt.Fprintln(os.Stderr, "usage: scuttlectl api-key create --name <name> --scopes <scope1,scope2> [--expires 720h]")
68677f9… noreply 564 os.Exit(1)
68677f9… noreply 565 }
68677f9… noreply 566
68677f9… noreply 567 scopes := strings.Split(*scopesFlag, ",")
68677f9… noreply 568 raw, err := api.CreateAPIKey(*nameFlag, scopes, *expiresFlag)
68677f9… noreply 569 die(err)
68677f9… noreply 570
68677f9… noreply 571 if asJSON {
68677f9… noreply 572 printJSON(raw)
68677f9… noreply 573 return
68677f9… noreply 574 }
68677f9… noreply 575
68677f9… noreply 576 var key struct {
68677f9… noreply 577 ID string `json:"id"`
68677f9… noreply 578 Name string `json:"name"`
68677f9… noreply 579 Token string `json:"token"`
68677f9… noreply 580 }
68677f9… noreply 581 must(json.Unmarshal(raw, &key))
68677f9… noreply 582
68677f9… noreply 583 fmt.Printf("API key created: %s\n\n", key.Name)
68677f9… noreply 584 fmt.Printf(" Token: %s\n\n", key.Token)
68677f9… noreply 585 fmt.Println("Store this token — it will not be shown again.")
68677f9… noreply 586 }
68677f9… noreply 587
68677f9… noreply 588 func cmdAPIKeyRevoke(api *apiclient.Client, id string) {
68677f9… noreply 589 die(api.RevokeAPIKey(id))
68677f9… noreply 590 fmt.Printf("API key revoked: %s\n", id)
68677f9… noreply 591 }
68677f9… noreply 592
4c4aa94… lmata 593 func usage() {
4c4aa94… lmata 594 fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI
4c4aa94… lmata 595
4c4aa94… lmata 596 Usage:
4c4aa94… lmata 597 scuttlectl [flags] <command> [subcommand] [args]
4c4aa94… lmata 598
4c4aa94… lmata 599 Global flags:
4c4aa94… lmata 600 --url API base URL (default: $SCUTTLEBOT_URL or http://localhost:8080)
4c4aa94… lmata 601 --token API bearer token (default: $SCUTTLEBOT_TOKEN)
4c4aa94… lmata 602 --json output raw JSON
4c4aa94… lmata 603 --version print version and exit
4c4aa94… lmata 604
4c4aa94… lmata 605 Commands:
5ac549c… lmata 606 setup [path] interactive wizard — write scuttlebot.yaml (no token needed)
4c4aa94… lmata 607 status daemon + ergo health
4c4aa94… lmata 608 agents list list all registered agents
4c4aa94… lmata 609 agent get <nick> get a single agent
4c4aa94… lmata 610 agent register <nick> register a new agent, print credentials
73ef90f… lmata 611 [--type worker|orchestrator|observer|operator]
4c4aa94… lmata 612 [--channels #a,#b,#c]
4c4aa94… lmata 613 agent revoke <nick> revoke agent credentials
5ac549c… lmata 614 agent delete <nick> permanently remove agent from registry
4c4aa94… lmata 615 agent rotate <nick> rotate agent password
5ac549c… lmata 616 channels list list active channels
5ac549c… lmata 617 channels users <channel> list users in a channel
5ac549c… lmata 618 channels delete <channel> part bridge from channel (closes when empty)
73ef90f… lmata 619 backend list list LLM backends
73ef90f… lmata 620 backend get <name> show a single backend
73ef90f… lmata 621 backend delete <name> remove a backend
73ef90f… lmata 622 backend rename <old> <new> rename a backend
5ac549c… lmata 623 admin list list admin accounts
5ac549c… lmata 624 admin add <username> add admin (prompts for password)
5ac549c… lmata 625 admin remove <username> remove admin
5ac549c… lmata 626 admin passwd <username> change admin password (prompts)
68677f9… noreply 627 api-key list list API keys
68677f9… noreply 628 api-key create --name <name> --scopes <s1,s2> [--expires 720h]
68677f9… noreply 629 api-key revoke <id> revoke an API key
4c4aa94… lmata 630 `, version)
4c4aa94… lmata 631 }
4c4aa94… lmata 632
4c4aa94… lmata 633 func printJSON(raw json.RawMessage) {
4c4aa94… lmata 634 var buf []byte
4c4aa94… lmata 635 buf, _ = json.MarshalIndent(raw, "", " ")
4c4aa94… lmata 636 fmt.Println(string(buf))
4c4aa94… lmata 637 }
4c4aa94… lmata 638
4c4aa94… lmata 639 func requireArgs(args []string, n int, usage string) {
4c4aa94… lmata 640 if len(args) < n {
4c4aa94… lmata 641 fmt.Fprintf(os.Stderr, "usage: %s\n", usage)
4c4aa94… lmata 642 os.Exit(1)
4c4aa94… lmata 643 }
4c4aa94… lmata 644 }
4c4aa94… lmata 645
4c4aa94… lmata 646 func die(err error) {
4c4aa94… lmata 647 if err != nil {
4c4aa94… lmata 648 fmt.Fprintln(os.Stderr, "error:", err)
4c4aa94… lmata 649 os.Exit(1)
4c4aa94… lmata 650 }
4c4aa94… lmata 651 }
4c4aa94… lmata 652
4c4aa94… lmata 653 func must(err error) {
4c4aa94… lmata 654 if err != nil {
4c4aa94… lmata 655 fmt.Fprintln(os.Stderr, "internal error:", err)
4c4aa94… lmata 656 os.Exit(1)
4c4aa94… lmata 657 }
4c4aa94… lmata 658 }
4c4aa94… lmata 659
4c4aa94… lmata 660 func envOr(key, def string) string {
4c4aa94… lmata 661 if v := os.Getenv(key); v != "" {
4c4aa94… lmata 662 return v
4c4aa94… lmata 663 }
4c4aa94… lmata 664 return def
cadb504… lmata 665 }

Keyboard Shortcuts

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