A => .gitignore +26 -0
@@ 1,26 @@
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+# direnv/devenv
+.direnv
+.devenv
+.envrc
A => client.go +316 -0
@@ 1,316 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+)
+
+var (
+ ErrPostNotFound error = errors.New("not found")
+)
+
+type BaseResponse struct {
+ OK bool `json:"ok,omitempty"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+type Post struct {
+ Title string `json:"title,omitempty"`
+ Slug string `json:"slug,omitempty"`
+ Body string `json:"body,omitempty"`
+ PublishedAt interface{} `json:"published_at,omitempty"`
+ Url string `json:"url,omitempty"`
+}
+
+type GetResponse struct {
+ BaseResponse
+ Post
+}
+
+type ListResponse struct {
+ BaseResponse
+ Posts []Post `json:"post_list,omitempty"`
+}
+
+type CreateRequest struct {
+ Title string `json:"title,omitempty"`
+ Body string `json:"body,omitempty"`
+ PublishedAt string `json:"published_at,omitempty"`
+}
+
+type CreateResponse struct {
+ BaseResponse `json:"base_response"`
+ Slug string `json:"slug,omitempty"`
+ Url string `json:"url,omitempty"`
+}
+
+type DeleteResponse struct {
+ BaseResponse
+}
+
+type UpdateRequest struct {
+ Title string `json:"title,omitempty"`
+ Slug string `json:"slug,omitempty"`
+ Body string `json:"body,omitempty"`
+ PublishedAt string `json:"published_at,omitempty"`
+}
+
+type UpdateResponse struct {
+ BaseResponse
+ Slug string `json:"slug,omitempty"`
+ Url string `json:"url,omitempty"`
+}
+
+type Mataroa interface {
+ Create(context.Context, CreateRequest) (CreateResponse, error)
+ Get(context.Context, string) (GetResponse, error)
+ List(context.Context) (ListResponse, error)
+ Delete(context.Context, string) (DeleteResponse, error)
+ Update(context.Context, string, UpdateRequest) (UpdateResponse, error)
+}
+
+type Client struct {
+ httpClient *http.Client
+ baseURL string
+ token string
+}
+
+var _ Mataroa = (*Client)(nil)
+
+func NewClient(httpClient *http.Client, baseURL string, token string) (Client, error) {
+ if httpClient == nil {
+ httpClient = &http.Client{Timeout: time.Second * 10}
+ }
+
+ if baseURL == "" {
+ return Client{}, fmt.Errorf("creating client: 'baseURL' cannot be empty")
+ }
+ baseURL = strings.TrimSuffix(baseURL, "/")
+
+ if token == "" {
+ return Client{}, fmt.Errorf("creating client: 'token' cannot be empty")
+ }
+
+ return Client{
+ httpClient, baseURL, token,
+ }, nil
+}
+
+func (c *Client) newRequest(ctx context.Context, method string, url string, body io.Reader) (*http.Request, error) {
+ if !strings.HasSuffix(url, "/") {
+ url += "/"
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, url, body)
+ if err != nil {
+ return req, err
+ }
+
+ req.Header.Add("Authorization", "Bearer "+c.token)
+ req.Header.Set("Content-Type", "application/json")
+
+ return req, nil
+}
+
+func (c *Client) Create(ctx context.Context, post CreateRequest) (CreateResponse, error) {
+ urlPath := fmt.Sprintf("%s/api/posts", c.baseURL)
+
+ if post.Title == "" {
+ return CreateResponse{}, fmt.Errorf("create post: title cannot be empty")
+ }
+
+ b := new(bytes.Buffer)
+ err := json.NewEncoder(b).Encode(&post)
+ if err != nil {
+ return CreateResponse{}, fmt.Errorf("create post: %w", err)
+ }
+
+ req, err := c.newRequest(ctx, http.MethodPost, urlPath, b)
+ if err != nil {
+ return CreateResponse{}, fmt.Errorf("create post: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return CreateResponse{}, fmt.Errorf("create post: %w", err)
+ }
+ defer resp.Body.Close()
+
+ var value CreateResponse
+ decoder := json.NewDecoder(resp.Body)
+ err = decoder.Decode(&value)
+ if err != nil {
+ return CreateResponse{}, fmt.Errorf("create post: %w", err)
+ }
+
+ if !value.OK {
+ log.Println(value.OK)
+ log.Println(value.Message)
+ return value, fmt.Errorf("create post: %w", errors.New(value.Message))
+ }
+
+ return value, nil
+}
+
+func (c *Client) Delete(ctx context.Context, slug string) (DeleteResponse, error) {
+ urlPath := fmt.Sprintf("%s/api/posts/%s/", c.baseURL, slug)
+
+ req, err := c.newRequest(ctx, http.MethodDelete, urlPath, nil)
+ if err != nil {
+ return DeleteResponse{}, fmt.Errorf("delete post: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return DeleteResponse{}, fmt.Errorf("delete post: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return DeleteResponse{}, fmt.Errorf("delete post: %w", ErrPostNotFound)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return DeleteResponse{}, fmt.Errorf("delete post: %w", errors.New(http.StatusText(resp.StatusCode)))
+ }
+
+ var value DeleteResponse
+ decoder := json.NewDecoder(resp.Body)
+ err = decoder.Decode(&value)
+ if err != nil {
+ return DeleteResponse{}, fmt.Errorf("delete post: %w", err)
+ }
+
+ if !value.OK {
+ return value, fmt.Errorf("delete post: %w", errors.New(value.Message))
+ }
+
+ return value, nil
+}
+
+func (c *Client) Get(ctx context.Context, slug string) (GetResponse, error) {
+ urlPath := fmt.Sprintf("%s/api/posts/%s", c.baseURL, slug)
+
+ req, err := c.newRequest(ctx, http.MethodGet, urlPath, nil)
+ if err != nil {
+ return GetResponse{}, fmt.Errorf("get post: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return GetResponse{}, fmt.Errorf("get post: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return GetResponse{}, fmt.Errorf("get post: %w", ErrPostNotFound)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return GetResponse{}, fmt.Errorf("get post: %w", errors.New(http.StatusText(resp.StatusCode)))
+ }
+
+ var value GetResponse
+ decoder := json.NewDecoder(resp.Body)
+ err = decoder.Decode(&value)
+ if err != nil {
+ return GetResponse{}, fmt.Errorf("error decoding post: %w", err)
+ }
+
+ if !value.OK {
+ fmt.Println(value.Message)
+ fmt.Println(value.Error)
+ return value, fmt.Errorf("error getting post: %w", errors.New(value.Message))
+ }
+
+ return value, nil
+}
+
+func (c *Client) List(ctx context.Context) (ListResponse, error) {
+ urlPath := fmt.Sprintf("%s/api/posts", c.baseURL)
+
+ req, err := c.newRequest(ctx, http.MethodGet, urlPath, nil)
+ if err != nil {
+ return ListResponse{}, fmt.Errorf("list posts: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return ListResponse{}, fmt.Errorf("list posts: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return ListResponse{}, fmt.Errorf("list posts: %w", ErrPostNotFound)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return ListResponse{}, fmt.Errorf("list post: %w", errors.New(http.StatusText(resp.StatusCode)))
+ }
+
+ var value ListResponse
+ decoder := json.NewDecoder(resp.Body)
+ err = decoder.Decode(&value)
+ if err != nil {
+ return ListResponse{}, fmt.Errorf("list posts: %w", err)
+ }
+
+ if !value.OK {
+ return value, fmt.Errorf("list posts: %w", errors.New(value.Message))
+ }
+
+ return value, nil
+}
+
+func (c *Client) Update(ctx context.Context, slug string, post UpdateRequest) (UpdateResponse, error) {
+ urlPath := fmt.Sprintf("%s/api/posts/%s", c.baseURL, slug)
+
+ b := new(bytes.Buffer)
+ err := json.NewEncoder(b).Encode(&post)
+ if err != nil {
+ return UpdateResponse{}, fmt.Errorf("update post: %w", err)
+ }
+
+ req, err := c.newRequest(ctx, http.MethodPatch, urlPath, b)
+ if err != nil {
+ return UpdateResponse{}, fmt.Errorf("update post: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return UpdateResponse{}, fmt.Errorf("update post: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return UpdateResponse{}, fmt.Errorf("update post '%s': %w", slug, ErrPostNotFound)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return UpdateResponse{}, fmt.Errorf("update post: %w", errors.New(http.StatusText(resp.StatusCode)))
+ }
+
+ var value UpdateResponse
+ decoder := json.NewDecoder(resp.Body)
+ err = decoder.Decode(&value)
+ if err != nil {
+ return UpdateResponse{}, fmt.Errorf("update post: %w", err)
+ }
+
+ if !value.OK {
+ log.Println(value.OK)
+ log.Println(value.Message)
+ return value, fmt.Errorf("update post: %w", errors.New(value.Message))
+ }
+
+ return value, nil
+}
A => commands.go +128 -0
@@ 1,128 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+
+ "github.com/spf13/cobra"
+)
+
+func CommandsRoot() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "mata",
+ Short: "mata is a CLI tool for mataroa.blog",
+ CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
+ DisableAutoGenTag: true,
+ }
+
+ cmd.AddCommand(CommandsPosts())
+
+ return cmd
+}
+
+func CommandsPosts() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "posts",
+ Aliases: []string{"p"},
+ Short: "Manage posts",
+ }
+
+ cmd.AddCommand(CommandsPostsList())
+ cmd.AddCommand(CommandsPostsGet())
+ cmd.AddCommand(CommandsPostsDelete())
+
+ return cmd
+}
+
+func CommandsPostsList() *cobra.Command {
+ run := func(cmd *cobra.Command, args []string) {
+ ctx := cmd.Context()
+
+ client, err := NewClient(nil, "https://mataroa.blog", "769fa349a28f63f93c8b6c25c07e6bf2")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ response, err := client.List(ctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ value, err := json.Marshal(&response)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Println(string(value))
+ }
+
+ return &cobra.Command{
+ Use: "list",
+ Aliases: []string{"l", "ls"},
+ Short: "List posts",
+ Args: cobra.ExactArgs(0),
+ Run: run,
+ }
+}
+
+func CommandsPostsGet() *cobra.Command {
+ run := func(cmd *cobra.Command, args []string) {
+ ctx := cmd.Context()
+
+ slug := args[0]
+
+ client, err := NewClient(nil, "https://mataroa.blog", "769fa349a28f63f93c8b6c25c07e6bf2")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ response, err := client.Get(ctx, slug)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ value, err := json.Marshal(&response)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Println(string(value))
+ }
+
+ return &cobra.Command{
+ Use: "get",
+ Aliases: []string{"g"},
+ Short: "Get a post",
+ Args: cobra.ExactArgs(1),
+ Run: run,
+ }
+}
+
+func CommandsPostsDelete() *cobra.Command {
+ run := func(cmd *cobra.Command, args []string) {
+ ctx := cmd.Context()
+
+ slug := args[0]
+
+ client, err := NewClient(nil, "https://mataroa.blog", "769fa349a28f63f93c8b6c25c07e6bf2")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ _, err = client.Delete(ctx, slug)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Println("deleted post successfully")
+ }
+
+ return &cobra.Command{
+ Use: "delete",
+ Aliases: []string{"rm", "d"},
+ Short: "Delete a post",
+ Args: cobra.ExactArgs(1),
+ Run: run,
+ }
+}
A => config.go +6 -0
@@ 1,6 @@
+package main
+
+type Config struct {
+ BaseURL string
+ Token string
+}
A => flake.lock +218 -0
@@ 1,218 @@
+{
+ "nodes": {
+ "devenv": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "nix": "nix",
+ "nixpkgs": "nixpkgs",
+ "pre-commit-hooks": "pre-commit-hooks"
+ },
+ "locked": {
+ "lastModified": 1684251214,
+ "narHash": "sha256-c+hiXMdeBXKWGZqU/3RPz3Wymg0UcgOnbcW1eJip+88=",
+ "owner": "cachix",
+ "repo": "devenv",
+ "rev": "957e4eb5ce74541f63af5e221a7917107c0c1d22",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "devenv",
+ "type": "github"
+ }
+ },
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1673956053,
+ "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "locked": {
+ "lastModified": 1667395993,
+ "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "devenv",
+ "pre-commit-hooks",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1660459072,
+ "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
+ "lowdown-src": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1633514407,
+ "narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
+ "owner": "kristapsdz",
+ "repo": "lowdown",
+ "rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
+ "type": "github"
+ },
+ "original": {
+ "owner": "kristapsdz",
+ "repo": "lowdown",
+ "type": "github"
+ }
+ },
+ "nix": {
+ "inputs": {
+ "lowdown-src": "lowdown-src",
+ "nixpkgs": [
+ "devenv",
+ "nixpkgs"
+ ],
+ "nixpkgs-regression": "nixpkgs-regression"
+ },
+ "locked": {
+ "lastModified": 1676545802,
+ "narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
+ "owner": "domenkozar",
+ "repo": "nix",
+ "rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "domenkozar",
+ "ref": "relaxed-flakes",
+ "repo": "nix",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1678875422,
+ "narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs-regression": {
+ "locked": {
+ "lastModified": 1643052045,
+ "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ }
+ },
+ "nixpkgs-stable": {
+ "locked": {
+ "lastModified": 1678872516,
+ "narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "9b8e5abb18324c7fe9f07cb100c3cd4a29cda8b8",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-22.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1684171562,
+ "narHash": "sha256-BMUWjVWAUdyMWKk0ATMC9H0Bv4qAV/TXwwPUvTiC5IQ=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "55af203d468a6f5032a519cba4f41acf5a74b638",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-22.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "pre-commit-hooks": {
+ "inputs": {
+ "flake-compat": [
+ "devenv",
+ "flake-compat"
+ ],
+ "flake-utils": "flake-utils",
+ "gitignore": "gitignore",
+ "nixpkgs": [
+ "devenv",
+ "nixpkgs"
+ ],
+ "nixpkgs-stable": "nixpkgs-stable"
+ },
+ "locked": {
+ "lastModified": 1682596858,
+ "narHash": "sha256-Hf9XVpqaGqe/4oDGr30W8HlsWvJXtMsEPHDqHZA6dDg=",
+ "owner": "cachix",
+ "repo": "pre-commit-hooks.nix",
+ "rev": "fb58866e20af98779017134319b5663b8215d912",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "pre-commit-hooks.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "devenv": "devenv",
+ "nixpkgs": "nixpkgs_2"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
A => flake.nix +29 -0
@@ 1,29 @@
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
+ devenv.url = "github:cachix/devenv";
+ };
+
+ outputs = { self, nixpkgs, devenv, ... } @ inputs:
+ let
+ forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
+ in
+ {
+ devShells = forAllSystems (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in
+ {
+ default = devenv.lib.mkShell {
+ inherit inputs pkgs;
+ modules = [
+ ({ pkgs, ... }: {
+ languages.go.enable = true;
+ packages = with pkgs; [ gcc ];
+ })
+ ];
+ };
+ });
+ };
+}
+
A => go.mod +9 -0
@@ 1,9 @@
+module github.com/mataroa/mataroa-cli
+
+go 1.19
+
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/spf13/cobra v1.7.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+)
A => go.sum +10 -0
@@ 1,10 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A => main.go +11 -0
@@ 1,11 @@
+package main
+
+import (
+ "log"
+)
+
+func main() {
+ if err := CommandsRoot().Execute(); err != nil {
+ log.Fatalf("error running mata: %s", err)
+ }
+}