From 406a6f02fadf08443c0661b4a465b139f0377e3d Mon Sep 17 00:00:00 2001 From: Nofre Date: Wed, 14 May 2025 21:12:32 +0200 Subject: [PATCH 1/4] chore(mod): tided --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c8c9ff0..706b1d8 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require github.com/spf13/cobra v1.9.1 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.6 // indirect -) \ No newline at end of file +) From 228a42aa7d490be40a0d2ef612c2e4defad2be4f Mon Sep 17 00:00:00 2001 From: Nofre Date: Wed, 14 May 2025 21:14:17 +0200 Subject: [PATCH 2/4] chore(ci): go version updated --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 371eea4..8a571e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.24.2' + go-version: '1.24.3' - name: Ensure go mod tidy has zero output run: go mod tidy -v && git diff --exit-code @@ -55,7 +55,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.24.2' + go-version: '1.24.3' - name: Run go-semantic-release uses: go-semantic-release/action@v1 From b9f7689647f339f693a1b5840a922670d3ed1837 Mon Sep 17 00:00:00 2001 From: Nofre Date: Wed, 14 May 2025 21:41:07 +0200 Subject: [PATCH 3/4] feat(cmd): added simple run command sample --- cmd/root.go | 1 + cmd/run.go | 107 ++++++++++++++++++++++++++++++++++++ core/executor/executor.go | 24 ++++++++ core/plan/plan.go | 54 ++++++++++++++++++ go.mod | 26 ++++++++- go.sum | 49 +++++++++++++++++ plugins/http/http_plugin.go | 44 +++++++++++++++ plugins/interface.go | 31 +++++++++++ 8 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 cmd/run.go create mode 100644 core/executor/executor.go create mode 100644 core/plan/plan.go create mode 100644 plugins/http/http_plugin.go create mode 100644 plugins/interface.go diff --git a/cmd/root.go b/cmd/root.go index 5f77738..8984010 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,4 +13,5 @@ func Execute() { func init() { rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(runCmd) } diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..8c4f8a4 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,107 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "time" + + "github.com/charmbracelet/bubbles/progress" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run test suite with interactive CLI", + Run: func(cmd *cobra.Command, args []string) { + RunInteractiveTestUI() + }, +} + +type testCase struct { + Name string + Status string +} + +type model struct { + tests []testCase + progress progress.Model + index int + quitting bool +} + +func initialModel() model { + return model{ + tests: []testCase{ + {"Register User", "pending"}, + {"Login User", "pending"}, + {"Create Resource", "pending"}, + {"Delete Resource", "pending"}, + }, + progress: progress.New(progress.WithDefaultGradient()), + } +} + +func (m model) Init() tea.Cmd { + return tea.Batch( + tea.Tick(time.Second*3, func(t time.Time) tea.Msg { + return tickMsg{} + }), + ) +} + +type tickMsg struct{} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tickMsg: + if m.index >= len(m.tests) { + m.quitting = true + return m, tea.Quit + } + + m.tests[m.index].Status = "โœ“ passed" + m.index++ + return m, tea.Tick(700*time.Millisecond, func(t time.Time) tea.Msg { + return tickMsg{} + }) + + case tea.KeyMsg: + if msg.String() == "q" { + m.quitting = true + return m, tea.Quit + } + } + + return m, nil +} + +func (m model) View() string { + if m.quitting { + return lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("10")). + Render("\nโœ… All tests complete!\n\n") + } + + s := "๐Ÿงช Running test cases:\n\n" + + for i, t := range m.tests { + style := lipgloss.NewStyle() + if i == m.index { + style = style.Bold(true).Foreground(lipgloss.Color("12")) + } + s += style.Render(fmt.Sprintf("โ€ข %s [%s]", t.Name, t.Status)) + "\n" + } + + s += "\nPress 'q' to quit at any time.\n" + return s +} + +func RunInteractiveTestUI() { + if _, err := tea.NewProgram(initialModel()).Run(); err != nil { + fmt.Println("Error running UI:", err) + os.Exit(1) + } +} diff --git a/core/executor/executor.go b/core/executor/executor.go new file mode 100644 index 0000000..a8be0fc --- /dev/null +++ b/core/executor/executor.go @@ -0,0 +1,24 @@ +package executor + +import ( + "fmt" + "github.com/apiqube/cli/core/plan" + "github.com/apiqube/cli/plugins" +) + +func ExecutePlan(plan *plan.ExecutionPlan) { + for _, step := range plan.Steps { + plugin, err := plugins.GetPlugin(step.Type) + if err != nil { + fmt.Printf("โŒ Unknown plugin for step '%s'\n", step.Name) + continue + } + fmt.Printf("๐Ÿ”ง Executing step: %s\n", step.Name) + res, err := plugin.Execute(step, nil) + if err != nil || !res.Success { + fmt.Printf("โŒ Step '%s' failed: %v\n", step.Name, err) + continue + } + fmt.Printf("โœ… Step '%s' passed\n", step.Name) + } +} diff --git a/core/plan/plan.go b/core/plan/plan.go new file mode 100644 index 0000000..0941404 --- /dev/null +++ b/core/plan/plan.go @@ -0,0 +1,54 @@ +package plan + +import ( + "encoding/json" + "os" + "time" +) + +type StepConfig struct { + Name string `json:"name"` + Type string `json:"type"` + Method string `json:"method"` + URL string `json:"url"` + Body string `json:"body"` + Headers map[string]string `json:"headers"` +} + +type ExecutionPlan struct { + Name string `json:"name"` + Steps []StepConfig `json:"steps"` + Time time.Time `json:"time"` +} + +func BuildExecutionPlan(_ string) (*ExecutionPlan, error) { + return &ExecutionPlan{ + Name: "default-plan", + Time: time.Now(), + Steps: []StepConfig{{Name: "Example", Type: "http", Method: "GET", URL: "http://localhost"}}, + }, nil +} + +func SavePlan(plan *ExecutionPlan) error { + data, err := json.MarshalIndent(plan, "", " ") + if err != nil { + return err + } + + return os.WriteFile(".testman/plan.json", data, 0644) +} + +func LoadPlan() (*ExecutionPlan, error) { + data, err := os.ReadFile(".apiqube/plan.json") + if err != nil { + return nil, err + } + + var plan ExecutionPlan + + if err = json.Unmarshal(data, &plan); err != nil { + return nil, err + } + + return &plan, nil +} diff --git a/go.mod b/go.mod index c830ff8..4307c8c 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,33 @@ module github.com/apiqube/cli go 1.24.2 -require github.com/spf13/cobra v1.9.1 +require ( + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.5 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/spf13/cobra v1.9.1 +) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index ffae55e..11a8c07 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,59 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= +github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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= diff --git a/plugins/http/http_plugin.go b/plugins/http/http_plugin.go new file mode 100644 index 0000000..9ecd1b2 --- /dev/null +++ b/plugins/http/http_plugin.go @@ -0,0 +1,44 @@ +package http + +import ( + "bytes" + "fmt" + "github.com/apiqube/cli/core/plan" + "github.com/apiqube/cli/plugins" + "net/http" +) + +func init() { + plugins.Register(Plugin{}) +} + +type Plugin struct{} + +func (p Plugin) Name() string { + return "http" +} + +func (p Plugin) Execute(step plan.StepConfig, ctx interface{}) (plugins.PluginResult, error) { + req, _ := http.NewRequest(step.Method, step.URL, bytes.NewBuffer([]byte(step.Body))) + for k, v := range step.Headers { + req.Header.Set(k, v) + } + client := &http.Client{} + resp, err := client.Do(req) + + if err != nil { + return plugins.PluginResult{Success: false}, err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + fmt.Printf("Error closing response body %v\n", err) + } + }() + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return plugins.PluginResult{Success: true}, nil + } + + return plugins.PluginResult{Success: false}, nil +} diff --git a/plugins/interface.go b/plugins/interface.go new file mode 100644 index 0000000..212b668 --- /dev/null +++ b/plugins/interface.go @@ -0,0 +1,31 @@ +package plugins + +import ( + "fmt" + "github.com/apiqube/cli/core/plan" +) + +type Plugin interface { + Name() string + Execute(step plan.StepConfig, ctx interface{}) (PluginResult, error) +} + +type PluginResult struct { + Success bool + Code int + Message string +} + +var registry = map[string]Plugin{} + +func Register(p Plugin) { + registry[p.Name()] = p +} + +func GetPlugin(name string) (Plugin, error) { + p, ok := registry[name] + if !ok { + return nil, fmt.Errorf("plugin '%s' not found", name) + } + return p, nil +} From 4391eef5ce4bfc57874ca34a6620b8dd3c00b7c1 Mon Sep 17 00:00:00 2001 From: Nofre Date: Wed, 14 May 2025 21:41:57 +0200 Subject: [PATCH 4/4] chore(mod): tiding, linting --- cmd/run.go | 3 ++- core/executor/executor.go | 1 + core/plan/plan.go | 2 +- plugins/http/http_plugin.go | 4 ++-- plugins/interface.go | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 8c4f8a4..c58d62e 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -2,10 +2,11 @@ package cmd import ( "fmt" - "github.com/spf13/cobra" "os" "time" + "github.com/spf13/cobra" + "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" diff --git a/core/executor/executor.go b/core/executor/executor.go index a8be0fc..fe6ed2f 100644 --- a/core/executor/executor.go +++ b/core/executor/executor.go @@ -2,6 +2,7 @@ package executor import ( "fmt" + "github.com/apiqube/cli/core/plan" "github.com/apiqube/cli/plugins" ) diff --git a/core/plan/plan.go b/core/plan/plan.go index 0941404..5025231 100644 --- a/core/plan/plan.go +++ b/core/plan/plan.go @@ -35,7 +35,7 @@ func SavePlan(plan *ExecutionPlan) error { return err } - return os.WriteFile(".testman/plan.json", data, 0644) + return os.WriteFile(".testman/plan.json", data, 0o644) } func LoadPlan() (*ExecutionPlan, error) { diff --git a/plugins/http/http_plugin.go b/plugins/http/http_plugin.go index 9ecd1b2..2304f7d 100644 --- a/plugins/http/http_plugin.go +++ b/plugins/http/http_plugin.go @@ -3,9 +3,10 @@ package http import ( "bytes" "fmt" + "net/http" + "github.com/apiqube/cli/core/plan" "github.com/apiqube/cli/plugins" - "net/http" ) func init() { @@ -25,7 +26,6 @@ func (p Plugin) Execute(step plan.StepConfig, ctx interface{}) (plugins.PluginRe } client := &http.Client{} resp, err := client.Do(req) - if err != nil { return plugins.PluginResult{Success: false}, err } diff --git a/plugins/interface.go b/plugins/interface.go index 212b668..145a1f0 100644 --- a/plugins/interface.go +++ b/plugins/interface.go @@ -2,6 +2,7 @@ package plugins import ( "fmt" + "github.com/apiqube/cli/core/plan" )