frame out cli tool

This commit is contained in:
2026-01-17 17:06:32 -05:00
parent 7661bff6f1
commit edc86feccd
14 changed files with 800 additions and 0 deletions

65
CLAUDE.md Normal file
View File

@@ -0,0 +1,65 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build and Test Commands
```bash
# Build library packages
go build ./pkg/...
# Build CLI tool
go build ./cmd/...
# Run all tests
go test ./...
# Run tests for a specific package
go test ./pkg/toughswitch/...
# Run a single test
go test ./pkg/toughswitch/... -run TestClient_AddDel
# Tidy module dependencies
go mod tidy
```
## Architecture
This repository provides Go client libraries for interacting with Ubiquiti network devices via reverse-engineered REST APIs.
### Package Structure
- `pkg/toughswitch/` - Client for ToughSwitch devices (e.g., TS-8-PRO)
- `pkg/edgeos/` - Client for EdgeOS devices (EdgeRouter, EdgeSwitch)
- `cmd/` - Optional CLI tool using cobra (work in progress)
### Client Design Pattern
Both packages follow the same multi-device client pattern:
1. **Client** - Top-level struct holding a map of `deviceClient` instances keyed by host
2. **deviceClient** - Per-device HTTP client with authentication state (token for ToughSwitch, cookies for EdgeOS)
3. **Config** - Device connection settings (host, credentials, TLS options, timeout)
Key characteristics:
- Thread-safe: Uses `sync.RWMutex` for device map access and `sync.Mutex` for per-device operations
- Auto-login: Automatically authenticates on 401 responses and retries the request
- Concurrent multi-device: `GetAll*` methods use `sync.WaitGroup.Go()` for parallel queries
### API Pattern
Each package exposes:
- `MustNew(ctx, []Config)` - Constructor that accepts multiple device configs
- `Add(cfg)` / `Del(host)` - Dynamic device management
- `Get<Resource>(ctx, host)` - Single device query
- `GetAll<Resources>(ctx)` - Parallel query across all devices, returns `map[string]*Resource`
### Authentication
- **ToughSwitch**: Token-based via `x-auth-token` header from `/api/v1.0/user/login`
- **EdgeOS**: Cookie-based from `/api/login2` endpoint
### Testing
Tests use `mockTransport` implementing `http.RoundTripper` to mock HTTP responses without network calls.

21
cmd/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2026 Ryan McGuire <ryan@mcguire.app>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

21
cmd/cmd/get.go Normal file
View File

@@ -0,0 +1,21 @@
package cmd
import (
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
"github.com/spf13/cobra"
)
var getCmd = &cobra.Command{
Use: "get",
Short: "Get device information",
Long: `Get device information from configured Ubiquiti devices.`,
}
func init() {
rootCmd.AddCommand(getCmd)
getCmd.PersistentFlags().BoolP(util.FLAG_PRETTY, util.FLAG_PRETTY_P, false,
"pretty print output with indentation")
getCmd.PersistentFlags().BoolP(util.FLAG_COLOR, util.FLAG_COLOR_P, false,
"colorize YAML output")
}

102
cmd/cmd/get_device.go Normal file
View File

@@ -0,0 +1,102 @@
package cmd
import (
"context"
"fmt"
"os"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/edgeos"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/toughswitch"
"github.com/spf13/cobra"
)
var deviceCmd = &cobra.Command{
Use: "device <name>",
Short: "Get information from a single device",
Long: `Get information from a single configured device by name.`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeDeviceName,
RunE: runGetDevice,
}
func init() {
getCmd.AddCommand(deviceCmd)
}
func completeDeviceName(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
conf := util.ConfigFromContext(cmd.Context())
if conf == nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return conf.GetClientNames(), cobra.ShellCompDirectiveNoFileComp
}
func runGetDevice(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
conf := util.ConfigFromContext(ctx)
logger := util.LoggerFromContext(ctx)
deviceName := args[0]
clientConf := conf.GetClientByName(deviceName)
if clientConf == nil {
return fmt.Errorf("device %q not found in configuration", deviceName)
}
pretty, _ := cmd.Flags().GetBool(util.FLAG_PRETTY)
colorize, _ := cmd.Flags().GetBool(util.FLAG_COLOR)
logger.Debug().Str("device", deviceName).Str("type", string(clientConf.Type)).Msg("fetching device info")
var result any
var err error
switch clientConf.Type {
case config.TypeToughSwitch:
result, err = getToughSwitchDevice(ctx, clientConf)
case config.TypeEdgeOS:
result, err = getEdgeOSDevice(ctx, clientConf)
default:
return fmt.Errorf("unknown device type: %s", clientConf.Type)
}
if err != nil {
return fmt.Errorf("failed to get device info: %w", err)
}
return util.YAMLOutput(os.Stdout, result, pretty, colorize)
}
func getToughSwitchDevice(ctx context.Context, clientConf *config.ClientConfig) (any, error) {
cfg := toughswitch.Config{
Host: clientConf.Host,
Scheme: clientConf.Scheme,
Insecure: clientConf.Insecure,
Username: clientConf.User,
Password: clientConf.Pass,
Timeout: clientConf.Timeout,
}
client := toughswitch.MustNew(ctx, []toughswitch.Config{cfg})
return client.GetDevice(ctx, clientConf.Host)
}
func getEdgeOSDevice(ctx context.Context, clientConf *config.ClientConfig) (any, error) {
cfg := edgeos.Config{
Host: clientConf.Host,
Scheme: clientConf.Scheme,
Insecure: clientConf.Insecure,
Username: clientConf.User,
Password: clientConf.Pass,
Timeout: clientConf.Timeout,
}
client := edgeos.MustNew(ctx, []edgeos.Config{cfg})
return client.GetConfig(ctx, clientConf.Host)
}

123
cmd/cmd/get_devices.go Normal file
View File

@@ -0,0 +1,123 @@
package cmd
import (
"context"
"fmt"
"os"
"sync"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/edgeos"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/toughswitch"
"github.com/spf13/cobra"
)
var devicesCmd = &cobra.Command{
Use: "devices",
Short: "Get information from all configured devices",
Long: `Get information from all configured devices in parallel.`,
RunE: runGetDevices,
}
func init() {
getCmd.AddCommand(devicesCmd)
}
// DeviceResult holds the result of fetching a single device.
type DeviceResult struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Host string `yaml:"host"`
Data any `yaml:"data,omitempty"`
Error string `yaml:"error,omitempty"`
Failed bool `yaml:"failed,omitempty"`
}
func runGetDevices(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
conf := util.ConfigFromContext(ctx)
logger := util.LoggerFromContext(ctx)
pretty, _ := cmd.Flags().GetBool(util.FLAG_PRETTY)
colorize, _ := cmd.Flags().GetBool(util.FLAG_COLOR)
if len(conf.Clients) == 0 {
return fmt.Errorf("no devices configured")
}
logger.Debug().Int("count", len(conf.Clients)).Msg("fetching all devices")
results := make([]DeviceResult, len(conf.Clients))
var wg sync.WaitGroup
var mu sync.Mutex
for i, clientConf := range conf.Clients {
wg.Add(1)
go func(idx int, cc config.ClientConfig) {
defer wg.Done()
result := DeviceResult{
Name: cc.Name,
Type: string(cc.Type),
Host: cc.Host,
}
var data any
var err error
switch cc.Type {
case config.TypeToughSwitch:
data, err = fetchToughSwitch(ctx, &cc)
case config.TypeEdgeOS:
data, err = fetchEdgeOS(ctx, &cc)
default:
err = fmt.Errorf("unknown device type: %s", cc.Type)
}
if err != nil {
result.Error = err.Error()
result.Failed = true
logger.Error().Err(err).Str("device", cc.Name).Msg("failed to fetch device")
} else {
result.Data = data
}
mu.Lock()
results[idx] = result
mu.Unlock()
}(i, clientConf)
}
wg.Wait()
return util.YAMLOutput(os.Stdout, results, pretty, colorize)
}
func fetchToughSwitch(ctx context.Context, clientConf *config.ClientConfig) (any, error) {
cfg := toughswitch.Config{
Host: clientConf.Host,
Scheme: clientConf.Scheme,
Insecure: clientConf.Insecure,
Username: clientConf.User,
Password: clientConf.Pass,
Timeout: clientConf.Timeout,
}
client := toughswitch.MustNew(ctx, []toughswitch.Config{cfg})
return client.GetDevice(ctx, clientConf.Host)
}
func fetchEdgeOS(ctx context.Context, clientConf *config.ClientConfig) (any, error) {
cfg := edgeos.Config{
Host: clientConf.Host,
Scheme: clientConf.Scheme,
Insecure: clientConf.Insecure,
Username: clientConf.User,
Password: clientConf.Pass,
Timeout: clientConf.Timeout,
}
client := edgeos.MustNew(ctx, []edgeos.Config{cfg})
return client.GetConfig(ctx, clientConf.Host)
}

86
cmd/cmd/prerun.go Normal file
View File

@@ -0,0 +1,86 @@
package cmd
import (
"fmt"
"io"
"os"
"strings"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
)
// preRunFunc is a function that runs before command execution.
type preRunFunc func(cmd *cobra.Command, args []string) error
// preRunFuncs is the list of functions to run before command execution.
var preRunFuncs = []preRunFunc{
validateConfigFile,
prepareConfig,
prepareLogger,
}
// preRun executes all registered pre-run functions in order.
func preRun(cmd *cobra.Command, args []string) error {
for _, fn := range preRunFuncs {
if err := fn(cmd, args); err != nil {
return err
}
}
return nil
}
func prepareConfig(cmd *cobra.Command, _ []string) error {
configFile, _ := cmd.Root().PersistentFlags().GetString(util.FLAG_CONFIG_FILE)
conf, err := config.LoadConfig(&configFile)
if err != nil {
return err
}
ctx := util.ContextWithConfig(cmd.Context(), conf)
cmd.SetContext(ctx)
return nil
}
func prepareLogger(cmd *cobra.Command, _ []string) error {
conf := util.ConfigFromContext(cmd.Context())
level, err := zerolog.ParseLevel(conf.LogLevel)
if err != nil {
return err
}
var writer io.Writer
switch conf.LogFormat {
case "console":
writer = zerolog.NewConsoleWriter()
case "json":
writer = os.Stderr
}
logger := zerolog.New(writer).Level(level)
ctx := util.ContextWithLogger(cmd.Context(), &logger)
cmd.SetContext(ctx)
logger.Trace().Any("config", conf).Msg("config and logging prepared")
return nil
}
// validateConfigFile checks that the config file exists if provided.
func validateConfigFile(cmd *cobra.Command, args []string) error {
configFile, _ := cmd.Root().PersistentFlags().GetString(util.FLAG_CONFIG_FILE)
if configFile == "" {
return nil
}
if !strings.HasSuffix(configFile, ".yaml") && !strings.HasSuffix(configFile, ".json") {
return fmt.Errorf("config file must end in .yaml or .json: %s", configFile)
}
if _, err := os.Stat(configFile); os.IsNotExist(err) {
return fmt.Errorf("config file does not exist: %s", configFile)
}
return nil
}

68
cmd/cmd/root.go Normal file
View File

@@ -0,0 +1,68 @@
/*
Package cmd -- ubiquiti-clients test command
Copyright © 2026 Ryan McGuire <ryan@mcguire.app>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"os"
"os/signal"
"syscall"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cmd",
Short: "Simple CLI tool for using ubiquiti-clients packages",
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
ctx, cncl := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer cncl()
err := rootCmd.ExecuteContext(ctx)
if err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringP(util.FLAG_CONFIG_FILE, util.FLAG_CONFIG_FILE_P, "",
"config file for ubiquiti-clients (yaml/json), env takes priority")
rootCmd.RegisterFlagCompletionFunc(util.FLAG_CONFIG_FILE, completeConfigFile)
cobra.EnableTraverseRunHooks = true
rootCmd.PersistentPreRunE = preRun
}
// completeConfigFile provides shell completion for config file flag,
// returning only files ending in .yaml or .json.
func completeConfigFile(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
}

View File

@@ -0,0 +1,80 @@
package config
import (
"fmt"
"os"
"time"
"github.com/caarlos0/env/v11"
yaml "github.com/oasdiff/yaml3"
)
type ClientsConfig struct {
LogLevel string `json:"logLevel" yaml:"logLevel" env:"LOG_LEVEL" envDefault:"warn"`
LogFormat string `json:"logFormat" yaml:"logFormat" env:"LOG_FORMAT" envDefault:"console"`
Clients []ClientConfig `json:"clients" yaml:"clients" envPrefix:"CLIENT_"`
}
type ClientType string
const (
TypeEdgeOS ClientType = "edgeOS"
TypeToughSwitch ClientType = "toughSwitch"
)
type ClientConfig struct {
Type ClientType `json:"type" yaml:"type" env:"TYPE"`
Name string `json:"name" yaml:"name" env:"NAME"`
Host string `json:"host" yaml:"host" env:"HOST"`
Scheme string `json:"scheme" yaml:"scheme" env:"SCHEME" envDefault:"https"`
User string `json:"user" yaml:"user" env:"USER"`
Pass string `json:"pass" yaml:"pass" env:"PASS"`
Insecure bool `json:"insecure" yaml:"insecure" env:"INSECURE" envDefault:"false"`
Timeout time.Duration `json:"timeout" yaml:"timeout" env:"TIMEOUT" envDefault:"10s"`
}
// LoadConfig will load a file if given, layering env on-top of the config
// if present. Environment variables take the form:
// - LOG_LEVEL, LOG_FORMAT for top-level settings
// - CLIENT_0_NAME, CLIENT_0_HOST, CLIENT_0_TYPE, etc. for client array
func LoadConfig(configPath *string) (*ClientsConfig, error) {
conf, err := env.ParseAs[ClientsConfig]()
if err != nil {
return nil, fmt.Errorf("could not parse env config: %w", err)
}
if configPath != nil && *configPath != "" {
file, err := os.Open(*configPath)
if err != nil {
return nil, fmt.Errorf("could not open config file: %w", err)
}
defer file.Close()
decoder := yaml.NewDecoder(file)
if err := decoder.Decode(&conf); err != nil {
return nil, fmt.Errorf("could not decode config file: %w", err)
}
}
return &conf, nil
}
// GetClientByName returns a client config by its name, or nil if not found.
func (c *ClientsConfig) GetClientByName(name string) *ClientConfig {
for i := range c.Clients {
if c.Clients[i].Name == name {
return &c.Clients[i]
}
}
return nil
}
// GetClientNames returns a list of all configured client names.
func (c *ClientsConfig) GetClientNames() []string {
names := make([]string, len(c.Clients))
for i, client := range c.Clients {
names[i] = client.Name
}
return names
}

View File

@@ -0,0 +1,12 @@
package util
const (
FLAG_CONFIG_FILE = "config"
FLAG_CONFIG_FILE_P = "f"
FLAG_PRETTY = "pretty"
FLAG_PRETTY_P = "p"
FLAG_COLOR = "color"
FLAG_COLOR_P = "c"
)

View File

@@ -0,0 +1,43 @@
package util
import (
"context"
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
"github.com/rs/zerolog"
)
type CmdContextVal uint8
const (
CTX_CONFIG CmdContextVal = iota
CTX_LOGGER
)
func ContextWithConfig(baseCtx context.Context, config *config.ClientsConfig) context.Context {
return context.WithValue(baseCtx, CTX_CONFIG, config)
}
func ConfigFromContext(ctx context.Context) *config.ClientsConfig {
val := ctx.Value(CTX_CONFIG)
conf, ok := val.(*config.ClientsConfig)
if !ok {
return nil
}
return conf
}
func ContextWithLogger(baseCtx context.Context, logger *zerolog.Logger) context.Context {
return context.WithValue(baseCtx, CTX_LOGGER, logger)
}
func LoggerFromContext(ctx context.Context) *zerolog.Logger {
val := ctx.Value(CTX_LOGGER)
logger, ok := val.(*zerolog.Logger)
if !ok {
return nil
}
return logger
}

101
cmd/internal/util/output.go Normal file
View File

@@ -0,0 +1,101 @@
package util
import (
"io"
"regexp"
"github.com/fatih/color"
yaml "github.com/oasdiff/yaml3"
)
// YAMLOutput writes data as YAML to the given writer.
// If pretty is true, output is indented with 2 spaces.
// If colorize is true, YAML syntax is colorized.
func YAMLOutput(w io.Writer, data any, pretty, colorize bool) error {
encoder := yaml.NewEncoder(w)
defer encoder.Close()
if pretty {
encoder.SetIndent(2)
}
if !colorize {
return encoder.Encode(data)
}
// For colored output, encode to bytes first, then colorize
var buf []byte
bufWriter := &byteWriter{buf: &buf}
bufEncoder := yaml.NewEncoder(bufWriter)
if pretty {
bufEncoder.SetIndent(2)
}
if err := bufEncoder.Encode(data); err != nil {
return err
}
bufEncoder.Close()
colorized := colorizeYAML(string(buf))
_, err := w.Write([]byte(colorized))
return err
}
type byteWriter struct {
buf *[]byte
}
func (b *byteWriter) Write(p []byte) (n int, err error) {
*b.buf = append(*b.buf, p...)
return len(p), nil
}
// colorizeYAML applies ANSI colors to YAML output.
func colorizeYAML(input string) string {
// Color definitions - wrap to match ReplaceAllStringFunc signature
keyColor := func(s string) string { return color.New(color.FgCyan).Sprint(s) }
stringColor := func(s string) string { return color.New(color.FgGreen).Sprint(s) }
numberColor := func(s string) string { return color.New(color.FgYellow).Sprint(s) }
boolColor := func(s string) string { return color.New(color.FgMagenta).Sprint(s) }
nullColor := func(s string) string { return color.New(color.FgRed).Sprint(s) }
// Patterns for YAML elements
keyPattern := regexp.MustCompile(`(?m)^(\s*)([a-zA-Z_][a-zA-Z0-9_-]*)(:)`)
stringPattern := regexp.MustCompile(`:\s*"([^"]*)"`)
quotedStringPattern := regexp.MustCompile(`:\s*'([^']*)'`)
numberPattern := regexp.MustCompile(`:\s*(-?\d+\.?\d*)\s*$`)
boolPattern := regexp.MustCompile(`:\s*(true|false)\s*$`)
nullPattern := regexp.MustCompile(`:\s*(null|~)\s*$`)
result := input
// Apply colors in order (specific patterns first)
result = nullPattern.ReplaceAllStringFunc(result, func(s string) string {
return regexp.MustCompile(`(null|~)`).ReplaceAllStringFunc(s, nullColor)
})
result = boolPattern.ReplaceAllStringFunc(result, func(s string) string {
return regexp.MustCompile(`(true|false)`).ReplaceAllStringFunc(s, boolColor)
})
result = numberPattern.ReplaceAllStringFunc(result, func(s string) string {
return regexp.MustCompile(`(-?\d+\.?\d*)\s*$`).ReplaceAllStringFunc(s, numberColor)
})
result = stringPattern.ReplaceAllStringFunc(result, func(s string) string {
return regexp.MustCompile(`"([^"]*)"`).ReplaceAllStringFunc(s, stringColor)
})
result = quotedStringPattern.ReplaceAllStringFunc(result, func(s string) string {
return regexp.MustCompile(`'([^']*)'`).ReplaceAllStringFunc(s, stringColor)
})
result = keyPattern.ReplaceAllStringFunc(result, func(s string) string {
matches := keyPattern.FindStringSubmatch(s)
if len(matches) >= 4 {
return matches[1] + keyColor(matches[2]) + matches[3]
}
return s
})
return result
}

28
cmd/main.go Normal file
View File

@@ -0,0 +1,28 @@
/*
Copyright © 2026 Ryan McGuire <ryan@mcguire.app>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package main
import "gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/cmd"
func main() {
cmd.Execute()
}

16
go.mod
View File

@@ -1,3 +1,19 @@
module gitea.libretechconsulting.com/rmcguire/ubiquiti-clients
go 1.25.5
require (
github.com/caarlos0/env/v11 v11.3.1
github.com/fatih/color v1.18.0
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90
github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.10.2
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.25.0 // indirect
)

34
go.sum Normal file
View File

@@ -0,0 +1,34 @@
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=