feat: formulario de flags y filtro launcher en pipeline_launcher
Parsea flags de -help de cada pipeline para mostrar formulario de argumentos antes de ejecutar. Filtra pipelines por tag 'launcher'. Corrige selección en historial delegando enter al list antes de leer item.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
ops "fn-registry/fn_operations"
|
||||
"fn-registry/registry"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lucasdataproyects/devfactory/tui"
|
||||
@@ -17,12 +18,14 @@ type pipelinesState int
|
||||
const (
|
||||
pipelinesLoading pipelinesState = iota
|
||||
pipelinesList
|
||||
pipelinesArgs
|
||||
pipelinesRunning
|
||||
pipelinesOutput
|
||||
)
|
||||
|
||||
type pipelinesLoadedMsg []registry.Function
|
||||
type pipelineFinishedMsg RunResult
|
||||
type pipelineFlagsMsg []PipelineFlag
|
||||
|
||||
// PipelinesModel lists and launches pipelines.
|
||||
type PipelinesModel struct {
|
||||
@@ -31,6 +34,10 @@ type PipelinesModel struct {
|
||||
spinner tui.SpinnerModel
|
||||
styles tui.Styles
|
||||
pipelines []registry.Function
|
||||
selectedFn *registry.Function
|
||||
flags []PipelineFlag
|
||||
inputs []textinput.Model
|
||||
focusIdx int
|
||||
output string
|
||||
lastResult *RunResult
|
||||
scrollOff int
|
||||
@@ -66,10 +73,67 @@ func (m PipelinesModel) loadPipelines() tea.Cmd {
|
||||
if err != nil {
|
||||
return pipelinesLoadedMsg(nil)
|
||||
}
|
||||
return pipelinesLoadedMsg(fns)
|
||||
// Only show pipelines tagged with "launcher"
|
||||
var launchable []registry.Function
|
||||
for _, f := range fns {
|
||||
for _, t := range f.Tags {
|
||||
if t == "launcher" {
|
||||
launchable = append(launchable, f)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return pipelinesLoadedMsg(launchable)
|
||||
}
|
||||
}
|
||||
|
||||
// buildInputs creates a textinput for each flag, pre-filled with defaults.
|
||||
func (m *PipelinesModel) buildInputs() tea.Cmd {
|
||||
m.inputs = make([]textinput.Model, len(m.flags))
|
||||
for i, f := range m.flags {
|
||||
ti := textinput.New()
|
||||
ti.CharLimit = 256
|
||||
ti.Width = 40
|
||||
if f.Default != "" {
|
||||
ti.SetValue(f.Default)
|
||||
}
|
||||
if f.Required {
|
||||
ti.Placeholder = "(requerido)"
|
||||
}
|
||||
m.inputs[i] = ti
|
||||
}
|
||||
m.focusIdx = 0
|
||||
if len(m.inputs) > 0 {
|
||||
m.inputs[0].Focus()
|
||||
return textinput.Blink
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PipelinesModel) focusInput(idx int) tea.Cmd {
|
||||
if idx < 0 || idx >= len(m.inputs) {
|
||||
return nil
|
||||
}
|
||||
for i := range m.inputs {
|
||||
m.inputs[i].Blur()
|
||||
}
|
||||
m.focusIdx = idx
|
||||
m.inputs[idx].Focus()
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
// collectArgs builds CLI args from the form inputs.
|
||||
func (m PipelinesModel) collectArgs() []string {
|
||||
var args []string
|
||||
for i, f := range m.flags {
|
||||
val := strings.TrimSpace(m.inputs[i].Value())
|
||||
if val != "" {
|
||||
args = append(args, "--"+f.Name, val)
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case pipelinesLoadedMsg:
|
||||
@@ -86,19 +150,23 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) {
|
||||
m.state = pipelinesList
|
||||
return m, nil
|
||||
|
||||
case pipelineFlagsMsg:
|
||||
m.flags = []PipelineFlag(msg)
|
||||
cmd := m.buildInputs()
|
||||
return m, cmd
|
||||
|
||||
case pipelineFinishedMsg:
|
||||
result := RunResult(msg)
|
||||
m.lastResult = &result
|
||||
// Build output
|
||||
var sb strings.Builder
|
||||
if result.Status == ops.ExecSuccess {
|
||||
sb.WriteString("[OK] ")
|
||||
} else {
|
||||
sb.WriteString("[FAIL] ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Pipeline: %s\n", result.PipelineID))
|
||||
sb.WriteString(fmt.Sprintf("Execution: %s\n", result.ExecID))
|
||||
sb.WriteString(fmt.Sprintf("Duration: %dms\n", result.DurationMs))
|
||||
fmt.Fprintf(&sb, "Pipeline: %s\n", result.PipelineID)
|
||||
fmt.Fprintf(&sb, "Execution: %s\n", result.ExecID)
|
||||
fmt.Fprintf(&sb, "Duration: %dms\n", result.DurationMs)
|
||||
sb.WriteString("\n--- stdout ---\n")
|
||||
if result.Stdout != "" {
|
||||
sb.WriteString(result.Stdout)
|
||||
@@ -110,7 +178,7 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) {
|
||||
sb.WriteString(result.Stderr)
|
||||
}
|
||||
if result.Err != nil {
|
||||
sb.WriteString(fmt.Sprintf("\n--- error ---\n%v", result.Err))
|
||||
fmt.Fprintf(&sb, "\n--- error ---\n%v", result.Err)
|
||||
}
|
||||
m.output = sb.String()
|
||||
m.state = pipelinesOutput
|
||||
@@ -126,12 +194,42 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) {
|
||||
m.spinner = tui.NewSpinner("Loading pipelines...")
|
||||
return m, tea.Batch(m.spinner.Init(), m.loadPipelines())
|
||||
case "enter":
|
||||
updated, _ := m.list.Update(msg)
|
||||
m.list = updated.(tui.FilteredListModel)
|
||||
if item := m.list.SelectedItem(); item != nil {
|
||||
fn := item.Value.(registry.Function)
|
||||
m.state = pipelinesRunning
|
||||
m.spinner = tui.NewSpinner(fmt.Sprintf("Running %s...", fn.Name))
|
||||
return m, tea.Batch(m.spinner.Init(), m.runPipelineCmd(&fn))
|
||||
m.selectedFn = &fn
|
||||
m.flags = nil
|
||||
m.inputs = nil
|
||||
m.state = pipelinesArgs
|
||||
root := m.registryRoot
|
||||
fnCopy := fn
|
||||
return m, func() tea.Msg {
|
||||
return pipelineFlagsMsg(GetPipelineFlags(&fnCopy, root))
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
case pipelinesArgs:
|
||||
switch msg.String() {
|
||||
case "tab", "down":
|
||||
cmd := m.focusInput((m.focusIdx + 1) % max(len(m.inputs), 1))
|
||||
return m, cmd
|
||||
case "shift+tab", "up":
|
||||
idx := m.focusIdx - 1
|
||||
if idx < 0 {
|
||||
idx = max(len(m.inputs)-1, 0)
|
||||
}
|
||||
cmd := m.focusInput(idx)
|
||||
return m, cmd
|
||||
case "ctrl+enter", "ctrl+s":
|
||||
args := m.collectArgs()
|
||||
m.state = pipelinesRunning
|
||||
m.spinner = tui.NewSpinner(fmt.Sprintf("Running %s...", m.selectedFn.Name))
|
||||
return m, tea.Batch(m.spinner.Init(), m.runPipelineCmd(m.selectedFn, args))
|
||||
case "esc":
|
||||
m.state = pipelinesList
|
||||
return m, nil
|
||||
}
|
||||
case pipelinesOutput:
|
||||
switch msg.String() {
|
||||
@@ -157,16 +255,20 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) {
|
||||
var listModel tea.Model
|
||||
listModel, cmd = m.list.Update(msg)
|
||||
m.list = listModel.(tui.FilteredListModel)
|
||||
case pipelinesArgs:
|
||||
if m.focusIdx >= 0 && m.focusIdx < len(m.inputs) {
|
||||
m.inputs[m.focusIdx], cmd = m.inputs[m.focusIdx].Update(msg)
|
||||
}
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m PipelinesModel) runPipelineCmd(fn *registry.Function) tea.Cmd {
|
||||
func (m PipelinesModel) runPipelineCmd(fn *registry.Function, args []string) tea.Cmd {
|
||||
regRoot := m.registryRoot
|
||||
opsDB := m.opsDB
|
||||
fnCopy := *fn
|
||||
return func() tea.Msg {
|
||||
result := RunPipeline(&fnCopy, regRoot, opsDB)
|
||||
result := RunPipeline(&fnCopy, regRoot, opsDB, args)
|
||||
return pipelineFinishedMsg(result)
|
||||
}
|
||||
}
|
||||
@@ -174,6 +276,9 @@ func (m PipelinesModel) runPipelineCmd(fn *registry.Function) tea.Cmd {
|
||||
// HandleBack retrocede un nivel. Retorna true si ya en estado base.
|
||||
func (m *PipelinesModel) HandleBack() bool {
|
||||
switch m.state {
|
||||
case pipelinesArgs:
|
||||
m.state = pipelinesList
|
||||
return false
|
||||
case pipelinesOutput:
|
||||
m.state = pipelinesList
|
||||
return false
|
||||
@@ -192,6 +297,8 @@ func (m PipelinesModel) View() string {
|
||||
}
|
||||
help := m.styles.Muted.Render(" Enter: launch │ r: refresh │ /: filter")
|
||||
return m.list.View() + "\n" + help
|
||||
case pipelinesArgs:
|
||||
return m.renderArgsForm()
|
||||
case pipelinesRunning:
|
||||
return m.spinner.View()
|
||||
case pipelinesOutput:
|
||||
@@ -200,6 +307,48 @@ func (m PipelinesModel) View() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m PipelinesModel) renderArgsForm() string {
|
||||
header := m.styles.Header.Render(m.selectedFn.Name)
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, header, "")
|
||||
|
||||
if len(m.flags) == 0 {
|
||||
parts = append(parts, m.styles.Muted.Render(" Loading flags..."))
|
||||
} else if len(m.inputs) == 0 {
|
||||
parts = append(parts, m.styles.Muted.Render(" No flags available. Ctrl+S to run."))
|
||||
} else {
|
||||
for i, f := range m.flags {
|
||||
marker := " "
|
||||
if f.Required {
|
||||
marker = m.styles.Error.Render("* ")
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("--%-16s", f.Name)
|
||||
cursor := " "
|
||||
if i == m.focusIdx {
|
||||
cursor = m.styles.Info.Render("> ")
|
||||
}
|
||||
|
||||
label := fmt.Sprintf("%s%s%s", cursor, marker, m.styles.Label.Render(name))
|
||||
input := m.inputs[i].View()
|
||||
|
||||
desc := f.Desc
|
||||
if f.Default != "" {
|
||||
desc += m.styles.Muted.Render(fmt.Sprintf(" (default: %s)", f.Default))
|
||||
}
|
||||
|
||||
parts = append(parts, label+input)
|
||||
parts = append(parts, " "+m.styles.Muted.Render(desc))
|
||||
}
|
||||
}
|
||||
|
||||
parts = append(parts, "")
|
||||
parts = append(parts, m.styles.Muted.Render(" ↑/↓: navigate │ Ctrl+S: run │ Esc: cancel"))
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
}
|
||||
|
||||
func (m PipelinesModel) renderOutput() string {
|
||||
lines := splitLines(m.output)
|
||||
maxLines := 20
|
||||
|
||||
Reference in New Issue
Block a user