feat: refactor soundtouch-cli to use urfave/cli framework

- Added urfave/cli/v2 dependency for better CLI structure
- Created modular command structure with separate files:
  - common.go: Shared utilities and client setup
  - cmd_discover.go: Device discovery commands
  - cmd_info.go: Device information commands
  - cmd_volume.go: Volume control commands
  - cmd_playback.go: Playback control commands
  - cmd_source.go: Source selection commands
- Replaced giant main() function (complexity 149) with organized subcommands
- Added proper flag handling and validation
- Improved help text and user experience

WIP: Some issues remain (flag conflicts, missing commands)
Next: Complete remaining commands and fix conflicts
This commit is contained in:
Tobias Gesellchen
2026-01-09 23:35:23 +01:00
parent f96cd758d4
commit 97677bc8d1
10 changed files with 1144 additions and 2364 deletions
+89
View File
@@ -0,0 +1,89 @@
package main
import (
"context"
"fmt"
"time"
"github.com/user_account/bose-soundtouch/pkg/config"
"github.com/user_account/bose-soundtouch/pkg/discovery"
"github.com/urfave/cli/v2"
)
// discoverDevices handles device discovery command
func discoverDevices(c *cli.Context) error {
timeout := c.Duration("timeout")
showAll := c.Bool("all")
fmt.Printf("Discovering SoundTouch devices...\n")
if showAll {
fmt.Printf("Timeout: %v\n", timeout)
fmt.Printf("Mode: Detailed information\n")
}
fmt.Println()
// Load configuration
cfg, err := config.LoadFromEnv()
if err != nil {
cfg = config.DefaultConfig()
}
// Override discovery timeout if provided
if timeout > 0 {
cfg.DiscoveryTimeout = timeout
}
// Create discovery service
discoveryService := discovery.NewUnifiedDiscoveryService(cfg)
ctx, cancel := context.WithTimeout(context.Background(), cfg.DiscoveryTimeout+5*time.Second)
defer cancel()
// Perform discovery
devices, err := discoveryService.DiscoverDevices(ctx)
if err != nil {
return fmt.Errorf("discovery failed: %w", err)
}
if len(devices) == 0 {
fmt.Println("No SoundTouch devices found on the network.")
fmt.Println()
fmt.Println("This could mean:")
fmt.Println("- No SoundTouch devices are powered on")
fmt.Println("- Devices are on a different network segment")
fmt.Println("- Network blocks multicast traffic")
fmt.Println("- Firewall is blocking discovery ports")
return nil
}
// Display results
fmt.Printf("Found %d SoundTouch device(s):\n\n", len(devices))
for i, device := range devices {
fmt.Printf("%d. %s\n", i+1, device.Name)
fmt.Printf(" Host: %s:%d\n", device.Host, device.Port)
fmt.Printf(" Model: %s\n", device.ModelID)
if device.SerialNo != "" {
fmt.Printf(" Serial: %s\n", device.SerialNo)
}
if device.Location != "" {
fmt.Printf(" Location: %s\n", device.Location)
}
if showAll {
fmt.Printf(" Last Seen: %s\n", device.LastSeen.Format("2006-01-02 15:04:05"))
}
// Add spacing between devices
if i < len(devices)-1 {
fmt.Println()
}
}
fmt.Println()
fmt.Printf("Use any of these hosts with other commands:\n")
fmt.Printf("Example: soundtouch-cli info --host %s\n", devices[0].Host)
return nil
}
+191
View File
@@ -0,0 +1,191 @@
package main
import (
"fmt"
"github.com/urfave/cli/v2"
)
// getDeviceInfo handles the device info command
func getDeviceInfo(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting device information", clientConfig.Host, clientConfig.Port)
deviceInfo, err := client.GetDeviceInfo()
if err != nil {
return fmt.Errorf("failed to get device info: %w", err)
}
// Display basic device information
fmt.Printf("Device Information:\n")
fmt.Printf(" Name: %s\n", deviceInfo.Name)
fmt.Printf(" Type: %s\n", deviceInfo.Type)
fmt.Printf(" Device ID: %s\n", deviceInfo.DeviceID)
if deviceInfo.MargeAccountUUID != "" {
fmt.Printf(" Account UUID: %s\n", deviceInfo.MargeAccountUUID)
}
if len(deviceInfo.NetworkInfo) > 0 {
fmt.Printf(" Network Info:\n")
for _, net := range deviceInfo.NetworkInfo {
fmt.Printf(" - Type: %s\n", net.Type)
fmt.Printf(" MAC Address: %s\n", net.MacAddress)
fmt.Printf(" IP Address: %s\n", net.IPAddress)
}
}
if len(deviceInfo.Components) > 0 {
fmt.Printf(" Components:\n")
for _, component := range deviceInfo.Components {
fmt.Printf(" - Category: %s\n", component.ComponentCategory)
if component.SoftwareVersion != "" {
fmt.Printf(" Software Version: %s\n", component.SoftwareVersion)
}
if component.SerialNumber != "" {
fmt.Printf(" Serial Number: %s\n", component.SerialNumber)
}
}
}
return nil
}
// getDeviceName handles getting the device name
func getDeviceName(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting device name", clientConfig.Host, clientConfig.Port)
name, err := client.GetName()
if err != nil {
return fmt.Errorf("failed to get device name: %w", err)
}
fmt.Printf("Device Name: %s\n", name)
return nil
}
// setDeviceName handles setting the device name
func setDeviceName(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
newName := c.String("value")
if newName == "" {
return fmt.Errorf("device name cannot be empty")
}
PrintDeviceHeader(fmt.Sprintf("Setting device name to '%s'", newName), clientConfig.Host, clientConfig.Port)
err = client.SetName(newName)
if err != nil {
return fmt.Errorf("failed to set device name: %w", err)
}
PrintSuccess(fmt.Sprintf("Device name set to '%s'", newName))
return nil
}
// getCapabilities handles getting device capabilities
func getCapabilities(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting device capabilities", clientConfig.Host, clientConfig.Port)
capabilities, err := client.GetCapabilities()
if err != nil {
return fmt.Errorf("failed to get capabilities: %w", err)
}
fmt.Printf("Device Capabilities:\n")
fmt.Printf(" Device ID: %s\n", capabilities.DeviceID)
// Network capabilities
networkCaps := capabilities.GetNetworkCapabilities()
if len(networkCaps) > 0 {
fmt.Printf(" Network Capabilities:\n")
for _, cap := range networkCaps {
fmt.Printf(" - %s\n", cap)
}
}
// Extended capabilities
capNames := capabilities.GetCapabilityNames()
if len(capNames) > 0 {
fmt.Printf(" Extended Capabilities:\n")
for _, capName := range capNames {
capability := capabilities.GetCapabilityByName(capName)
fmt.Printf(" - %s", capName)
if capability.URL != "" {
fmt.Printf(" (%s)", capability.URL)
}
fmt.Println()
}
}
return nil
}
// getPresets handles getting device presets
func getPresets(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting device presets", clientConfig.Host, clientConfig.Port)
presets, err := client.GetPresets()
if err != nil {
return fmt.Errorf("failed to get presets: %w", err)
}
fmt.Printf("Device Presets:\n")
if len(presets.Preset) == 0 {
fmt.Printf(" No presets configured\n")
return nil
}
fmt.Printf(" Configured Presets:\n")
for _, preset := range presets.Preset {
fmt.Printf(" %d. %s\n", preset.ID, preset.GetDisplayName())
fmt.Printf(" Source: %s\n", preset.ContentItem.Source)
if preset.ContentItem.SourceAccount != "" && preset.ContentItem.SourceAccount != preset.ContentItem.Source {
fmt.Printf(" Account: %s\n", preset.ContentItem.SourceAccount)
}
if preset.ContentItem.Location != "" {
fmt.Printf(" Location: %s\n", preset.ContentItem.Location)
}
// Show preset creation time if available
if preset.CreatedOn != nil && *preset.CreatedOn != 0 {
fmt.Printf(" Created: Unix timestamp %d\n", *preset.CreatedOn)
}
}
return nil
}
+159
View File
@@ -0,0 +1,159 @@
package main
import (
"fmt"
"github.com/user_account/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
// getNowPlaying handles getting the current playback status
func getNowPlaying(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting current playback status", clientConfig.Host, clientConfig.Port)
nowPlaying, err := client.GetNowPlaying()
if err != nil {
return fmt.Errorf("failed to get now playing: %w", err)
}
fmt.Printf("Now Playing:\n")
fmt.Printf(" Device ID: %s\n", nowPlaying.DeviceID)
if nowPlaying.IsEmpty() {
fmt.Printf(" Status: No content playing\n")
return nil
}
fmt.Printf(" Source: %s\n", nowPlaying.Source)
fmt.Printf(" Status: %s\n", nowPlaying.PlayStatus.String())
if nowPlaying.Track != "" {
fmt.Printf(" Track: %s\n", nowPlaying.Track)
}
if nowPlaying.Artist != "" {
fmt.Printf(" Artist: %s\n", nowPlaying.Artist)
}
if nowPlaying.Album != "" {
fmt.Printf(" Album: %s\n", nowPlaying.Album)
}
if nowPlaying.HasTimeInfo() {
fmt.Printf(" Duration: %s\n", nowPlaying.FormatDuration())
if nowPlaying.Position != nil {
fmt.Printf(" Position: %s\n", nowPlaying.FormatPosition())
}
}
if nowPlaying.StreamType != "" {
fmt.Printf(" Stream Type: %s\n", nowPlaying.StreamType)
}
if nowPlaying.PlayStatus == models.PlayStatusBuffering {
fmt.Printf(" Note: Content is buffering\n")
}
return nil
}
// playCommand handles play command
func playCommand(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Sending play command", clientConfig.Host, clientConfig.Port)
err = client.SendKeyPressOnly(models.KeyPlay)
if err != nil {
return fmt.Errorf("failed to send play command: %w", err)
}
PrintSuccess("Play command sent")
return nil
}
// pauseCommand handles pause command
func pauseCommand(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Sending pause command", clientConfig.Host, clientConfig.Port)
err = client.SendKeyPressOnly(models.KeyPause)
if err != nil {
return fmt.Errorf("failed to send pause command: %w", err)
}
PrintSuccess("Pause command sent")
return nil
}
// stopCommand handles stop command
func stopCommand(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Sending stop command", clientConfig.Host, clientConfig.Port)
err = client.SendKeyPressOnly(models.KeyStop)
if err != nil {
return fmt.Errorf("failed to send stop command: %w", err)
}
PrintSuccess("Stop command sent")
return nil
}
// nextCommand handles next track command
func nextCommand(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Sending next track command", clientConfig.Host, clientConfig.Port)
err = client.SendKeyPressOnly(models.KeyNextTrack)
if err != nil {
return fmt.Errorf("failed to send next track command: %w", err)
}
PrintSuccess("Next track command sent")
return nil
}
// prevCommand handles previous track command
func prevCommand(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Sending previous track command", clientConfig.Host, clientConfig.Port)
err = client.SendKeyPressOnly(models.KeyPrevTrack)
if err != nil {
return fmt.Errorf("failed to send previous track command: %w", err)
}
PrintSuccess("Previous track command sent")
return nil
}
+165
View File
@@ -0,0 +1,165 @@
package main
import (
"fmt"
"strings"
"github.com/urfave/cli/v2"
)
// listSources handles listing available audio sources
func listSources(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting available sources", clientConfig.Host, clientConfig.Port)
sources, err := client.GetSources()
if err != nil {
return fmt.Errorf("failed to get sources: %w", err)
}
fmt.Printf("Available Audio Sources:\n")
fmt.Printf(" Device ID: %s\n", sources.DeviceID)
// Show ready sources first
availableSources := sources.GetAvailableSources()
if len(availableSources) > 0 {
fmt.Printf(" Ready Sources:\n")
for _, source := range availableSources {
fmt.Printf(" • %s", source.GetDisplayName())
if source.SourceAccount != "" && source.SourceAccount != source.Source {
fmt.Printf(" (%s)", source.SourceAccount)
}
var attributes []string
if source.IsLocalSource() {
attributes = append(attributes, "Local")
}
if source.IsLocalSource() {
attributes = append(attributes, "Available")
}
if len(attributes) > 0 {
fmt.Printf(" [%s]", strings.Join(attributes, ", "))
}
fmt.Println()
}
}
// Show all configured sources
fmt.Printf(" All Sources:\n")
for _, source := range sources.SourceItem {
status := "Available"
if !source.IsLocalSource() {
status = "Remote"
}
fmt.Printf(" • %s (%s)\n", source.GetDisplayName(), status)
if source.SourceAccount != "" && source.SourceAccount != source.Source {
fmt.Printf(" Account: %s\n", source.SourceAccount)
}
}
// Show streaming sources
streamingSources := sources.GetStreamingSources()
if len(streamingSources) > 0 {
fmt.Printf(" Streaming Services:\n")
for _, source := range streamingSources {
fmt.Printf(" • %s", source.GetDisplayName())
if source.SourceAccount != "" {
fmt.Printf(" (%s)", source.SourceAccount)
}
fmt.Println()
}
}
return nil
}
// selectSource handles selecting an audio source
func selectSource(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
sourceName := strings.ToUpper(c.String("source"))
sourceAccount := c.String("account")
PrintDeviceHeader(fmt.Sprintf("Selecting source '%s'", sourceName), clientConfig.Host, clientConfig.Port)
err = client.SelectSource(sourceName, sourceAccount)
if err != nil {
return fmt.Errorf("failed to select source: %w", err)
}
if sourceAccount != "" {
PrintSuccess(fmt.Sprintf("Source '%s' with account '%s' selected", sourceName, sourceAccount))
} else {
PrintSuccess(fmt.Sprintf("Source '%s' selected", sourceName))
}
return nil
}
// selectSpotify handles selecting Spotify source
func selectSpotify(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Selecting Spotify source", clientConfig.Host, clientConfig.Port)
err = client.SelectSpotify("")
if err != nil {
return fmt.Errorf("failed to select Spotify: %w", err)
}
PrintSuccess("Spotify source selected")
return nil
}
// selectBluetooth handles selecting Bluetooth source
func selectBluetooth(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Selecting Bluetooth source", clientConfig.Host, clientConfig.Port)
err = client.SelectBluetooth()
if err != nil {
return fmt.Errorf("failed to select Bluetooth: %w", err)
}
PrintSuccess("Bluetooth source selected")
return nil
}
// selectAux handles selecting AUX input source
func selectAux(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Selecting AUX input source", clientConfig.Host, clientConfig.Port)
err = client.SelectAux()
if err != nil {
return fmt.Errorf("failed to select AUX: %w", err)
}
PrintSuccess("AUX input source selected")
return nil
}
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"fmt"
"time"
"github.com/user_account/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
// getVolume handles getting the current volume level
func getVolume(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting current volume", clientConfig.Host, clientConfig.Port)
volume, err := client.GetVolume()
if err != nil {
return fmt.Errorf("failed to get volume: %w", err)
}
fmt.Printf("Current Volume:\n")
fmt.Printf(" Device ID: %s\n", volume.DeviceID)
fmt.Printf(" Current Level: %d (%s)\n", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel()))
fmt.Printf(" Target Level: %d\n", volume.GetTargetLevel())
fmt.Printf(" Muted: %v\n", volume.IsMuted())
if !volume.IsVolumeSync() {
fmt.Printf(" Note: Volume is adjusting (target: %d, actual: %d)\n", volume.GetTargetLevel(), volume.GetLevel())
}
return nil
}
// setVolume handles setting the volume level
func setVolume(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
level := c.Int("level")
if level < 0 || level > 100 {
return fmt.Errorf("volume level must be between 0 and 100, got %d", level)
}
// Safety warning for loud volumes
if level > 30 {
PrintWarning(fmt.Sprintf("Setting volume to %d (this is quite loud!)", level))
fmt.Printf("Proceeding in 2 seconds... Press Ctrl+C to cancel\n")
time.Sleep(2 * time.Second)
}
PrintDeviceHeader(fmt.Sprintf("Setting volume to %d", level), clientConfig.Host, clientConfig.Port)
err = client.SetVolume(level)
if err != nil {
return fmt.Errorf("failed to set volume: %w", err)
}
// Get updated volume to confirm
volume, err := client.GetVolume()
if err != nil {
PrintSuccess("Volume set successfully")
} else {
PrintSuccess(fmt.Sprintf("Volume set to %d (%s)", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel())))
}
return nil
}
// volumeUp handles increasing the volume
func volumeUp(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
amount := c.Int("amount")
if amount < 1 || amount > 10 {
return fmt.Errorf("volume increase amount must be between 1 and 10, got %d", amount)
}
PrintDeviceHeader(fmt.Sprintf("Increasing volume by %d", amount), clientConfig.Host, clientConfig.Port)
volume, err := client.IncreaseVolume(amount)
if err != nil {
return fmt.Errorf("failed to increase volume: %w", err)
}
PrintSuccess(fmt.Sprintf("Volume increased to %d (%s)", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel())))
return nil
}
// volumeDown handles decreasing the volume
func volumeDown(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
amount := c.Int("amount")
if amount < 1 || amount > 10 {
return fmt.Errorf("volume decrease amount must be between 1 and 10, got %d", amount)
}
PrintDeviceHeader(fmt.Sprintf("Decreasing volume by %d", amount), clientConfig.Host, clientConfig.Port)
volume, err := client.DecreaseVolume(amount)
if err != nil {
return fmt.Errorf("failed to decrease volume: %w", err)
}
PrintSuccess(fmt.Sprintf("Volume decreased to %d (%s)", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel())))
return nil
}
+147
View File
@@ -0,0 +1,147 @@
package main
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/user_account/bose-soundtouch/pkg/client"
"github.com/user_account/bose-soundtouch/pkg/config"
"github.com/urfave/cli/v2"
)
// CommonFlags defines flags that are shared across multiple commands
var CommonFlags = []cli.Flag{
&cli.StringFlag{
Name: "host",
Aliases: []string{"h"},
Usage: "SoundTouch device host/IP address (can include port like host:8090)",
EnvVars: []string{"SOUNDTOUCH_HOST"},
},
&cli.IntFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "SoundTouch device port",
Value: 8090,
EnvVars: []string{"SOUNDTOUCH_PORT"},
},
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Usage: "Request timeout",
Value: 10 * time.Second,
},
}
// ClientConfig holds configuration for creating a SoundTouch client
type ClientConfig struct {
Host string
Port int
Timeout time.Duration
}
// GetClientConfig extracts client configuration from CLI context
func GetClientConfig(c *cli.Context) *ClientConfig {
host := c.String("host")
port := c.Int("port")
timeout := c.Duration("timeout")
// Parse host:port if host contains a port
if host != "" {
if finalHost, finalPort := parseHostPort(host, port); finalHost != "" {
host = finalHost
port = finalPort
}
}
return &ClientConfig{
Host: host,
Port: port,
Timeout: timeout,
}
}
// RequireHost validates that a host is provided for commands that need it
func RequireHost(c *cli.Context) error {
if c.String("host") == "" {
return fmt.Errorf("host is required. Use --host flag or set SOUNDTOUCH_HOST environment variable")
}
return nil
}
// CreateSoundTouchClient creates a configured SoundTouch client
func CreateSoundTouchClient(config *ClientConfig) (*client.Client, error) {
cfg, err := loadConfig(config.Timeout)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
clientConfig := &client.Config{
Host: config.Host,
Port: config.Port,
Timeout: cfg.HTTPTimeout,
UserAgent: cfg.UserAgent,
}
return client.NewClient(clientConfig), nil
}
// loadConfig loads the application configuration with optional timeout override
func loadConfig(timeout time.Duration) (*config.Config, error) {
cfg, err := config.LoadFromEnv()
if err != nil {
return nil, err
}
// Override timeout if provided
if timeout > 0 {
cfg.HTTPTimeout = timeout
}
return cfg, nil
}
// parseHostPort parses a host:port string and returns host and port separately
// If no port is specified, returns the defaultPort
func parseHostPort(hostPort string, defaultPort int) (string, int) {
if !strings.Contains(hostPort, ":") {
return hostPort, defaultPort
}
// Simple parsing - in real use, we'd use net.SplitHostPort
parts := strings.Split(hostPort, ":")
if len(parts) != 2 {
return hostPort, defaultPort
}
host := parts[0]
portStr := parts[1]
port, err := strconv.Atoi(portStr)
if err != nil {
return hostPort, defaultPort
}
return host, port
}
// PrintDeviceHeader prints a standard header for device commands
func PrintDeviceHeader(operation, host string, port int) {
fmt.Printf("%s from %s:%d...\n", operation, host, port)
}
// PrintSuccess prints a standard success message
func PrintSuccess(message string) {
fmt.Printf("✓ %s\n", message)
}
// PrintError prints a standard error message
func PrintError(message string) {
fmt.Printf("✗ %s\n", message)
}
// PrintWarning prints a standard warning message
func PrintWarning(message string) {
fmt.Printf("⚠️ %s\n", message)
}
+258 -2364
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -8,7 +8,11 @@ require (
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
+8
View File
@@ -1,3 +1,5 @@
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -7,6 +9,12 @@ github.com/hashicorp/mdns v1.0.6/go.mod h1:X4+yWh+upFECLOki1doUPaKpgNQII9gy4bUdC
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Executable
BIN
View File
Binary file not shown.