diff --git a/cmd/diff.go b/cmd/diff.go new file mode 100644 index 0000000..100f754 --- /dev/null +++ b/cmd/diff.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "log/slog" + + "github.com/foomo/sesamy-cli/cmd/diff" + "github.com/spf13/cobra" +) + +// NewDiff represents the diff command +func NewDiff(l *slog.Logger) *cobra.Command { + cmd := &cobra.Command{ + Use: "diff", + Short: "Print Google Tag Manager container status diff", + } + + cmd.AddCommand( + diff.NewWeb(l), + diff.NewServer(l), + ) + + return cmd +} diff --git a/cmd/diff/diff.go b/cmd/diff/diff.go new file mode 100644 index 0000000..a0af97f --- /dev/null +++ b/cmd/diff/diff.go @@ -0,0 +1,396 @@ +package diff + +import ( + "bytes" + "context" + "log/slog" + "strings" + + "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/itchyny/json2yaml" + "github.com/sters/yaml-diff/yamldiff" +) + +func diff(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager) (string, error) { + l.Info("└ ⬇︎ Loading status") + s, err := tm.Service().Accounts.Containers.Workspaces.GetStatus(tm.WorkspacePath()).Context(ctx).Do() + if err != nil { + return "", err + } else if len(s.WorkspaceChange) == 0 { + return "", nil + } + + l.Info("└ ⬇︎ Loading live version") + live, err := tm.Service().Accounts.Containers.Versions.Live(tm.ContainerPath()).Do() + if err != nil { + return "", err + } + + var res []string + for _, entity := range s.WorkspaceChange { + switch { + case entity.Tag != nil: + res = append(res, " # Tag: "+entity.Tag.Name+" ("+entity.ChangeStatus+")\n") + + // unset props + entity.Tag.Path = "" + entity.Tag.Fingerprint = "" + entity.Tag.WorkspaceId = "" + entity.Tag.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.Tag) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.Tag { + if value.Name == entity.Tag.Name { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.Folder != nil: + res = append(res, " # Folder: "+entity.Folder.Name+" ("+entity.ChangeStatus+")") + + // unset props + entity.Folder.Path = "" + entity.Folder.Fingerprint = "" + entity.Folder.WorkspaceId = "" + entity.Folder.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.Folder) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.Folder { + if value.Name == entity.Folder.Name { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.Trigger != nil: + res = append(res, " # Trigger: "+entity.Trigger.Name+" ("+entity.ChangeStatus+")") + + // unset props + entity.Trigger.Path = "" + entity.Trigger.Fingerprint = "" + entity.Trigger.WorkspaceId = "" + entity.Trigger.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.Trigger) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.Trigger { + if value.Name == entity.Trigger.Name { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.Variable != nil: + res = append(res, " # Variable: "+entity.Variable.Name+" ("+entity.ChangeStatus+")") + + // unset props + entity.Variable.Path = "" + entity.Variable.Fingerprint = "" + entity.Variable.WorkspaceId = "" + entity.Variable.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.Variable) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.Variable { + if value.Name == entity.Variable.Name { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.Client != nil: + res = append(res, " # Client: "+entity.Client.Name+" ("+entity.ChangeStatus+")") + + // unset props + entity.Client.Path = "" + entity.Client.Fingerprint = "" + entity.Client.WorkspaceId = "" + entity.Client.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.Client) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.Client { + if value.Name == entity.Client.Name { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.GtagConfig != nil: + res = append(res, " # GtagConfig: "+entity.GtagConfig.AccountId+" ("+entity.ChangeStatus+")") + + // unset props + entity.GtagConfig.Path = "" + entity.GtagConfig.Fingerprint = "" + entity.GtagConfig.WorkspaceId = "" + entity.GtagConfig.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.GtagConfig) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.GtagConfig { + if value.AccountId == entity.GtagConfig.AccountId { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.BuiltInVariable != nil: + res = append(res, " # BuiltInVariable: "+entity.BuiltInVariable.Name+" ("+entity.ChangeStatus+")") + + // unset props + entity.BuiltInVariable.Path = "" + entity.BuiltInVariable.WorkspaceId = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.BuiltInVariable) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.BuiltInVariable { + if value.Name == entity.BuiltInVariable.Name { + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.CustomTemplate != nil: + res = append(res, " # CustomTemplate: "+entity.CustomTemplate.Name+" ("+entity.ChangeStatus+")") + + // unset props + entity.CustomTemplate.Path = "" + entity.CustomTemplate.Fingerprint = "" + entity.CustomTemplate.WorkspaceId = "" + entity.CustomTemplate.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.CustomTemplate) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.CustomTemplate { + if value.Name == entity.CustomTemplate.Name { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + case entity.Transformation != nil: + res = append(res, " # Transformation: "+entity.Transformation.Name+" ("+entity.ChangeStatus+")") + + // unset props + entity.Transformation.Path = "" + entity.Transformation.Fingerprint = "" + entity.Transformation.WorkspaceId = "" + entity.Transformation.TagManagerUrl = "" + + var changed string + if entity.ChangeStatus != "deleted" { + changed, err = ToYalm(entity.Transformation) + if err != nil { + return "", err + } + } + + var original string + for _, value := range live.Transformation { + if value.Name == entity.Transformation.Name { + // unset props + value.Fingerprint = "" + + original, err = ToYalm(value) + if err != nil { + return "", err + } + break + } + } + + d, err := ToDiff(original, changed) + if err != nil { + return "", err + } + res = append(res, d...) + } + } + return strings.Join(res, " ---\n"), nil +} + +type Marshelable interface { + MarshalJSON() ([]byte, error) +} + +func ToDiff(original, changed string) ([]string, error) { + yamls1, err := yamldiff.Load(original) + if err != nil { + return nil, err + } + + yamls2, err := yamldiff.Load(changed) + if err != nil { + return nil, err + } + + var ret []string + for _, d := range yamldiff.Do(yamls1, yamls2) { + if value := d.Dump(); len(value) > 4 { + ret = append(ret, value) + } + } + return ret, nil +} + +func ToYalm(m Marshelable) (string, error) { + if m == nil { + return "", nil + } + + out, err := m.MarshalJSON() + if err != nil { + return "", err + } + + var ret bytes.Buffer + if err := json2yaml.Convert(&ret, bytes.NewBuffer(out)); err != nil { + return "", err + } + + return ret.String(), nil +} diff --git a/cmd/diff/server.go b/cmd/diff/server.go new file mode 100644 index 0000000..7f806d1 --- /dev/null +++ b/cmd/diff/server.go @@ -0,0 +1,68 @@ +package diff + +import ( + "fmt" + "log/slog" + + pkgcmd "github.com/foomo/sesamy-cli/pkg/cmd" + "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/foomo/sesamy-cli/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// NewServer represents the server command +func NewServer(l *slog.Logger) *cobra.Command { + c := viper.New() + + cmd := &cobra.Command{ + Use: "server", + Short: "Print Google Tag Manager Server Container status diff", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + l.Info("☕ Retrieving Server Container status") + + cfg, err := pkgcmd.ReadConfig(l, c, cmd) + if err != nil { + return err + } + + tm, err := tagmanager.New( + cmd.Context(), + l, + cfg.GoogleTagManager.AccountID, + cfg.GoogleTagManager.ServerContainer, + tagmanager.WithRequestQuota(cfg.GoogleAPI.RequestQuota), + tagmanager.WithClientOptions(cfg.GoogleAPI.GetClientOption()), + ) + if err != nil { + return err + } + + if err := tm.EnsureWorkspaceID(cmd.Context()); err != nil { + return err + } + + out, err := diff(cmd.Context(), l, tm) + if err != nil { + return err + } + + if !c.GetBool("raw") { + out = utils.Highlight(out) + } + _, err = fmt.Println(out) + return err + }, + } + + flags := cmd.Flags() + + flags.Bool("raw", false, "print raw output") + _ = c.BindPFlag("raw", flags.Lookup("raw")) + + flags.StringSliceP("config", "c", []string{"sesamy.yaml"}, "config files (default is sesamy.yaml)") + _ = c.BindPFlag("config", flags.Lookup("config")) + + return cmd +} diff --git a/cmd/diff/web.go b/cmd/diff/web.go new file mode 100644 index 0000000..9b9e44f --- /dev/null +++ b/cmd/diff/web.go @@ -0,0 +1,68 @@ +package diff + +import ( + "fmt" + "log/slog" + + pkgcmd "github.com/foomo/sesamy-cli/pkg/cmd" + "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/foomo/sesamy-cli/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// NewWeb represents the web command +func NewWeb(l *slog.Logger) *cobra.Command { + c := viper.New() + + cmd := &cobra.Command{ + Use: "web", + Short: "Print Google Tag Manager Web Container status diff", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + l.Info("☕ Retrieving Web Container status") + + cfg, err := pkgcmd.ReadConfig(l, c, cmd) + if err != nil { + return err + } + + tm, err := tagmanager.New( + cmd.Context(), + l, + cfg.GoogleTagManager.AccountID, + cfg.GoogleTagManager.WebContainer, + tagmanager.WithRequestQuota(cfg.GoogleAPI.RequestQuota), + tagmanager.WithClientOptions(cfg.GoogleAPI.GetClientOption()), + ) + if err != nil { + return err + } + + if err := tm.EnsureWorkspaceID(cmd.Context()); err != nil { + return err + } + + out, err := diff(cmd.Context(), l, tm) + if err != nil { + return err + } + + if !c.GetBool("raw") { + out = utils.Highlight(out) + } + _, err = fmt.Println(out) + return err + }, + } + + flags := cmd.Flags() + + flags.Bool("raw", false, "print raw output") + _ = c.BindPFlag("raw", flags.Lookup("raw")) + + flags.StringSliceP("config", "c", []string{"sesamy.yaml"}, "config files (default is sesamy.yaml)") + _ = c.BindPFlag("config", flags.Lookup("config")) + + return cmd +} diff --git a/cmd/list/list.go b/cmd/list/list.go index 1571f5a..1c5f19c 100644 --- a/cmd/list/list.go +++ b/cmd/list/list.go @@ -5,42 +5,34 @@ import ( "context" "fmt" "log/slog" - "os" "strings" - "github.com/alecthomas/chroma/quick" "github.com/foomo/sesamy-cli/pkg/tagmanager" "github.com/itchyny/json2yaml" ) -func dump(i interface{ MarshalJSON() ([]byte, error) }, err error) error { +func dump(i interface{ MarshalJSON() ([]byte, error) }, err error) (string, error) { if err != nil { - return err + return "", err } out, err := i.MarshalJSON() if err != nil { - return err + return "", err } - // if err := json.Indent(ret, out, "", " "); err != nil { - // return err - // } - // fmt.Println(ret.String()) + var output strings.Builder if err := json2yaml.Convert(&output, bytes.NewBuffer(out)); err != nil { - return err + return "", err } - // fmt.Print(output.String()) - return quick.Highlight(os.Stdout, output.String(), "yaml", "terminal", "monokai") + return output.String(), nil } -func list(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, resource string) error { +func list(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, resource string) (string, error) { switch resource { case "environments": return dump(tm.Service().Accounts.Containers.Environments.List(tm.ContainerPath()).Context(ctx).Do()) case "workspaces": return dump(tm.Service().Accounts.Containers.Workspaces.List(tm.ContainerPath()).Context(ctx).Do()) - case "status": - return dump(tm.Service().Accounts.Containers.Workspaces.GetStatus(tm.WorkspacePath()).Context(ctx).Do()) case "clients": return dump(tm.Service().Accounts.Containers.Workspaces.Clients.List(tm.WorkspacePath()).Context(ctx).Do()) case "tags": @@ -56,13 +48,14 @@ func list(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, resour case "templates-data": r, err := tm.Service().Accounts.Containers.Workspaces.Templates.List(tm.WorkspacePath()).Context(ctx).Do() if err != nil { - return err + return "", err } + var ret strings.Builder for _, template := range r.Template { - l.Info("---- Template data: " + template.Name + " ----------------------") - fmt.Println(template.TemplateData) + ret.WriteString("---- Template data: " + template.Name + " ----------------------\n") + ret.WriteString(template.TemplateData + "\n") } - return nil + return ret.String(), nil case "gtag-config": return dump(tm.Service().Accounts.Containers.Workspaces.GtagConfig.List(tm.WorkspacePath()).Context(ctx).Do()) case "triggers": @@ -72,6 +65,6 @@ func list(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, resour case "zones": return dump(tm.Service().Accounts.Containers.Workspaces.Zones.List(tm.WorkspacePath()).Context(ctx).Do()) default: - return fmt.Errorf("unknown resource %s", resource) + return "", fmt.Errorf("unknown resource %s", resource) } } diff --git a/cmd/list/server.go b/cmd/list/server.go index d38f3d8..448e8bc 100644 --- a/cmd/list/server.go +++ b/cmd/list/server.go @@ -1,10 +1,12 @@ package list import ( + "fmt" "log/slog" pkgcmd "github.com/foomo/sesamy-cli/pkg/cmd" "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/foomo/sesamy-cli/pkg/utils" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -58,12 +60,24 @@ func NewServer(l *slog.Logger) *cobra.Command { return err } - return list(cmd.Context(), l, tm, resource) + out, err := list(cmd.Context(), l, tm, resource) + if err != nil { + return err + } + + if !c.GetBool("raw") { + out = utils.Highlight(out) + } + _, err = fmt.Println(out) + return err }, } flags := cmd.Flags() + flags.Bool("raw", false, "print raw output") + _ = c.BindPFlag("raw", flags.Lookup("raw")) + flags.StringSliceP("config", "c", []string{"sesamy.yaml"}, "config files (default is sesamy.yaml)") _ = c.BindPFlag("config", flags.Lookup("config")) diff --git a/cmd/list/web.go b/cmd/list/web.go index cf43443..f420a70 100644 --- a/cmd/list/web.go +++ b/cmd/list/web.go @@ -1,10 +1,12 @@ package list import ( + "fmt" "log/slog" pkgcmd "github.com/foomo/sesamy-cli/pkg/cmd" "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/foomo/sesamy-cli/pkg/utils" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -57,12 +59,24 @@ func NewWeb(l *slog.Logger) *cobra.Command { return err } - return list(cmd.Context(), l, tm, resource) + out, err := list(cmd.Context(), l, tm, resource) + if err != nil { + return err + } + + if !c.GetBool("raw") { + out = utils.Highlight(out) + } + _, err = fmt.Println(out) + return err }, } flags := cmd.Flags() + flags.Bool("raw", false, "print raw output") + _ = c.BindPFlag("raw", flags.Lookup("raw")) + flags.StringSliceP("config", "c", []string{"sesamy.yaml"}, "config files (default is sesamy.yaml)") _ = c.BindPFlag("config", flags.Lookup("config")) diff --git a/cmd/open.go b/cmd/open.go new file mode 100644 index 0000000..c18a2c3 --- /dev/null +++ b/cmd/open.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "fmt" + "log/slog" + + pkgcmd "github.com/foomo/sesamy-cli/pkg/cmd" + "github.com/pkg/browser" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// NewOpen represents the open command +func NewOpen(l *slog.Logger) *cobra.Command { + c := viper.New() + + cmd := &cobra.Command{ + Use: "open", + Short: "Open links in the browser", + Args: cobra.OnlyValidArgs, + ValidArgs: []cobra.Completion{ + "ga", + "gtm-web", + "gtm-server", + }, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := pkgcmd.ReadConfig(l, c, cmd) + if err != nil { + return err + } + + var url string + switch args[0] { + case "ga": + if cfg.GoogleAnalytics.PropertyID == "" { + return fmt.Errorf("missing Google Analytics Property ID") + } + url = fmt.Sprintf( + "https://analytics.google.com/analytics/web/#/p%s/", + cfg.GoogleAnalytics.PropertyID, + ) + case "gtm-web": + url = fmt.Sprintf( + "https://tagmanager.google.com/#/container/accounts/%s/containers/%s/", + cfg.GoogleTagManager.AccountID, + cfg.GoogleTagManager.WebContainer.ContainerID, + ) + case "gtm-server": + url = fmt.Sprintf( + "https://tagmanager.google.com/#/container/accounts/%s/containers/%s/", + cfg.GoogleTagManager.AccountID, + cfg.GoogleTagManager.ServerContainer.ContainerID, + ) + default: + return fmt.Errorf("invalid container type: %s", args[0]) + } + + l.Info("↗ Navigating to Google Tag Manager Container: " + url) + + return browser.OpenURL(url) + }, + } + + flags := cmd.Flags() + + flags.StringSliceP("config", "c", []string{"sesamy.yaml"}, "config files (default is sesamy.yaml)") + _ = c.BindPFlag("config", flags.Lookup("config")) + + return cmd +} diff --git a/go.mod b/go.mod index fa216d1..aa8ac3c 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,12 @@ require ( github.com/knadh/koanf/providers/file v1.2.0 github.com/knadh/koanf/providers/rawbytes v1.0.0 github.com/knadh/koanf/v2 v2.2.0 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.80 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 + github.com/sters/yaml-diff v1.4.1 github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.10.0 github.com/wissance/stringFormatter v1.4.1 @@ -45,6 +47,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-yaml v1.15.6 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect diff --git a/go.sum b/go.sum index c3b52b9..95b3803 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.15.6 h1:gy5kf1yjMia3/c3wWD+u1z3lU5XlhpT8FZGaLJU9cOA= +github.com/goccy/go-yaml v1.15.6/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -129,6 +131,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -165,6 +169,8 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/sters/yaml-diff v1.4.1 h1:0W3jnFKCu8/DV7nh2aXSDA2VVfxfHu2+qdh81CuFmZo= +github.com/sters/yaml-diff v1.4.1/go.mod h1:K286Xp2z+aGkok7z9k3zXcq0ZsrDaDp7/wyGwFjM9Y8= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/main.go b/main.go index 5f60843..ee147e5 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,8 @@ func main() { root.AddCommand( cmd.NewConfig(l), cmd.NewList(l), + cmd.NewDiff(l), + cmd.NewOpen(l), cmd.NewProvision(l), cmd.NewTags(l), cmd.NewTypeScript(l), diff --git a/pkg/config/googleanalytics.go b/pkg/config/googleanalytics.go index 99caa8a..7131c82 100644 --- a/pkg/config/googleanalytics.go +++ b/pkg/config/googleanalytics.go @@ -7,6 +7,10 @@ import ( type GoogleAnalytics struct { // Enable provider Enabled bool `json:"enabled" yaml:"enabled"` + // Google Analytics account id + AccountID string `json:"accountId" yaml:"accountId"` + // Google Analytics property id + PropertyID string `json:"propertyId" yaml:"propertyId"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // GTag.js override configuration diff --git a/pkg/utils/highlight.go b/pkg/utils/highlight.go new file mode 100644 index 0000000..6faaa19 --- /dev/null +++ b/pkg/utils/highlight.go @@ -0,0 +1,98 @@ +package utils + +import ( + "bytes" + "fmt" + + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/formatters" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" + "github.com/pterm/pterm" +) + +func Highlight(source string) string { + out := &numberWriter{ + w: bytes.NewBufferString(""), + currentLine: 1, + } + // Determine lexer. + l := lexers.Get("yaml") + if l == nil { + l = lexers.Analyse(source) + } + if l == nil { + l = lexers.Fallback + } + l = chroma.Coalesce(l) + + // Determine formatter. + f := formatters.Get("terminal256") + if f == nil { + f = formatters.Fallback + } + + // Determine style. + s := styles.Get("monokai") + if s == nil { + s = styles.Fallback + } + + it, err := l.Tokenise(nil, source) + if err != nil { + pterm.Error.Println(err.Error()) + } + + if err = f.Format(out, s, it); err != nil { + pterm.Error.Println(err.Error()) + } + + return out.w.String() +} + +type numberWriter struct { + w *bytes.Buffer + currentLine uint64 + buf []byte +} + +func (w *numberWriter) Write(p []byte) (int, error) { + // Early return. + // Can't calculate the line numbers until the line breaks are made, so store them all in a buffer. + if !bytes.Contains(p, []byte{'\n'}) { + w.buf = append(w.buf, p...) + return len(p), nil + } + + var ( + original = p + tokenLen uint + ) + for i, c := range original { + tokenLen++ + if c != '\n' { + continue + } + + token := p[:tokenLen] + p = original[i+1:] + tokenLen = 0 + + format := "%4d |\t%s%s" + if w.currentLine > 9999 { + format = "%d |\t%s%s" + } + format = "\033[34m" + format + "\033[0m" + + if _, err := fmt.Fprintf(w.w, format, w.currentLine, string(w.buf), string(token)); err != nil { + return i + 1, err + } + w.buf = w.buf[:0] + w.currentLine++ + } + + if len(p) > 0 { + w.buf = append(w.buf, p...) + } + return len(original), nil +}