Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ func Execute() {

func init() {
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(runCmd)
}
108 changes: 108 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cmd

import (
"fmt"
"os"
"time"

"github.com/spf13/cobra"

"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)
}
}
25 changes: 25 additions & 0 deletions core/executor/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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)
}
}
54 changes: 54 additions & 0 deletions core/plan/plan.go
Original file line number Diff line number Diff line change
@@ -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, 0o644)
}

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
}
26 changes: 25 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
49 changes: 49 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
44 changes: 44 additions & 0 deletions plugins/http/http_plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package http

import (
"bytes"
"fmt"
"net/http"

"github.com/apiqube/cli/core/plan"
"github.com/apiqube/cli/plugins"
)

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
}
32 changes: 32 additions & 0 deletions plugins/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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
}
Loading