frame out cli tool
This commit is contained in:
21
cmd/LICENSE
Normal file
21
cmd/LICENSE
Normal 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
21
cmd/cmd/get.go
Normal 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
102
cmd/cmd/get_device.go
Normal 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
123
cmd/cmd/get_devices.go
Normal 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
86
cmd/cmd/prerun.go
Normal 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
68
cmd/cmd/root.go
Normal 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
|
||||
}
|
||||
80
cmd/internal/config/config.go
Normal file
80
cmd/internal/config/config.go
Normal 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
|
||||
}
|
||||
12
cmd/internal/util/constants.go
Normal file
12
cmd/internal/util/constants.go
Normal 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"
|
||||
)
|
||||
43
cmd/internal/util/context.go
Normal file
43
cmd/internal/util/context.go
Normal 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
101
cmd/internal/util/output.go
Normal 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
28
cmd/main.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user