From 213562956c884497a751f4f90b0694f7a0e11f70 Mon Sep 17 00:00:00 2001 From: Kirillkriv Date: Tue, 4 Aug 2026 01:00:00 +0500 Subject: [PATCH] feat - command handlers --- go.mod | 19 +++++++- go.sum | 35 +++++++++++++++ internal/cli/add.go | 29 ++++++++++++ internal/cli/rm.go | 18 ++++++++ internal/cli/root.go | 103 +++++++++++++++++++++++++++++++++++++++++++ tests/mock/mock.go | 46 +++++++++++++++++++ 6 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 internal/cli/add.go create mode 100644 internal/cli/rm.go create mode 100644 internal/cli/root.go create mode 100644 tests/mock/mock.go diff --git a/go.mod b/go.mod index 7f196d8..70ede52 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,21 @@ module spellbook go 1.26.0 -require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/displaywidth v0.10.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.6 // indirect + github.com/olekukonko/tablewriter v1.1.4 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect + golang.org/x/sys v0.30.0 // indirect +) diff --git a/go.sum b/go.sum index 0be9157..821803c 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,37 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= +github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= +github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/cli/add.go b/internal/cli/add.go new file mode 100644 index 0000000..d66cf84 --- /dev/null +++ b/internal/cli/add.go @@ -0,0 +1,29 @@ +package cli + +import ( + "github.com/spf13/cobra" + "spellbook/internal/models" + "strings" +) + +func NewAddCommand(app Application) *cobra.Command { + spell := models.Spell{} + + cmd := &cobra.Command{ + Use: "add [flags] -- [spell]", + Short: "Add new spell", + Long: `WIP`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + spell.Spell = strings.Join(args, " ") + return app.AddSpell(ctx, spell) + }, + } + + cmd.Flags().StringVarP(&spell.Name, "name", "n", "", "Spell name") + cmd.Flags().StringSliceVarP(&spell.Tags, "tags", "t", nil, "Spell tags") + cmd.Flags().StringVarP(&spell.ShortDesc, "desc", "d", "", "Short spell description") + + return cmd +} diff --git a/internal/cli/rm.go b/internal/cli/rm.go new file mode 100644 index 0000000..276554a --- /dev/null +++ b/internal/cli/rm.go @@ -0,0 +1,18 @@ +package cli + +import "github.com/spf13/cobra" + +func NewRmCommand(app Application) *cobra.Command { + cmd := &cobra.Command{ + Use: "rm [spell_name]", + Short: "Delete spell", + Long: "WIP", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + return app.DeleteSpell(ctx, args[0]) + }, + } + + return cmd +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..bc4f16b --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,103 @@ +package cli + +import ( + "context" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" + "os" + "spellbook/internal/models" + "strings" +) + +type Application interface { + AddSpell(ctx context.Context, spell models.Spell) error + GetSpell(ctx context.Context, spellName string) (models.Spell, error) + GetSpells(ctx context.Context, query models.SearchQuery) ([]models.Spell, error) + DeleteSpell(ctx context.Context, spellName string) error + CastSpell(ctx context.Context, spellCast models.SpellCast) (models.CastResult, error) +} + +func rootCmd(app Application) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if len(args) < 1 { + return listCommands(ctx, cmd, app) + } + + return runCommand(ctx, app, args) + } +} + +func runCommand(ctx context.Context, app Application, args []string) error { + spell, err := app.GetSpell(ctx, args[0]) + if err != nil { + return err + } + + m := make(map[string]string, len(args[1:])) + for _, pair := range args[1:] { + keyVal := strings.Split(pair, "=") + m[keyVal[0]] = keyVal[1] + } + + _, err = app.CastSpell(ctx, models.SpellCast{ + Spell: spell, + Args: m, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + }) + if err != nil { + return err + } + + return nil +} + +func listCommands(ctx context.Context, cmd *cobra.Command, app Application) error { + spells, err := app.GetSpells(ctx, models.SearchQuery{}) + if err != nil { + return err + } + + if len(spells) == 0 { + cmd.Println("Spell book is empty. Try `spellbook add` to add your first spell") + } + + return printTable(cmd, spells) +} + +func printTable(cmd *cobra.Command, spells []models.Spell) error { + table := tablewriter.NewTable(cmd.OutOrStdout()) + + rows := make([][]string, 0, len(spells)) + for _, spell := range spells { + row := []string{ + spell.Name, spell.Spell, spell.ShortDesc, strings.Join(spell.Tags, ", "), + } + rows = append(rows, row) + } + + table.Header("Name", "Spell", "Short description", "Tags") + _ = table.Bulk(rows) + if err := table.Render(); err != nil { + return err + } + + return nil +} + +func NewRootCmd(app Application) *cobra.Command { + cmd := &cobra.Command{ + Use: "spellbook", + Short: "CLI tool to store, manage and execute command templates", + Long: `Spellbook is a lightweight binary command manager written in Go. +It allows you to save complex shell commands as parametrized templates (spells) +and execute them instantly with custom arguments from anywhere in your terminal`, + RunE: rootCmd(app), + Args: cobra.ArbitraryArgs, + } + + return cmd +} diff --git a/tests/mock/mock.go b/tests/mock/mock.go new file mode 100644 index 0000000..684545e --- /dev/null +++ b/tests/mock/mock.go @@ -0,0 +1,46 @@ +package mock + +import ( + "context" + "fmt" + "spellbook/internal/adapters/caster" + "spellbook/internal/models" +) + +var testSpell = models.Spell{ + Id: 0, + Name: "test", + Spell: "curl {{.target}}", + ShortDesc: "test spell", + Tags: []string{"tag1", "tag2", "tag3"}, +} + +type Application struct{} + +func (a *Application) AddSpell(_ context.Context, spell models.Spell) error { + fmt.Println("TEST - AddSpell", spell) + return nil +} + +func (a *Application) GetSpell(_ context.Context, spellName string) (models.Spell, error) { + fmt.Println("TEST - GetSpell", spellName) + return testSpell, nil +} + +func (a *Application) GetSpells(_ context.Context, query models.SearchQuery) ([]models.Spell, error) { + fmt.Println("TEST - GetSpells", query) + + return []models.Spell{ + testSpell, + }, nil +} + +func (a *Application) DeleteSpell(_ context.Context, spellName string) error { + fmt.Println("TEST - DeleteSpell", spellName) + return nil +} + +func (a *Application) CastSpell(ctx context.Context, spellCast models.SpellCast) (models.CastResult, error) { + fmt.Println("TEST - CastSpell") + return (&caster.SpellCaster{}).Cast(ctx, spellCast) +}