Tooling, Mod Browser, Cleanup, CI

This commit is contained in:
Vilsol 2021-12-02 06:00:33 +02:00
parent 6f63878987
commit e329e48e9b
44 changed files with 2155 additions and 285 deletions

27
.github/workflows/build.yaml vendored Normal file
View file

@ -0,0 +1,27 @@
name: build
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Go Generate
run: go generate -tags tools -x ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
- name: Build
run: go build -v -o ficsit-cli .
env:
CGO_ENABLED: 1

6
.gitignore vendored
View file

@ -109,3 +109,9 @@ modules.xml
.idea/sonarlint
# End of https://www.gitignore.io/api/go,jetbrains+all
dist/
/testdata
/.graphqlconfig
schema.graphql
*.log

40
.golangci.yml Normal file
View file

@ -0,0 +1,40 @@
linters-settings:
wrapcheck:
ignoreSigs:
- .Errorf(
- errors.New(
- errors.Unwrap(
- .Wrap(
- .Wrapf(
- .WithMessage(
- .WithMessagef(
- .WithStack(
ignorePackageGlobs:
- github.com/satisfactorymodding/ficsit-cli/*
linters:
disable-all: true
enable:
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
- bidichk
- contextcheck
- durationcheck
- errorlint
- goconst
- goimports
- revive
- ifshort
- misspell
- prealloc
- whitespace
- wrapcheck

47
.goreleaser.yml Normal file
View file

@ -0,0 +1,47 @@
project_name: ficsit
before:
hooks:
- go generate -x -tags tools ./...
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
- ppc64le
goarm:
- 7
universal_binaries:
- replace: true
archives:
- format: binary
allow_different_binary_count: true
nfpms:
- formats:
- apk
- deb
- rpm
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

17
cfg/test_defaults.go Normal file
View file

@ -0,0 +1,17 @@
package cfg
import (
"path/filepath"
"runtime"
"github.com/spf13/viper"
)
func SetDefaults() {
_, file, _, _ := runtime.Caller(0)
viper.SetDefault("cache-dir", filepath.Clean(filepath.Join(filepath.Dir(file), "../", "testdata", "cache")))
viper.SetDefault("profiles-file", "profiles.json")
viper.SetDefault("installations-file", "installations.json")
viper.SetDefault("dry-run", false)
viper.SetDefault("api", "https://api.ficsit.app/v2/query")
}

47
cli/context.go Normal file
View file

@ -0,0 +1,47 @@
package cli
import "github.com/pkg/errors"
type GlobalContext struct {
Installations *Installations
Profiles *Profiles
}
var globalContext *GlobalContext
func InitCLI() (*GlobalContext, error) {
if globalContext != nil {
return globalContext, nil
}
profiles, err := InitProfiles()
if err != nil {
return nil, errors.Wrap(err, "failed to initialize profiles")
}
installations, err := InitInstallations()
if err != nil {
return nil, errors.Wrap(err, "failed to initialize installations")
}
ctx := &GlobalContext{
Installations: installations,
Profiles: profiles,
}
globalContext = ctx
return ctx, nil
}
func (g *GlobalContext) Save() error {
if err := g.Installations.Save(); err != nil {
return err
}
if err := g.Profiles.Save(); err != nil {
return err
}
return nil
}

View file

@ -1,4 +1,175 @@
package cli
type Installation struct {
import (
"encoding/json"
"fmt"
"os"
"path"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
type InstallationsVersion int
const (
InitialInstallationsVersion = InstallationsVersion(iota)
// Always last
nextInstallationsVersion
)
type Installations struct {
Version InstallationsVersion `json:"version"`
Installations []*Installation `json:"installations"`
SelectedInstallation string `json:"selected_installation"`
}
type Installation struct {
Path string `json:"path"`
Profile string `json:"profile"`
}
func InitInstallations() (*Installations, error) {
cacheDir := viper.GetString("cache-dir")
installationsFile := path.Join(cacheDir, viper.GetString("installations-file"))
_, err := os.Stat(installationsFile)
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed to stat installations file")
}
_, err := os.Stat(cacheDir)
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed to read cache directory")
}
err = os.MkdirAll(cacheDir, 0755)
if err != nil {
return nil, errors.Wrap(err, "failed to create cache directory")
}
}
emptyInstallations := Installations{
Version: nextInstallationsVersion - 1,
}
if err := emptyInstallations.Save(); err != nil {
return nil, errors.Wrap(err, "failed to save empty installations")
}
}
installationsData, err := os.ReadFile(installationsFile)
if err != nil {
return nil, errors.Wrap(err, "failed to read installations")
}
var installations Installations
if err := json.Unmarshal(installationsData, &installations); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal installations")
}
if installations.Version >= nextInstallationsVersion {
return nil, fmt.Errorf("unknown installations version: %d", installations.Version)
}
return &installations, nil
}
func (i *Installations) Save() error {
if viper.GetBool("dry-run") {
log.Info().Msg("dry-run: skipping installation saving")
return nil
}
installationsFile := path.Join(viper.GetString("cache-dir"), viper.GetString("installations-file"))
log.Info().Str("path", installationsFile).Msg("saving installations")
installationsJSON, err := json.MarshalIndent(i, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal installations")
}
if err := os.WriteFile(installationsFile, installationsJSON, 0755); err != nil {
return errors.Wrap(err, "failed to write installations")
}
return nil
}
func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string, profile string) (*Installation, error) {
installation := &Installation{
Path: installPath,
Profile: profile,
}
if err := installation.Validate(ctx); err != nil {
return nil, errors.Wrap(err, "failed to validate installation")
}
newStat, err := os.Stat(installation.Path)
if err != nil {
return nil, errors.Wrap(err, "failed to stat installation directory")
}
found := false
for _, install := range ctx.Installations.Installations {
stat, err := os.Stat(install.Path)
if err != nil {
continue
}
found = os.SameFile(newStat, stat)
if found {
break
}
}
if found {
return nil, errors.New("installation already present")
}
i.Installations = append(i.Installations, installation)
return installation, nil
}
func (i *Installations) GetInstallation(installPath string) *Installation {
for _, install := range i.Installations {
if install.Path == installPath {
return install
}
}
return nil
}
func (i *Installation) Validate(ctx *GlobalContext) error {
found := false
for _, p := range ctx.Profiles.Profiles {
if p.Name == i.Profile {
found = true
break
}
}
if !found {
return errors.New("profile not found")
}
// TODO Validate installation path
return nil
}
func (i *Installation) Install(ctx *GlobalContext) error {
if err := i.Validate(ctx); err != nil {
return errors.Wrap(err, "failed to validate installation")
}
return nil
}

35
cli/installations_test.go Normal file
View file

@ -0,0 +1,35 @@
package cli
import (
"testing"
"github.com/MarvinJWendt/testza"
"github.com/satisfactorymodding/ficsit-cli/cfg"
)
func init() {
cfg.SetDefaults()
}
func TestInstallationsInit(t *testing.T) {
installations, err := InitInstallations()
testza.AssertNoError(t, err)
testza.AssertNotNil(t, installations)
}
func TestAddInstallation(t *testing.T) {
ctx, err := InitCLI()
testza.AssertNoError(t, err)
profileName := "InstallationTest"
profile := ctx.Profiles.AddProfile(profileName)
testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5"))
testza.AssertNoError(t, profile.AddMod("ArmorModules__Modpack_All", ">=1.4.1"))
installation, err := ctx.Installations.AddInstallation(ctx, "../testdata/server", profileName)
testza.AssertNoError(t, err)
testza.AssertNotNil(t, installation)
err = installation.Install(ctx)
testza.AssertNoError(t, err)
}

View file

@ -1,4 +1,200 @@
package cli
type Profile struct {
import (
"encoding/json"
"fmt"
"os"
"path"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/utils"
"github.com/spf13/viper"
)
const defaultProfileName = "Default"
var defaultProfile = Profile{
Name: defaultProfileName,
}
type ProfilesVersion int
const (
InitialProfilesVersion = ProfilesVersion(iota)
// Always last
nextProfilesVersion
)
type Profiles struct {
Version ProfilesVersion `json:"version"`
Profiles []*Profile `json:"profiles"`
SelectedProfile string `json:"selected_profile"`
}
type Profile struct {
Name string `json:"name"`
Mods map[string]ProfileMod `json:"mods"`
}
type ProfileMod struct {
Version string `json:"version"`
InstalledVersion string `json:"installed_version"`
}
func InitProfiles() (*Profiles, error) {
cacheDir := viper.GetString("cache-dir")
profilesFile := path.Join(cacheDir, viper.GetString("profiles-file"))
_, err := os.Stat(profilesFile)
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed to stat profiles file")
}
_, err := os.Stat(cacheDir)
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed to read cache directory")
}
err = os.MkdirAll(cacheDir, 0755)
if err != nil {
return nil, errors.Wrap(err, "failed to create cache directory")
}
}
emptyProfiles := Profiles{
Version: nextProfilesVersion - 1,
Profiles: []*Profile{&defaultProfile},
}
if err := emptyProfiles.Save(); err != nil {
return nil, errors.Wrap(err, "failed to save empty profiles")
}
}
profilesData, err := os.ReadFile(profilesFile)
if err != nil {
return nil, errors.Wrap(err, "failed to read profiles")
}
var profiles Profiles
if err := json.Unmarshal(profilesData, &profiles); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal profiles")
}
if profiles.Version >= nextProfilesVersion {
return nil, fmt.Errorf("unknown profiles version: %d", profiles.Version)
}
if len(profiles.Profiles) == 0 {
profiles.Profiles = []*Profile{&defaultProfile}
profiles.SelectedProfile = defaultProfileName
}
if profiles.SelectedProfile == "" {
profiles.SelectedProfile = profiles.Profiles[0].Name
}
return &profiles, nil
}
// Save the profiles to the profiles file.
func (p *Profiles) Save() error {
if viper.GetBool("dry-run") {
log.Info().Msg("dry-run: skipping profile saving")
return nil
}
profilesFile := path.Join(viper.GetString("cache-dir"), viper.GetString("profiles-file"))
log.Info().Str("path", profilesFile).Msg("saving profiles")
profilesJSON, err := json.MarshalIndent(p, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal profiles")
}
if err := os.WriteFile(profilesFile, profilesJSON, 0755); err != nil {
return errors.Wrap(err, "failed to write profiles")
}
return nil
}
// AddProfile adds a new profile with the given name to the profiles list.
func (p *Profiles) AddProfile(name string) *Profile {
profile := &Profile{
Name: name,
}
p.Profiles = append(p.Profiles, profile)
return profile
}
// DeleteProfile deletes the profile with the given name.
func (p *Profiles) DeleteProfile(name string) {
i := 0
for _, profile := range p.Profiles {
if profile.Name == name {
break
}
i++
}
if i < len(p.Profiles) {
p.Profiles = append(p.Profiles[:i], p.Profiles[i+1:]...)
}
}
// GetProfile returns the profile with the given name or nil if it doesn't exist.
func (p *Profiles) GetProfile(name string) *Profile {
for _, profile := range p.Profiles {
if profile.Name == name {
return profile
}
}
return nil
}
// AddMod adds a mod to the profile with given version.
func (p *Profile) AddMod(reference string, version string) error {
if p.Mods == nil {
p.Mods = make(map[string]ProfileMod)
}
if !utils.SemVerRegex.MatchString(version) {
return errors.New("invalid semver version")
}
p.Mods[reference] = ProfileMod{
Version: version,
}
return nil
}
// RemoveMod removes a mod from the profile.
func (p *Profile) RemoveMod(reference string) {
if p.Mods == nil {
return
}
delete(p.Mods, reference)
}
// HasMod returns true if the profile has a mod with the given reference
func (p *Profile) HasMod(reference string) bool {
if p.Mods == nil {
return false
}
_, ok := p.Mods[reference]
return ok
}

18
cli/profiles_test.go Normal file
View file

@ -0,0 +1,18 @@
package cli
import (
"testing"
"github.com/MarvinJWendt/testza"
"github.com/satisfactorymodding/ficsit-cli/cfg"
)
func init() {
cfg.SetDefaults()
}
func TestProfilesInit(t *testing.T) {
profiles, err := InitProfiles()
testza.AssertNoError(t, err)
testza.AssertNotNil(t, profiles)
}

24
cmd/cli.go Normal file
View file

@ -0,0 +1,24 @@
package cmd
import (
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/tea"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(cliCmd)
}
var cliCmd = &cobra.Command{
Use: "cli",
Short: "Start interactive CLI",
RunE: func(cmd *cobra.Command, args []string) error {
global, err := cli.InitCLI()
if err != nil {
panic(err)
}
return tea.RunTea(global)
},
}

View file

@ -1,84 +0,0 @@
package cmd
import (
"bufio"
"encoding/binary"
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/spf13/cobra"
"net"
"strings"
"time"
)
// Slice of strings with placeholder text.
var fakeInstallList = strings.Split("pseudo-excel pseudo-photoshop pseudo-chrome pseudo-outlook pseudo-explorer "+
"pseudo-dops pseudo-git pseudo-vsc pseudo-intellij pseudo-minecraft pseudo-scoop pseudo-chocolatey", " ")
func init() {
rootCmd.AddCommand(downloadCmd)
}
var downloadCmd = &cobra.Command{
Use: "download",
Aliases: []string{"dl"},
Short: "Download a mod",
RunE: func(cmd *cobra.Command, args []string) error {
conn, err := net.Dial("udp", "127.0.0.1:15777")
if err != nil {
return err
}
defer conn.Close()
const protoVersion = 0
encoded := make([]byte, 8)
binary.LittleEndian.PutUint64(encoded, uint64(time.Now().UnixMilli()))
query := append([]byte{0, protoVersion}, encoded...)
spew.Dump("Query:", query)
if _, err := conn.Write(query); err != nil {
return err
}
response := make([]byte, 17)
_, err = bufio.NewReader(conn).Read(response)
spew.Dump("Response:", response)
serverQueryID := response[0]
serverProtocolVersion := response[1]
serverTimestamp := binary.LittleEndian.Uint64(response[2:10])
serverState := response[10]
serverNetCL := binary.LittleEndian.Uint32(response[11:15])
beaconPort := binary.LittleEndian.Uint16(response[15:])
fmt.Printf("Server query ID: %d\n", serverQueryID)
fmt.Printf("Server protocol version: %d\n", serverProtocolVersion)
fmt.Printf("Server timestamp: %d\n", serverTimestamp)
fmt.Printf("Server state: %d\n", serverState)
fmt.Printf("Server net CL: %d\n", serverNetCL)
fmt.Printf("Server beacon port: %d\n", beaconPort)
//for i := 0; i < 5; i++ {
// log.Info().Int("i", i).Msg("Foo")
// time.Sleep(time.Second)
//}
//
//p, _ := pterm.DefaultProgressbar.WithTotal(len(fakeInstallList)).WithTitle("Downloading stuff").Start()
//
//for i := 0; i < p.Total; i++ {
// p.UpdateTitle("Downloading " + fakeInstallList[i]) // Update the title of the progressbar.
// pterm.Success.Println("Downloading " + fakeInstallList[i]) // If a progressbar is running, each print will be printed above the progressbar.
// p.Increment() // Increment the progressbar by one. Use Add(x int) to increment by a custom amount.
// time.Sleep(time.Millisecond * 350) // Sleep 350 milliseconds.
//}
//
//for i := 0; i < 5; i++ {
// log.Info().Int("i", i).Msg("Bar")
// time.Sleep(time.Second)
//}
return nil
},
}

View file

@ -1,21 +1,24 @@
package cmd
import (
"io"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/tea"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"path"
"time"
)
var rootCmd = &cobra.Command{
Use: "ficsit",
Short: "cli mod manager for satisfactory",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
viper.SetConfigName("config")
viper.AddConfigPath(".")
viper.SetEnvPrefix("ficsit")
@ -30,24 +33,50 @@ var rootCmd = &cobra.Command{
zerolog.SetGlobalLevel(level)
writers := make([]io.Writer, 0)
if viper.GetBool("pretty") {
pterm.EnableStyling()
log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger().Output(zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
})
} else {
pterm.DisableStyling()
}
if !viper.GetBool("quiet") {
writers = append(writers, zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
})
}
if viper.GetString("log-file") != "" {
logFile, err := os.OpenFile(viper.GetString("log-file"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return errors.Wrap(err, "failed to open log file")
}
writers = append(writers, logFile)
}
log.Logger = zerolog.New(io.MultiWriter(writers...)).With().Timestamp().Logger()
return nil
},
}
func Execute() {
// Execute tea as default
cmd, _, err := rootCmd.Find(os.Args[1:])
cli := len(os.Args) >= 2 && os.Args[1] == "cli"
if (len(os.Args) <= 1 || os.Args[1] != "help") && (err != nil || cmd == rootCmd) {
tea.RunTea()
return
args := append([]string{"cli"}, os.Args[1:]...)
rootCmd.SetArgs(args)
cli = true
}
// Always be quiet in CLI mode
if cli {
viper.Set("quiet", true)
}
if err := rootCmd.Execute(); err != nil {
@ -62,12 +91,28 @@ func init() {
}
rootCmd.PersistentFlags().String("log", "info", "The log level to output")
rootCmd.PersistentFlags().String("log-file", "", "File to output logs to")
rootCmd.PersistentFlags().Bool("quiet", false, "Do not log anything to console")
rootCmd.PersistentFlags().Bool("pretty", true, "Whether to render pretty terminal output")
rootCmd.PersistentFlags().String("cache-dir", path.Join(baseCacheDir, "ficsit"), "The cache directory")
rootCmd.PersistentFlags().Bool("dry-run", false, "Dry-run. Do not save any changes")
rootCmd.PersistentFlags().String("cache-dir", filepath.Clean(filepath.Join(baseCacheDir, "ficsit")), "The cache directory")
rootCmd.PersistentFlags().String("profiles-file", "profiles.json", "The profiles file")
rootCmd.PersistentFlags().String("installations-file", "installations.json", "The installations file")
rootCmd.PersistentFlags().String("api", "https://api.ficsit.app/v2/query", "URL for API")
_ = viper.BindPFlag("log", rootCmd.PersistentFlags().Lookup("log"))
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
_ = viper.BindPFlag("quiet", rootCmd.PersistentFlags().Lookup("quiet"))
_ = viper.BindPFlag("pretty", rootCmd.PersistentFlags().Lookup("pretty"))
_ = viper.BindPFlag("dry-run", rootCmd.PersistentFlags().Lookup("dry-run"))
_ = viper.BindPFlag("cache-dir", rootCmd.PersistentFlags().Lookup("cache-dir"))
_ = viper.BindPFlag("profiles-file", rootCmd.PersistentFlags().Lookup("profiles-file"))
_ = viper.BindPFlag("installations-file", rootCmd.PersistentFlags().Lookup("installations-file"))
_ = viper.BindPFlag("api", rootCmd.PersistentFlags().Lookup("api"))
}

1
ficsit/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
types.go

36
ficsit/api_test.go Normal file
View file

@ -0,0 +1,36 @@
package ficsit
import (
"context"
"testing"
"github.com/Khan/genqlient/graphql"
"github.com/MarvinJWendt/testza"
"github.com/satisfactorymodding/ficsit-cli/cfg"
)
var client graphql.Client
func init() {
cfg.SetDefaults()
client = InitAPI()
}
func TestModVersions(t *testing.T) {
response, err := ModVersions(context.Background(), client, "SmartFoundations", VersionFilter{})
testza.AssertNoError(t, err)
testza.AssertNotNil(t, response)
testza.AssertNotNil(t, response.GetMod)
testza.AssertNotNil(t, response.GetMod.Versions)
testza.AssertNotZero(t, len(response.GetMod.Versions))
}
func TestMods(t *testing.T) {
response, err := Mods(context.Background(), client, ModFilter{})
testza.AssertNoError(t, err)
testza.AssertNotNil(t, response)
testza.AssertNotNil(t, response.GetMods)
testza.AssertNotNil(t, response.GetMods.Mods)
testza.AssertNotZero(t, response.GetMods.Count)
testza.AssertNotZero(t, len(response.GetMods.Mods))
}

View file

@ -0,0 +1,18 @@
query GetMod ($modId: ModID!) {
getMod(modId: $modId) {
id
mod_reference
name
views
downloads
authors {
role
user {
username
}
}
full_description
source_url
created_at
}
}

View file

@ -0,0 +1,13 @@
# @genqlient(omitempty: true)
query ModVersions (
$modId: ModID!,
$filter: VersionFilter
) {
getMod(modId: $modId) {
id
versions (filter: $filter) {
id
version
}
}
}

View file

@ -0,0 +1,11 @@
# @genqlient(omitempty: true)
query Mods ($filter: ModFilter) {
getMods (filter: $filter) {
count
mods {
id
name
mod_reference
}
}
}

12
ficsit/root.go Normal file
View file

@ -0,0 +1,12 @@
package ficsit
import (
"net/http"
"github.com/Khan/genqlient/graphql"
"github.com/spf13/viper"
)
func InitAPI() graphql.Client {
return graphql.NewClient(viper.GetString("api"), http.DefaultClient)
}

23
genqlient.yaml Normal file
View file

@ -0,0 +1,23 @@
# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml
schema: schema.graphql
operations:
- ficsit/queries/*.graphql
generated: ficsit/types.go
package: ficsit
bindings:
UserID:
type: string
ModReference:
type: string
BootstrapVersionID:
type: string
ModID:
type: string
VersionID:
type: string
GuideID:
type: string
SMLVersionID:
type: string
Date:
type: time.Time

23
go.mod
View file

@ -3,10 +3,14 @@ module github.com/satisfactorymodding/ficsit-cli
go 1.17
require (
github.com/Khan/genqlient v0.3.0
github.com/MarvinJWendt/testza v0.2.10
github.com/charmbracelet/bubbles v0.9.0
github.com/charmbracelet/bubbletea v0.19.0
github.com/charmbracelet/glamour v0.3.0
github.com/charmbracelet/lipgloss v0.4.0
github.com/davecgh/go-spew v1.1.1
github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.33
github.com/rs/zerolog v1.25.0
github.com/spf13/cobra v1.2.1
@ -14,23 +18,33 @@ require (
)
require (
github.com/agnivade/levenshtein v1.0.3 // indirect
github.com/alecthomas/chroma v0.8.2 // indirect
github.com/alexflint/go-arg v1.4.2 // indirect
github.com/alexflint/go-scalar v1.0.0 // indirect
github.com/atomicgo/cursor v0.0.1 // indirect
github.com/atotto/clipboard v0.1.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/containerd/console v1.0.2 // indirect
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gookit/color v1.4.2 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/microcosm-cc/bluemonday v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.9.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
@ -38,10 +52,17 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/vektah/gqlparser/v2 v2.1.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/yuin/goldmark v1.3.5 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/tools v0.1.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

79
go.sum
View file

@ -43,15 +43,37 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Khan/genqlient v0.3.0 h1:G35N630mNCW+j0rqSJUsvNkPLoX0bjrllRMnaQTbCak=
github.com/Khan/genqlient v0.3.0/go.mod h1:o9QUG7O7GhCeB3C83scbUQtdp+tdErC8OkVbSxIw1g4=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
github.com/MarvinJWendt/testza v0.2.10 h1:cX4zE9TofXxe72a6EPIYAxC+8cVWTsmmgsXTZIT+5bQ=
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg=
github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0=
github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM=
github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70=
github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@ -60,8 +82,11 @@ github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/charmbracelet/bubbles v0.9.0 h1:lqJ8FXwoLceQF2J0A+dWo1Cuu1dNyjbW4Opgdi2vkhw=
@ -69,6 +94,8 @@ github.com/charmbracelet/bubbles v0.9.0/go.mod h1:NWT/c+0rYEnYChz5qCyX4Lj6fDw9gG
github.com/charmbracelet/bubbletea v0.14.1/go.mod h1:b5lOf5mLjMg1tRn1HVla54guZB+jvsyV0yYAQja95zE=
github.com/charmbracelet/bubbletea v0.19.0 h1:1gz4rbxl3qZik/oP8QW2vUtul2gO8RDDzmoLGERpTQc=
github.com/charmbracelet/bubbletea v0.19.0/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc=
github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw=
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.3.0/go.mod h1:VkhdBS2eNAmRkTwRKLJCFhCOVkjntMusBDxv7TXahuk=
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
@ -86,10 +113,16 @@ github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -104,10 +137,12 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -181,6 +216,11 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
@ -230,10 +270,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -244,10 +286,13 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.6 h1:ZOvqHKtnx0fUpnbQm3m3zKFWE+DRC+XB1onh8JoEObE=
github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@ -258,6 +303,7 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
@ -267,12 +313,17 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
@ -297,6 +348,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
@ -306,7 +358,12 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -325,6 +382,8 @@ github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH
github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk=
github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -334,13 +393,21 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@ -398,6 +465,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -435,7 +503,9 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -494,6 +564,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -544,6 +615,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@ -551,6 +623,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@ -567,6 +640,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@ -597,10 +671,12 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -738,6 +814,7 @@ gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@ -754,3 +831,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

48
tea/components/header.go Normal file
View file

@ -0,0 +1,48 @@
package components
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*headerComponent)(nil)
type headerComponent struct {
root RootModel
labelStyle lipgloss.Style
}
func NewHeaderComponent(root RootModel) tea.Model {
return headerComponent{
root: root,
labelStyle: utils.LabelStyle,
}
}
func (h headerComponent) Init() tea.Cmd {
return nil
}
func (h headerComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return h, nil
}
func (h headerComponent) View() string {
out := h.labelStyle.Render("Installation: ")
if h.root.GetCurrentInstallation() != nil {
out += h.root.GetCurrentInstallation().Path
} else {
out += "None"
}
out += "\n"
out += h.labelStyle.Render("Profile: ")
if h.root.GetCurrentProfile() != nil {
out += h.root.GetCurrentProfile().Name
} else {
out += "None"
}
return lipgloss.NewStyle().Margin(1, 0).Render(out)
}

25
tea/components/types.go Normal file
View file

@ -0,0 +1,25 @@
package components
import (
"github.com/Khan/genqlient/graphql"
tea "github.com/charmbracelet/bubbletea"
"github.com/satisfactorymodding/ficsit-cli/cli"
)
type RootModel interface {
GetGlobal() *cli.GlobalContext
GetCurrentProfile() *cli.Profile
SetCurrentProfile(profile *cli.Profile) error
GetCurrentInstallation() *cli.Installation
SetCurrentInstallation(installation *cli.Installation) error
GetAPIClient() graphql.Client
Size() tea.WindowSizeMsg
SetSize(size tea.WindowSizeMsg)
View() string
Height() int
}

View file

@ -1,55 +0,0 @@
package tea
import "github.com/charmbracelet/bubbles/key"
type keyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Enter key.Binding
Help key.Binding
Quit key.Binding
}
func (k keyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Help, k.Quit}
}
func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Up, k.Down, k.Left, k.Right},
{k.Enter, k.Help, k.Quit},
}
}
var keys = keyMap{
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "move up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "move down"),
),
Left: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("←/h", "move left"),
),
Right: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("→/l", "move right"),
),
Enter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("Enter", "confirm selection"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "toggle help"),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("q", "quit"),
),
}

View file

@ -1,76 +1,89 @@
package tea
import (
"fmt"
"github.com/Khan/genqlient/graphql"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/pkg/errors"
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/scenes"
"os"
)
var docStyle = lipgloss.NewStyle().Margin(1, 2)
type item string
func (i item) FilterValue() string { return string(i) }
type rootModel struct {
currentModel tea.Model
currentProfile *cli.Profile
currentInstallation *cli.Installation
global *cli.GlobalContext
apiClient graphql.Client
currentSize tea.WindowSizeMsg
headerComponent tea.Model
}
func (m *rootModel) ChangeScene(model tea.Model) {
m.currentModel = model
func newModel(global *cli.GlobalContext) *rootModel {
m := &rootModel{
global: global,
currentProfile: global.Profiles.GetProfile(global.Profiles.SelectedProfile),
currentInstallation: global.Installations.GetInstallation(global.Installations.SelectedInstallation),
apiClient: ficsit.InitAPI(),
currentSize: tea.WindowSizeMsg{
Width: 20,
Height: 14,
},
}
m.headerComponent = components.NewHeaderComponent(m)
return m
}
func (m *rootModel) GetCurrentProfile() *cli.Profile {
return m.currentProfile
}
func (m *rootModel) SetCurrentProfile(profile *cli.Profile) {
func (m *rootModel) SetCurrentProfile(profile *cli.Profile) error {
m.currentProfile = profile
m.global.Profiles.SelectedProfile = profile.Name
return m.global.Save()
}
func (m *rootModel) GetCurrentInstallation() *cli.Installation {
return m.currentInstallation
}
func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) {
func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) error {
m.currentInstallation = installation
m.global.Installations.SelectedInstallation = installation.Path
return m.global.Save()
}
func newModel() rootModel {
m := rootModel{}
m.currentModel = scenes.NewMainMenu(&m)
return m
func (m *rootModel) GetAPIClient() graphql.Client {
return m.apiClient
}
func (m rootModel) Init() tea.Cmd {
return m.currentModel.Init()
func (m *rootModel) Size() tea.WindowSizeMsg {
return m.currentSize
}
func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.currentModel, cmd = m.currentModel.Update(msg)
return m, cmd
func (m *rootModel) SetSize(size tea.WindowSizeMsg) {
m.currentSize = size
}
func (m rootModel) View() string {
style := lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("220"))
out := style.Render("Installation:") + " " + "// TODO" + "\n"
out += style.Render("Profile:") + " " + "// TODO" + "\n"
out += "\n"
return out + m.currentModel.View()
func (m *rootModel) View() string {
return m.headerComponent.View()
}
func RunTea() {
if err := tea.NewProgram(newModel()).Start(); err != nil {
fmt.Printf("Could not start program :(\n%v\n", err)
os.Exit(1)
func (m *rootModel) Height() int {
return lipgloss.Height(m.View()) + 1
}
func (m *rootModel) GetGlobal() *cli.GlobalContext {
return m.global
}
func RunTea(global *cli.GlobalContext) error {
if err := tea.NewProgram(scenes.NewMainMenu(newModel(global))).Start(); err != nil {
return errors.Wrap(err, "internal tea error")
}
return nil
}

90
tea/scenes/exit_menu.go Normal file
View file

@ -0,0 +1,90 @@
package scenes
import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/davecgh/go-spew/spew"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*exitMenu)(nil)
type exitMenu struct {
root components.RootModel
list list.Model
}
func NewExitMenu(root components.RootModel) tea.Model {
model := mainMenu{
root: root,
}
items := []list.Item{
utils.SimpleItem{
Title: "Exit Saving Changes",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
if err := root.GetGlobal().Save(); err != nil {
panic(err) // TODO
}
return currentModel, tea.Quit
},
},
utils.SimpleItem{
Title: "Exit Discarding Changes",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
return currentModel, tea.Quit
},
},
}
model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height())
model.list.SetShowStatusBar(false)
model.list.SetFilteringEnabled(false)
model.list.Title = "Save Changes?"
model.list.Styles = utils.ListStyles
model.list.DisableQuitKeybindings()
model.list.SetSize(model.list.Width(), model.list.Height())
return model
}
func (m exitMenu) Init() tea.Cmd {
return nil
}
func (m exitMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Warn().Msg(spew.Sdump(msg))
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case KeyControlC:
return m, tea.Quit
case KeyEnter:
i, ok := m.list.SelectedItem().(utils.SimpleItem)
if ok {
if i.Activate != nil {
i.Activate(msg, m)
return m, tea.Quit
}
}
return m, tea.Quit
default:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
top, right, bottom, left := lipgloss.NewStyle().Margin(2, 2).GetMargin()
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
m.root.SetSize(msg)
}
return m, nil
}
func (m exitMenu) View() string {
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
}

View file

@ -1,7 +1,10 @@
package scenes
import tea "github.com/charmbracelet/bubbletea"
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
)
func NewInstallations(root RootModel) tea.Model {
func NewInstallations(root components.RootModel, parent tea.Model) tea.Model {
return nil
}

6
tea/scenes/keys.go Normal file
View file

@ -0,0 +1,6 @@
package scenes
const (
KeyControlC = "ctrl+c"
KeyEnter = "enter"
)

View file

@ -1,78 +1,82 @@
package scenes
import (
"fmt"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"io"
"github.com/davecgh/go-spew/spew"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*mainMenu)(nil)
type mainMenu struct {
root RootModel
help help.Model
inputStyle lipgloss.Style
lastKey string
quitting bool
root components.RootModel
list list.Model
}
type menuItem struct {
Title string
ModelFn func(model RootModel) tea.Model
}
func (i menuItem) FilterValue() string { return i.Title }
type itemDelegate struct{}
func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 0 }
func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(menuItem)
if !ok {
return
}
style := lipgloss.NewStyle().PaddingLeft(2)
str := style.Render("o " + i.Title)
if index == m.Index() {
str = style.Foreground(lipgloss.Color("202")).Render("• " + i.Title)
}
fmt.Fprintf(w, str)
}
func NewMainMenu(root RootModel) tea.Model {
items := []list.Item{
menuItem{
Title: "Installations",
ModelFn: NewInstallations,
},
menuItem{
Title: "Profiles",
ModelFn: NewProfiles,
},
menuItem{
Title: "Mods",
ModelFn: NewMods,
},
}
l := list.NewModel(items, itemDelegate{}, 20, 14)
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.SetShowTitle(false)
l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(2)
l.Styles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(2).PaddingBottom(1)
return mainMenu{
func NewMainMenu(root components.RootModel) tea.Model {
model := mainMenu{
root: root,
list: l,
}
items := []list.Item{
utils.SimpleItem{
Title: "Installations",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewInstallations(root, currentModel)
return newModel, newModel.Init()
},
},
utils.SimpleItem{
Title: "Profiles",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewProfiles(root, currentModel)
return newModel, newModel.Init()
},
},
utils.SimpleItem{
Title: "Mods",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewMods(root, currentModel)
return newModel, newModel.Init()
},
},
utils.SimpleItem{
Title: "Apply Changes",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
// TODO Apply changes to all changed profiles
return nil, nil
},
},
utils.SimpleItem{
Title: "Save",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
if err := root.GetGlobal().Save(); err != nil {
panic(err) // TODO Handle Error
}
return nil, nil
},
},
utils.SimpleItem{
Title: "Exit",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewExitMenu(root)
return newModel, newModel.Init()
},
},
}
model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height())
model.list.SetShowStatusBar(false)
model.list.SetFilteringEnabled(false)
model.list.Title = "Main Menu"
model.list.Styles = utils.ListStyles
model.list.SetSize(model.list.Width(), model.list.Height())
return model
}
func (m mainMenu) Init() tea.Cmd {
@ -80,19 +84,26 @@ func (m mainMenu) Init() tea.Cmd {
}
func (m mainMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Warn().Msg(spew.Sdump(msg))
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case "ctrl+c":
fallthrough
case "q":
m.quitting = true
case KeyControlC:
return m, tea.Quit
case "enter":
i, ok := m.list.SelectedItem().(menuItem)
case "q":
newModel := NewExitMenu(m.root)
return newModel, newModel.Init()
case KeyEnter:
i, ok := m.list.SelectedItem().(utils.SimpleItem)
if ok {
if i.ModelFn != nil {
m.root.ChangeScene(i.ModelFn(m.root))
if i.Activate != nil {
newModel, cmd := i.Activate(msg, m)
if newModel != nil || cmd != nil {
if newModel == nil {
newModel = m
}
return newModel, cmd
}
return m, nil
}
}
@ -105,11 +116,12 @@ func (m mainMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
top, right, bottom, left := lipgloss.NewStyle().Margin(2, 2).GetMargin()
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
m.root.SetSize(msg)
}
return m, nil
}
func (m mainMenu) View() string {
return m.list.View()
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
}

135
tea/scenes/mod.go Normal file
View file

@ -0,0 +1,135 @@
package scenes
import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/davecgh/go-spew/spew"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*modMenu)(nil)
type modMenu struct {
root components.RootModel
list list.Model
parent tea.Model
}
func NewModMenu(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
model := modMenu{
root: root,
parent: parent,
}
var items []list.Item
if root.GetCurrentProfile().HasMod(mod.Reference) {
items = []list.Item{
utils.SimpleItem{
Title: "Remove Mod",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
root.GetCurrentProfile().RemoveMod(mod.Reference)
return currentModel.(modMenu).parent, nil
},
},
utils.SimpleItem{
Title: "Change Version",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewModVersion(root, currentModel.(modMenu).parent, mod)
return newModel, newModel.Init()
},
},
}
} else {
items = []list.Item{
utils.SimpleItem{
Title: "Install Mod",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
err := root.GetCurrentProfile().AddMod(mod.Reference, ">=0.0.0")
if err != nil {
panic(err) // TODO Handle Error
}
return currentModel.(modMenu).parent, nil
},
},
utils.SimpleItem{
Title: "Install Mod with specific version",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewModVersion(root, currentModel.(modMenu).parent, mod)
return newModel, newModel.Init()
},
},
}
}
items = append(items, utils.SimpleItem{
Title: "View Mod info",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewModInfo(root, currentModel, mod)
return newModel, newModel.Init()
},
})
model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height())
model.list.SetShowStatusBar(false)
model.list.SetFilteringEnabled(false)
model.list.Title = mod.Name
model.list.Styles = utils.ListStyles
model.list.SetSize(model.list.Width(), model.list.Height())
model.list.KeyMap.Quit.SetHelp("q", "back")
return model
}
func (m modMenu) Init() tea.Cmd {
return nil
}
func (m modMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Warn().Msg(spew.Sdump(msg))
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case KeyControlC:
return m, tea.Quit
case "q":
if m.parent != nil {
m.parent.Update(m.root.Size())
return m.parent, nil
}
return m, tea.Quit
case KeyEnter:
i, ok := m.list.SelectedItem().(utils.SimpleItem)
if ok {
if i.Activate != nil {
newModel, cmd := i.Activate(msg, m)
if newModel != nil || cmd != nil {
if newModel == nil {
newModel.Update(m.root.Size())
newModel = m
}
return newModel, cmd
}
return m, nil
}
}
return m, tea.Quit
default:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
top, right, bottom, left := lipgloss.NewStyle().Margin(2, 2).GetMargin()
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
m.root.SetSize(msg)
}
return m, nil
}
func (m modMenu) View() string {
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
}

202
tea/scenes/mod_info.go Normal file
View file

@ -0,0 +1,202 @@
package scenes
import (
"context"
"strconv"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*modVersionMenu)(nil)
type modInfo struct {
root components.RootModel
viewport viewport.Model
spinner spinner.Model
parent tea.Model
modData chan ficsit.GetModGetMod
ready bool
help help.Model
keys modInfoKeyMap
}
type modInfoKeyMap struct {
Up key.Binding
UpHalf key.Binding
UpPage key.Binding
Down key.Binding
DownHalf key.Binding
DownPage key.Binding
Help key.Binding
Back key.Binding
}
func (k modInfoKeyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Help, k.Back}
}
func (k modInfoKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Up, k.UpHalf, k.UpPage},
{k.Down, k.DownHalf, k.DownPage},
{k.Help, k.Back},
}
}
func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
model := modInfo{
root: root,
viewport: viewport.Model{},
spinner: spinner.NewModel(),
parent: parent,
modData: make(chan ficsit.GetModGetMod),
ready: false,
help: help.NewModel(),
keys: modInfoKeyMap{
Up: key.NewBinding(key.WithHelp("↑/k", "move up")),
UpHalf: key.NewBinding(key.WithHelp("u", "up half page")),
UpPage: key.NewBinding(key.WithHelp("pgup/b", "page up")),
Down: key.NewBinding(key.WithHelp("↓/j", "move down")),
DownHalf: key.NewBinding(key.WithHelp("d", "down half page")),
DownPage: key.NewBinding(key.WithHelp("pgdn/ /f", "page down")),
Help: key.NewBinding(key.WithHelp("?", "toggle help")),
Back: key.NewBinding(key.WithHelp("q", "back")),
},
}
model.spinner.Spinner = spinner.MiniDot
model.help.Width = root.Size().Width
go func() {
fullMod, err := ficsit.GetMod(context.TODO(), root.GetAPIClient(), mod.ID)
if err != nil {
panic(err) // TODO Handle Error
}
if fullMod == nil {
panic("mod is nil") // TODO Handle Error
}
model.modData <- fullMod.GetMod
}()
return model
}
func (m modInfo) Init() tea.Cmd {
return tea.Batch(utils.Ticker(), spinner.Tick)
}
func (m modInfo) CalculateSizes(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
if m.viewport.Width == 0 {
return m, nil
}
bottomPadding := 2
if m.help.ShowAll {
bottomPadding = 4
}
top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 3, bottomPadding).GetMargin()
m.viewport.Width = msg.Width - left - right
m.viewport.Height = msg.Height - top - bottom
m.root.SetSize(msg)
m.help.Width = m.viewport.Width
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
func (m modInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case KeyControlC:
return m, tea.Quit
case "q":
if m.parent != nil {
m.parent.Update(m.root.Size())
return m.parent, nil
}
return m, tea.Quit
case "?":
m.help.ShowAll = !m.help.ShowAll
newModel, cmd := m.CalculateSizes(m.root.Size())
return newModel, cmd
default:
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
return m.CalculateSizes(msg)
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case utils.TickMsg:
select {
case mod := <-m.modData:
bottomPadding := 2
if m.help.ShowAll {
bottomPadding = 4
}
top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 3, bottomPadding).GetMargin()
m.viewport = viewport.Model{Width: m.root.Size().Width - left - right, Height: m.root.Size().Height - top - bottom}
title := lipgloss.NewStyle().Padding(0, 2).Render(utils.TitleStyle.Render(mod.Name)) + "\n"
sidebar := ""
sidebar += utils.LabelStyle.Render("Views: ") + strconv.Itoa(mod.Views) + "\n"
sidebar += utils.LabelStyle.Render("Downloads: ") + strconv.Itoa(mod.Downloads) + "\n"
sidebar += "\n"
sidebar += utils.LabelStyle.Render("Authors:") + "\n"
for _, author := range mod.Authors {
sidebar += "\n"
sidebar += utils.LabelStyle.Render(author.User.Username) + " - " + author.Role
}
description, err := glamour.Render(mod.Full_description, "dark")
if err != nil {
panic(err) // TODO Handle Error
}
bottomPart := lipgloss.JoinHorizontal(lipgloss.Top, sidebar, strings.TrimSpace(description))
m.viewport.SetContent(lipgloss.JoinVertical(lipgloss.Left, title, bottomPart))
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
default:
return m, utils.Ticker()
}
}
return m, nil
}
func (m modInfo) View() string {
if m.viewport.Height == 0 {
spinnerView := lipgloss.NewStyle().Padding(0, 2, 1).Render(m.spinner.View() + " Loading...")
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), spinnerView)
}
helpBar := lipgloss.NewStyle().Padding(1, 2).Render(m.help.View(m.keys))
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.viewport.View(), helpBar)
}

74
tea/scenes/mod_semver.go Normal file
View file

@ -0,0 +1,74 @@
package scenes
import (
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/davecgh/go-spew/spew"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*modSemver)(nil)
type modSemver struct {
root components.RootModel
parent tea.Model
input textinput.Model
title string
mod utils.Mod
}
func NewModSemver(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
model := modSemver{
root: root,
parent: parent,
input: textinput.NewModel(),
title: lipgloss.NewStyle().Padding(0, 2).Render(utils.TitleStyle.Render(mod.Name)),
mod: mod,
}
model.input.Placeholder = ">=1.2.3"
model.input.Focus()
model.input.Width = root.Size().Width
return model
}
func (m modSemver) Init() tea.Cmd {
return nil
}
func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Warn().Msg(spew.Sdump(msg))
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case KeyControlC:
return m, tea.Quit
case "q":
newModel := NewExitMenu(m.root)
return newModel, newModel.Init()
case KeyEnter:
err := m.root.GetCurrentProfile().AddMod(m.mod.Reference, m.input.Value())
if err != nil {
panic(err) // TODO Handle Error
}
return m.parent, nil
default:
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
m.root.SetSize(msg)
}
return m, nil
}
func (m modSemver) View() string {
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
}

119
tea/scenes/mod_version.go Normal file
View file

@ -0,0 +1,119 @@
package scenes
import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/davecgh/go-spew/spew"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*modVersionMenu)(nil)
type modVersionMenu struct {
root components.RootModel
list list.Model
parent tea.Model
}
func NewModVersion(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
model := modMenu{
root: root,
parent: parent,
}
items := []list.Item{
utils.SimpleItem{
Title: "Select Version",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewModVersionList(root, currentModel.(modMenu).parent, mod)
return newModel, newModel.Init()
},
},
utils.SimpleItem{
Title: "Enter Custom SemVer",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
newModel := NewModSemver(root, currentModel.(modMenu).parent, mod)
return newModel, newModel.Init()
},
},
}
if root.GetCurrentProfile().HasMod(mod.Reference) {
items = append([]list.Item{
utils.SimpleItem{
Title: "Latest",
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
err := root.GetCurrentProfile().AddMod(mod.Reference, ">=0.0.0")
if err != nil {
panic(err) // TODO Handle Error
}
return currentModel.(modMenu).parent, nil
},
},
}, items...)
}
model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height())
model.list.SetShowStatusBar(false)
model.list.SetFilteringEnabled(false)
model.list.Title = mod.Name
model.list.Styles = utils.ListStyles
model.list.SetSize(model.list.Width(), model.list.Height())
model.list.KeyMap.Quit.SetHelp("q", "back")
return model
}
func (m modVersionMenu) Init() tea.Cmd {
return nil
}
func (m modVersionMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Warn().Msg(spew.Sdump(msg))
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case KeyControlC:
return m, tea.Quit
case "q":
if m.parent != nil {
m.parent.Update(m.root.Size())
return m.parent, nil
}
return m, tea.Quit
case KeyEnter:
i, ok := m.list.SelectedItem().(utils.SimpleItem)
if ok {
if i.Activate != nil {
newModel, cmd := i.Activate(msg, m)
if newModel != nil || cmd != nil {
if newModel == nil {
newModel.Update(m.root.Size())
newModel = m
}
return newModel, cmd
}
return m, nil
}
}
return m, tea.Quit
default:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
top, right, bottom, left := lipgloss.NewStyle().Margin(2, 2).GetMargin()
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
m.root.SetSize(msg)
}
return m, nil
}
func (m modVersionMenu) View() string {
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
}

View file

@ -1,7 +1,155 @@
package scenes
import tea "github.com/charmbracelet/bubbletea"
import (
"context"
func NewMods(root RootModel) tea.Model {
return nil
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/davecgh/go-spew/spew"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*modsList)(nil)
type modsList struct {
root components.RootModel
list list.Model
parent tea.Model
items chan []list.Item
}
func NewMods(root components.RootModel, parent tea.Model) tea.Model {
// TODO Color mods that are installed in current profile
l := list.NewModel([]list.Item{}, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height())
l.SetShowStatusBar(true)
l.SetFilteringEnabled(false)
l.SetSpinner(spinner.MiniDot)
l.Title = "Mods"
l.Styles = utils.ListStyles
l.SetSize(l.Width(), l.Height())
l.KeyMap.Quit.SetHelp("q", "back")
m := &modsList{
root: root,
list: l,
parent: parent,
items: make(chan []list.Item),
}
go func() {
items := make([]list.Item, 0)
allMods := make([]ficsit.ModsGetModsModsMod, 0)
offset := 0
for {
mods, err := ficsit.Mods(context.TODO(), root.GetAPIClient(), ficsit.ModFilter{
Limit: 100,
Offset: offset,
Order_by: ficsit.ModFieldsLastVersionDate,
Order: ficsit.OrderDesc,
})
if err != nil {
panic(err) // TODO Handle Error
}
if len(mods.GetMods.Mods) == 0 {
break
}
allMods = append(allMods, mods.GetMods.Mods...)
for i := 0; i < len(mods.GetMods.Mods); i++ {
currentOffset := offset
currentI := i
items = append(items, utils.SimpleItem{
Title: mods.GetMods.Mods[i].Name,
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
mod := allMods[currentOffset+currentI]
return NewModMenu(root, currentModel, utils.Mod{
Name: mod.Name,
ID: mod.Id,
Reference: mod.Mod_reference,
}), nil
},
})
}
offset += len(mods.GetMods.Mods)
}
m.items <- items
}()
return m
}
func (m modsList) Init() tea.Cmd {
return utils.Ticker()
}
func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Info().Msg(spew.Sdump(msg))
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case KeyControlC:
return m, tea.Quit
case "q":
if m.parent != nil {
m.parent.Update(m.root.Size())
return m.parent, nil
}
return m, tea.Quit
case KeyEnter:
i, ok := m.list.SelectedItem().(utils.SimpleItem)
if ok {
if i.Activate != nil {
newModel, cmd := i.Activate(msg, m)
if newModel != nil || cmd != nil {
if newModel == nil {
newModel = m
}
return newModel, cmd
}
return m, nil
}
}
return m, tea.Quit
default:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 2, 0).GetMargin()
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
m.root.SetSize(msg)
case spinner.TickMsg:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
case utils.TickMsg:
select {
case items := <-m.items:
m.list.StopSpinner()
cmd := m.list.SetItems(items)
// Done to refresh keymap
m.list.SetFilteringEnabled(m.list.FilteringEnabled())
return m, cmd
default:
start := m.list.StartSpinner()
return m, tea.Batch(utils.Ticker(), start)
}
}
return m, nil
}
func (m modsList) View() string {
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
}

View file

@ -1,7 +1,10 @@
package scenes
import tea "github.com/charmbracelet/bubbletea"
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
)
func NewProfiles(root RootModel) tea.Model {
func NewProfiles(root components.RootModel, parent tea.Model) tea.Model {
return nil
}

View file

@ -0,0 +1,155 @@
package scenes
import (
"context"
"fmt"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/davecgh/go-spew/spew"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*selectModVersionList)(nil)
type selectModVersionList struct {
root components.RootModel
list list.Model
parent tea.Model
items chan []list.Item
}
func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
l := list.NewModel([]list.Item{}, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height())
l.SetShowStatusBar(true)
l.SetFilteringEnabled(false)
l.SetSpinner(spinner.MiniDot)
l.Title = fmt.Sprintf("Versions (%s)", mod.Name)
l.Styles = utils.ListStyles
l.SetSize(l.Width(), l.Height())
l.KeyMap.Quit.SetHelp("q", "back")
m := &selectModVersionList{
root: root,
list: l,
parent: parent,
items: make(chan []list.Item),
}
go func() {
items := make([]list.Item, 0)
allVersions := make([]ficsit.ModVersionsGetModVersionsVersion, 0)
offset := 0
for {
versions, err := ficsit.ModVersions(context.TODO(), root.GetAPIClient(), mod.ID, ficsit.VersionFilter{
Limit: 100,
Offset: offset,
Order: ficsit.OrderDesc,
Order_by: ficsit.VersionFieldsCreatedAt,
})
if err != nil {
panic(err) // TODO
}
if len(versions.GetMod.Versions) == 0 {
break
}
allVersions = append(allVersions, versions.GetMod.Versions...)
for i := 0; i < len(versions.GetMod.Versions); i++ {
currentOffset := offset
currentI := i
items = append(items, utils.SimpleItem{
Title: versions.GetMod.Versions[i].Version,
Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) {
version := allVersions[currentOffset+currentI]
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
if err != nil {
panic(err) // TODO
}
return currentModel.(selectModVersionList).parent, nil
},
})
}
offset += len(versions.GetMod.Versions)
}
m.items <- items
}()
return m
}
func (m selectModVersionList) Init() tea.Cmd {
return utils.Ticker()
}
func (m selectModVersionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Info().Msg(spew.Sdump(msg))
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case KeyControlC:
return m, tea.Quit
case "q":
if m.parent != nil {
m.parent.Update(m.root.Size())
return m.parent, nil
}
return m, tea.Quit
case KeyEnter:
i, ok := m.list.SelectedItem().(utils.SimpleItem)
if ok {
if i.Activate != nil {
newModel, cmd := i.Activate(msg, m)
if newModel != nil || cmd != nil {
if newModel == nil {
newModel = m
}
return newModel, cmd
}
return m, nil
}
}
return m, tea.Quit
default:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 2, 0).GetMargin()
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
m.root.SetSize(msg)
case spinner.TickMsg:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
case utils.TickMsg:
select {
case items := <-m.items:
m.list.StopSpinner()
cmd := m.list.SetItems(items)
// Done to refresh keymap
m.list.SetFilteringEnabled(m.list.FilteringEnabled())
return m, cmd
default:
start := m.list.StartSpinner()
return m, tea.Batch(utils.Ticker(), start)
}
}
return m, nil
}
func (m selectModVersionList) View() string {
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
}

View file

@ -1,16 +0,0 @@
package scenes
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/satisfactorymodding/ficsit-cli/cli"
)
type RootModel interface {
ChangeScene(model tea.Model)
GetCurrentProfile() *cli.Profile
SetCurrentProfile(profile *cli.Profile)
GetCurrentInstallation() *cli.Installation
SetCurrentInstallation(installation *cli.Installation)
}

51
tea/utils/basic_list.go Normal file
View file

@ -0,0 +1,51 @@
package utils
import (
"fmt"
"io"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var _ ListItem = (*SimpleItem)(nil)
var _ list.Item = (*SimpleItem)(nil)
type SimpleItem struct {
Title string
Activate func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd)
}
func (n SimpleItem) FilterValue() string {
return n.Title
}
func (n SimpleItem) GetTitle() string {
return n.Title
}
type ListItem interface {
GetTitle() string
}
type ItemDelegate struct{}
func (d ItemDelegate) Height() int { return 1 }
func (d ItemDelegate) Spacing() int { return 0 }
func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(ListItem)
if !ok {
return
}
style := lipgloss.NewStyle().PaddingLeft(2)
str := style.Render("o " + i.GetTitle())
if index == m.Index() {
str = style.Foreground(lipgloss.Color("202")).Render("• " + i.GetTitle())
}
fmt.Fprint(w, str)
}

19
tea/utils/styles.go Normal file
View file

@ -0,0 +1,19 @@
package utils
import (
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/lipgloss"
)
var (
ListStyles list.Styles
LabelStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("202"))
TitleStyle = list.DefaultStyles().Title.Background(lipgloss.Color("22"))
)
func init() {
ListStyles = list.DefaultStyles()
ListStyles.Title = TitleStyle
ListStyles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(2).PaddingBottom(1)
ListStyles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(2)
}

15
tea/utils/tick.go Normal file
View file

@ -0,0 +1,15 @@
package utils
import (
"time"
tea "github.com/charmbracelet/bubbletea"
)
type TickMsg struct{}
func Ticker() tea.Cmd {
return tea.Tick(time.Millisecond*50, func(time.Time) tea.Msg {
return TickMsg{}
})
}

7
tea/utils/types.go Normal file
View file

@ -0,0 +1,7 @@
package utils
type Mod struct {
Name string
ID string
Reference string
}

8
tools.go Normal file
View file

@ -0,0 +1,8 @@
//go:build tools
// +build tools
package smr
import _ "github.com/Khan/genqlient"
//go:generate go run github.com/Khan/genqlient

5
utils/version.go Normal file
View file

@ -0,0 +1,5 @@
package utils
import "regexp"
var SemVerRegex = regexp.MustCompile(`^(<=|<|>|>=|\^)?(0|[1-9]\d*)\.(0|[1-9]d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)