Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
3b2c48b5e4
commit
c0c4addb85
285 changed files with 25880 additions and 0 deletions
87
shoutrrr/cmd/docs/docs.go
Normal file
87
shoutrrr/cmd/docs/docs.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package docs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/router"
|
||||
"github.com/nicholas-fedor/shoutrrr/shoutrrr/cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
serviceRouter router.ServiceRouter
|
||||
services = serviceRouter.ListServices()
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "docs",
|
||||
Short: "Print documentation for services",
|
||||
Run: Run,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
serviceList := strings.Join(services, ", ")
|
||||
cmd.SetUsageTemplate(
|
||||
cmd.UsageTemplate() + "\nAvailable services: \n " + serviceList + "\n",
|
||||
)
|
||||
|
||||
return cobra.MinimumNArgs(1)(cmd, args)
|
||||
},
|
||||
ValidArgs: services,
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().StringP("format", "f", "console", "Output format")
|
||||
}
|
||||
|
||||
func Run(cmd *cobra.Command, args []string) {
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
res := printDocs(format, args)
|
||||
|
||||
if res.ExitCode != 0 {
|
||||
fmt.Fprintf(os.Stderr, "%s", res.Message)
|
||||
}
|
||||
|
||||
os.Exit(res.ExitCode)
|
||||
}
|
||||
|
||||
func printDocs(docFormat string, services []string) cmd.Result {
|
||||
var renderer format.TreeRenderer
|
||||
|
||||
switch docFormat {
|
||||
case "console":
|
||||
renderer = format.ConsoleTreeRenderer{WithValues: false}
|
||||
case "markdown":
|
||||
renderer = format.MarkdownTreeRenderer{
|
||||
HeaderPrefix: "### ",
|
||||
PropsDescription: "Props can be either supplied using the params argument, or through the URL using \n`?key=value&key=value` etc.\n",
|
||||
PropsEmptyMessage: "*The services does not support any query/param props*",
|
||||
}
|
||||
default:
|
||||
return cmd.InvalidUsage("invalid format")
|
||||
}
|
||||
|
||||
logger := log.New(os.Stderr, "", 0) // Concrete logger implementing types.StdLogger
|
||||
|
||||
for _, scheme := range services {
|
||||
service, err := serviceRouter.NewService(scheme)
|
||||
if err != nil {
|
||||
return cmd.InvalidUsage("failed to init service: " + err.Error())
|
||||
}
|
||||
// Initialize the service to populate Config
|
||||
dummyURL, _ := url.Parse(scheme + "://dummy@dummy.com")
|
||||
if err := service.Initialize(dummyURL, logger); err != nil {
|
||||
return cmd.InvalidUsage(fmt.Sprintf("failed to initialize service %q: %v", scheme, err))
|
||||
}
|
||||
|
||||
config := format.GetServiceConfig(service)
|
||||
configNode := format.GetConfigFormat(config)
|
||||
fmt.Fprint(os.Stdout, renderer.RenderTree(configNode, scheme), "\n")
|
||||
}
|
||||
|
||||
return cmd.Success
|
||||
}
|
53
shoutrrr/cmd/exit_codes.go
Normal file
53
shoutrrr/cmd/exit_codes.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package cmd
|
||||
|
||||
const (
|
||||
// ExSuccess is the exit code that signals that everything went as expected.
|
||||
ExSuccess = 0
|
||||
// ExUsage is the exit code that signals that the application was not started with the correct arguments.
|
||||
ExUsage = 64
|
||||
// ExUnavailable is the exit code that signals that the application failed to perform the intended task.
|
||||
ExUnavailable = 69
|
||||
// ExConfig is the exit code that signals that the task failed due to a configuration error.
|
||||
ExConfig = 78
|
||||
)
|
||||
|
||||
// Success is the empty Result that is used whenever the command ran successfully.
|
||||
//
|
||||
//nolint:errname
|
||||
var Success = Result{}
|
||||
|
||||
// Result contains the final exit message and code for a CLI session.
|
||||
//
|
||||
//nolint:errname
|
||||
type Result struct {
|
||||
ExitCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e Result) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// InvalidUsage returns a Result with the exit code ExUsage.
|
||||
func InvalidUsage(message string) Result {
|
||||
return Result{
|
||||
ExUsage,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
// TaskUnavailable returns a Result with the exit code ExUnavailable.
|
||||
func TaskUnavailable(message string) Result {
|
||||
return Result{
|
||||
ExUnavailable,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigurationError returns a Result with the exit code ExConfig.
|
||||
func ConfigurationError(message string) Result {
|
||||
return Result{
|
||||
ExConfig,
|
||||
message,
|
||||
}
|
||||
}
|
240
shoutrrr/cmd/generate/generate.go
Normal file
240
shoutrrr/cmd/generate/generate.go
Normal file
|
@ -0,0 +1,240 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/generators"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/router"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// MaximumNArgs defines the maximum number of positional arguments allowed.
|
||||
const MaximumNArgs = 2
|
||||
|
||||
// ErrNoServiceSpecified indicates that no service was provided for URL generation.
|
||||
var (
|
||||
ErrNoServiceSpecified = errors.New("no service specified")
|
||||
)
|
||||
|
||||
// serviceRouter manages the creation of notification services.
|
||||
var serviceRouter router.ServiceRouter
|
||||
|
||||
// Cmd generates a notification service URL from user input.
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generates a notification service URL from user input",
|
||||
Run: Run,
|
||||
PreRun: loadArgsFromAltSources,
|
||||
Args: cobra.MaximumNArgs(MaximumNArgs),
|
||||
}
|
||||
|
||||
// loadArgsFromAltSources populates command flags from positional arguments if provided.
|
||||
func loadArgsFromAltSources(cmd *cobra.Command, args []string) {
|
||||
if len(args) > 0 {
|
||||
_ = cmd.Flags().Set("service", args[0])
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
_ = cmd.Flags().Set("generator", args[1])
|
||||
}
|
||||
}
|
||||
|
||||
// init initializes the command flags for the generate command.
|
||||
func init() {
|
||||
serviceRouter = router.ServiceRouter{}
|
||||
|
||||
Cmd.Flags().
|
||||
StringP("service", "s", "", "Notification service to generate a URL for (e.g., discord, smtp)")
|
||||
Cmd.Flags().
|
||||
StringP("generator", "g", "basic", "Generator to use (e.g., basic, or service-specific)")
|
||||
Cmd.Flags().
|
||||
StringArrayP("property", "p", []string{}, "Configuration property in key=value format (e.g., token=abc123)")
|
||||
Cmd.Flags().
|
||||
BoolP("show-sensitive", "x", false, "Show sensitive data in the generated URL (default: masked)")
|
||||
}
|
||||
|
||||
// maskSensitiveURL masks sensitive parts of a Shoutrrr URL based on the service schema.
|
||||
func maskSensitiveURL(serviceSchema, urlStr string) string {
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return urlStr // Return original URL if parsing fails
|
||||
}
|
||||
|
||||
switch serviceSchema {
|
||||
case "discord", "slack", "teams":
|
||||
maskUser(parsedURL, "REDACTED")
|
||||
case "smtp":
|
||||
maskSMTPUser(parsedURL)
|
||||
case "pushover":
|
||||
maskPushoverQuery(parsedURL)
|
||||
case "gotify":
|
||||
maskGotifyQuery(parsedURL)
|
||||
default:
|
||||
maskGeneric(parsedURL)
|
||||
}
|
||||
|
||||
return parsedURL.String()
|
||||
}
|
||||
|
||||
// maskUser redacts the username in a URL with a placeholder.
|
||||
func maskUser(parsedURL *url.URL, placeholder string) {
|
||||
if parsedURL.User != nil {
|
||||
parsedURL.User = url.User(placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
// maskSMTPUser redacts the password in an SMTP URL, preserving the username.
|
||||
func maskSMTPUser(parsedURL *url.URL) {
|
||||
if parsedURL.User != nil {
|
||||
parsedURL.User = url.UserPassword(parsedURL.User.Username(), "REDACTED")
|
||||
}
|
||||
}
|
||||
|
||||
// maskPushoverQuery redacts token and user query parameters in a Pushover URL.
|
||||
func maskPushoverQuery(parsedURL *url.URL) {
|
||||
queryParams := parsedURL.Query()
|
||||
if queryParams.Get("token") != "" {
|
||||
queryParams.Set("token", "REDACTED")
|
||||
}
|
||||
|
||||
if queryParams.Get("user") != "" {
|
||||
queryParams.Set("user", "REDACTED")
|
||||
}
|
||||
|
||||
parsedURL.RawQuery = queryParams.Encode()
|
||||
}
|
||||
|
||||
// maskGotifyQuery redacts the token query parameter in a Gotify URL.
|
||||
func maskGotifyQuery(parsedURL *url.URL) {
|
||||
queryParams := parsedURL.Query()
|
||||
if queryParams.Get("token") != "" {
|
||||
queryParams.Set("token", "REDACTED")
|
||||
}
|
||||
|
||||
parsedURL.RawQuery = queryParams.Encode()
|
||||
}
|
||||
|
||||
// maskGeneric redacts userinfo and all query parameters for unrecognized services.
|
||||
func maskGeneric(parsedURL *url.URL) {
|
||||
maskUser(parsedURL, "REDACTED")
|
||||
|
||||
queryParams := parsedURL.Query()
|
||||
for key := range queryParams {
|
||||
queryParams.Set(key, "REDACTED")
|
||||
}
|
||||
|
||||
parsedURL.RawQuery = queryParams.Encode()
|
||||
}
|
||||
|
||||
// Run executes the generate command, producing a notification service URL.
|
||||
func Run(cmd *cobra.Command, _ []string) {
|
||||
var service types.Service
|
||||
|
||||
var err error
|
||||
|
||||
serviceSchema, _ := cmd.Flags().GetString("service")
|
||||
generatorName, _ := cmd.Flags().GetString("generator")
|
||||
propertyFlags, _ := cmd.Flags().GetStringArray("property")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// Parse properties into a key-value map.
|
||||
props := make(map[string]string, len(propertyFlags))
|
||||
|
||||
for _, prop := range propertyFlags {
|
||||
parts := strings.Split(prop, "=")
|
||||
if len(parts) != MaximumNArgs {
|
||||
fmt.Fprint(
|
||||
color.Output,
|
||||
"Invalid property key/value pair: ",
|
||||
color.HiYellowString(prop),
|
||||
"\n",
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
props[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
if len(propertyFlags) > 0 {
|
||||
fmt.Fprint(color.Output, "\n") // Add spacing after property warnings
|
||||
}
|
||||
|
||||
// Validate and create the service.
|
||||
if serviceSchema == "" {
|
||||
err = ErrNoServiceSpecified
|
||||
} else {
|
||||
service, err = serviceRouter.NewService(serviceSchema)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprint(os.Stdout, "Error: ", err, "\n")
|
||||
}
|
||||
|
||||
if service == nil {
|
||||
services := serviceRouter.ListServices()
|
||||
serviceList := strings.Join(services, ", ")
|
||||
cmd.SetUsageTemplate(cmd.UsageTemplate() + "\nAvailable services:\n " + serviceList + "\n")
|
||||
_ = cmd.Usage()
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Determine the generator to use.
|
||||
var generator types.Generator
|
||||
|
||||
generatorFlag := cmd.Flags().Lookup("generator")
|
||||
if !generatorFlag.Changed {
|
||||
// Use the service-specific default generator if available and no explicit generator is set.
|
||||
generator, _ = generators.NewGenerator(serviceSchema)
|
||||
}
|
||||
|
||||
if generator != nil {
|
||||
generatorName = serviceSchema
|
||||
} else {
|
||||
var genErr error
|
||||
|
||||
generator, genErr = generators.NewGenerator(generatorName)
|
||||
if genErr != nil {
|
||||
fmt.Fprint(os.Stdout, "Error: ", genErr, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if generator == nil {
|
||||
generatorList := strings.Join(generators.ListGenerators(), ", ")
|
||||
cmd.SetUsageTemplate(
|
||||
cmd.UsageTemplate() + "\nAvailable generators:\n " + generatorList + "\n",
|
||||
)
|
||||
|
||||
_ = cmd.Usage()
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate and display the URL.
|
||||
fmt.Fprint(color.Output, "Generating URL for ", color.HiCyanString(serviceSchema))
|
||||
fmt.Fprint(color.Output, " using ", color.HiMagentaString(generatorName), " generator\n")
|
||||
|
||||
serviceConfig, err := generator.Generate(service, props, cmd.Flags().Args())
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprint(os.Stdout, "Error: ", err, "\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Fprint(color.Output, "\n")
|
||||
|
||||
maskedURL := maskSensitiveURL(serviceSchema, serviceConfig.GetURL().String())
|
||||
|
||||
if showSensitive {
|
||||
fmt.Fprint(os.Stdout, "URL: ", serviceConfig.GetURL().String(), "\n")
|
||||
} else {
|
||||
fmt.Fprint(os.Stdout, "URL: ", maskedURL, "\n")
|
||||
}
|
||||
}
|
134
shoutrrr/cmd/send/send.go
Normal file
134
shoutrrr/cmd/send/send.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package send
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/dedupe"
|
||||
internalUtil "github.com/nicholas-fedor/shoutrrr/internal/util"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/router"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/util"
|
||||
cli "github.com/nicholas-fedor/shoutrrr/shoutrrr/cmd"
|
||||
)
|
||||
|
||||
// MaximumNArgs defines the maximum number of arguments accepted by the command.
|
||||
const (
|
||||
MaximumNArgs = 2
|
||||
MaxMessageLength = 100
|
||||
)
|
||||
|
||||
// Cmd sends a notification using a service URL.
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "send",
|
||||
Short: "Send a notification using a service url",
|
||||
Args: cobra.MaximumNArgs(MaximumNArgs),
|
||||
PreRun: internalUtil.LoadFlagsFromAltSources,
|
||||
RunE: Run,
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().BoolP("verbose", "v", false, "")
|
||||
Cmd.Flags().StringArrayP("url", "u", []string{}, "The notification url")
|
||||
_ = Cmd.MarkFlagRequired("url")
|
||||
Cmd.Flags().
|
||||
StringP("message", "m", "", "The message to send to the notification url, or - to read message from stdin")
|
||||
|
||||
_ = Cmd.MarkFlagRequired("message")
|
||||
Cmd.Flags().StringP("title", "t", "", "The title used for services that support it")
|
||||
}
|
||||
|
||||
func logf(format string, a ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", a...)
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command) error {
|
||||
flags := cmd.Flags()
|
||||
verbose, _ := flags.GetBool("verbose")
|
||||
|
||||
urls, _ := flags.GetStringArray("url")
|
||||
urls = dedupe.RemoveDuplicates(urls)
|
||||
message, _ := flags.GetString("message")
|
||||
title, _ := flags.GetString("title")
|
||||
|
||||
if message == "-" {
|
||||
logf("Reading from STDIN...")
|
||||
|
||||
stringBuilder := strings.Builder{}
|
||||
|
||||
count, err := io.Copy(&stringBuilder, os.Stdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read message from stdin: %w", err)
|
||||
}
|
||||
|
||||
logf("Read %d byte(s)", count)
|
||||
|
||||
message = stringBuilder.String()
|
||||
}
|
||||
|
||||
var logger *log.Logger
|
||||
|
||||
if verbose {
|
||||
urlsPrefix := "URLs:"
|
||||
for i, url := range urls {
|
||||
logf("%s %s", urlsPrefix, url)
|
||||
|
||||
if i == 0 {
|
||||
// Only display "URLs:" prefix for first line, replace with indentation for the subsequent
|
||||
urlsPrefix = strings.Repeat(" ", len(urlsPrefix))
|
||||
}
|
||||
}
|
||||
|
||||
logf("Message: %s", util.Ellipsis(message, MaxMessageLength))
|
||||
|
||||
if title != "" {
|
||||
logf("Title: %v", title)
|
||||
}
|
||||
|
||||
logger = log.New(os.Stderr, "SHOUTRRR ", log.LstdFlags)
|
||||
} else {
|
||||
logger = util.DiscardLogger
|
||||
}
|
||||
|
||||
serviceRouter, err := router.New(logger, urls...)
|
||||
if err != nil {
|
||||
return cli.ConfigurationError(fmt.Sprintf("error invoking send: %s", err))
|
||||
}
|
||||
|
||||
params := make(types.Params)
|
||||
if title != "" {
|
||||
params["title"] = title
|
||||
}
|
||||
|
||||
errs := serviceRouter.SendAsync(message, ¶ms)
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
return cli.TaskUnavailable(err.Error())
|
||||
}
|
||||
|
||||
logf("Notification sent")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run executes the send command and handles its result.
|
||||
func Run(cmd *cobra.Command, _ []string) error {
|
||||
err := run(cmd)
|
||||
if err != nil {
|
||||
var result cli.Result
|
||||
if errors.As(err, &result) && result.ExitCode != cli.ExUsage {
|
||||
// If the error is not related to CLI usage, report error and exit to avoid cobra error output
|
||||
_, _ = fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(result.ExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
63
shoutrrr/cmd/verify/verify.go
Normal file
63
shoutrrr/cmd/verify/verify.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
internalUtil "github.com/nicholas-fedor/shoutrrr/internal/util"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/router"
|
||||
)
|
||||
|
||||
// Cmd verifies the validity of a service url.
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify the validity of a notification service URL",
|
||||
PreRun: internalUtil.LoadFlagsFromAltSources,
|
||||
Run: Run,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
}
|
||||
|
||||
var serviceRouter router.ServiceRouter
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().StringP("url", "u", "", "The notification url")
|
||||
_ = Cmd.MarkFlagRequired("url")
|
||||
}
|
||||
|
||||
// Run the verify command.
|
||||
func Run(cmd *cobra.Command, _ []string) {
|
||||
URL, _ := cmd.Flags().GetString("url")
|
||||
serviceRouter = router.ServiceRouter{}
|
||||
|
||||
service, err := serviceRouter.Locate(URL)
|
||||
if err != nil {
|
||||
wrappedErr := fmt.Errorf("locating service for URL: %w", err)
|
||||
fmt.Fprint(os.Stdout, "error verifying URL: ", sanitizeError(wrappedErr), "\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config := format.GetServiceConfig(service)
|
||||
configNode := format.GetConfigFormat(config)
|
||||
|
||||
fmt.Fprint(color.Output, format.ColorFormatTree(configNode, true))
|
||||
}
|
||||
|
||||
// sanitizeError removes sensitive details from an error message.
|
||||
func sanitizeError(err error) string {
|
||||
errStr := err.Error()
|
||||
// Check for common error patterns without exposing URL details
|
||||
if strings.Contains(errStr, "unknown service") {
|
||||
return "service not recognized"
|
||||
}
|
||||
|
||||
if strings.Contains(errStr, "parse") || strings.Contains(errStr, "invalid") {
|
||||
return "invalid URL format"
|
||||
}
|
||||
// Fallback for other errors
|
||||
return "unable to process URL"
|
||||
}
|
35
shoutrrr/main.go
Normal file
35
shoutrrr/main.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/meta"
|
||||
"github.com/nicholas-fedor/shoutrrr/shoutrrr/cmd"
|
||||
"github.com/nicholas-fedor/shoutrrr/shoutrrr/cmd/docs"
|
||||
"github.com/nicholas-fedor/shoutrrr/shoutrrr/cmd/generate"
|
||||
"github.com/nicholas-fedor/shoutrrr/shoutrrr/cmd/send"
|
||||
"github.com/nicholas-fedor/shoutrrr/shoutrrr/cmd/verify"
|
||||
)
|
||||
|
||||
var cobraCmd = &cobra.Command{
|
||||
Use: "shoutrrr",
|
||||
Version: meta.Version,
|
||||
Short: "Shoutrrr CLI",
|
||||
}
|
||||
|
||||
func init() {
|
||||
viper.AutomaticEnv()
|
||||
cobraCmd.AddCommand(verify.Cmd)
|
||||
cobraCmd.AddCommand(generate.Cmd)
|
||||
cobraCmd.AddCommand(send.Cmd)
|
||||
cobraCmd.AddCommand(docs.Cmd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := cobraCmd.Execute(); err != nil {
|
||||
os.Exit(cmd.ExUsage)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue