Files
fn_registry/bash/functions/pipelines/init_cli_app.sh
T
egutierrez d763d8144f feat: init_cli_app bash pipeline — scaffold Go CLI app con TUI opcional
Genera apps/{nombre}/ con main.go (subcommand routing via os.Args + switch),
cmd_version.go, cmd_status.go, Makefile (build/run/install/test/vet/clean),
.gitignore, go.mod y app.md. Sin cobra/urfave — consistente con el resto de
apps del registry.

Flag --with-tui anade model.go con un modelo Bubbletea fullscreen (lista
filtrable con bubbles/list, spinner con bubbles/spinner, dark theme con
lipgloss). main.go arranca la TUI con tea.NewProgram(m, WithAltScreen) si no
hay args; sino hace subcommand routing normal.

Verifica con go mod tidy + go vet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:56:55 +02:00

461 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# init_cli_app
# ------------
# Scaffold de Go CLI app con subcomandos, opcionalmente con TUI Bubbletea.
#
# Genera main.go con routing de subcomandos (os.Args + switch), cmd_version.go,
# cmd_status.go, Makefile, .gitignore, go.mod y app.md.
#
# Con --with-tui genera ademas model.go con un modelo Bubbletea base y main.go
# arranca la TUI con tea.NewProgram().Run() en modo fullscreen.
#
# USO:
# ./init_cli_app.sh <nombre> [--with-tui]
#
# EJEMPLOS:
# ./init_cli_app.sh my_cli
# ./init_cli_app.sh deploy_helper --with-tui
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
NOMBRE=""
WITH_TUI="false"
while [ $# -gt 0 ]; do
case "$1" in
--with-tui) WITH_TUI="true"; shift ;;
-h|--help) grep "^#" "$0" | sed 's/^# \?//' ; exit 0 ;;
-*) echo "Flag desconocido: $1" >&2 ; exit 1 ;;
*)
if [ -z "$NOMBRE" ]; then NOMBRE="$1"
else echo "Argumento extra ignorado: $1" >&2
fi
shift ;;
esac
done
if [ -z "$NOMBRE" ]; then
echo "Uso: $0 <nombre> [--with-tui]" >&2
exit 1
fi
APP_DIR="${REGISTRY_ROOT}/apps/${NOMBRE}"
if [ -d "$APP_DIR" ]; then
echo "ERROR: ${APP_DIR} ya existe. Abortando." >&2
exit 1
fi
echo ""
echo "════════════════════════════════════════════════════════════"
echo " INIT CLI APP: ${NOMBRE}"
echo " Directorio: ${APP_DIR}"
echo " TUI: ${WITH_TUI}"
echo "════════════════════════════════════════════════════════════"
echo ""
# ── 1. Verificar Go ──────────────────────────────────────────
echo "[1/5] Verificando herramientas..."
assert_command_exists go
echo " Go: $(go version)"
# ── 2. Crear estructura ──────────────────────────────────────
echo "[2/5] Creando estructura..."
mkdir -p "$APP_DIR"
# go.mod
if [ "$WITH_TUI" = "true" ]; then
cat > "$APP_DIR/go.mod" <<EOF
module ${NOMBRE}
go 1.25.0
require (
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
)
require (
fn-registry v0.0.0-00010101000000-000000000000
)
replace fn-registry => ${REGISTRY_ROOT}
EOF
else
cat > "$APP_DIR/go.mod" <<EOF
module ${NOMBRE}
go 1.25.0
require (
fn-registry v0.0.0-00010101000000-000000000000
)
replace fn-registry => ${REGISTRY_ROOT}
EOF
fi
# ── 3. Archivos Go ───────────────────────────────────────────
echo "[3/5] Escribiendo archivos Go..."
# cmd_version.go — siempre existe
cat > "$APP_DIR/cmd_version.go" <<EOF
package main
import "fmt"
var version = "0.1.0"
func cmdVersion() {
fmt.Printf("${NOMBRE} %s\n", version)
}
EOF
# cmd_status.go — siempre existe
cat > "$APP_DIR/cmd_status.go" <<EOF
package main
import (
"fmt"
"runtime"
)
func cmdStatus() {
fmt.Printf("app: ${NOMBRE}\n")
fmt.Printf("version: %s\n", version)
fmt.Printf("go: %s\n", runtime.Version())
fmt.Printf("os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
EOF
if [ "$WITH_TUI" = "true" ]; then
# model.go — Bubbletea BaseModel con spinner y lista
cat > "$APP_DIR/model.go" <<EOF
package main
import (
"fmt"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// --- estilos (dark theme) ---
var (
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7D56F4"))
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262"))
selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#06B6D4"))
)
// --- item de la lista ---
type item struct {
title, desc string
}
func (i item) Title() string { return i.title }
func (i item) Description() string { return i.desc }
func (i item) FilterValue() string { return i.title }
// --- modelo raiz ---
type Model struct {
spinner spinner.Model
list list.Model
status string
quit bool
}
func NewModel() Model {
items := []list.Item{
item{title: "Deploy", desc: "Subir codigo al VPS"},
item{title: "Status", desc: "Ver estado de servicios"},
item{title: "Logs", desc: "Tail de logs en tiempo real"},
item{title: "Exit", desc: "Salir"},
}
l := list.New(items, list.NewDefaultDelegate(), 0, 0)
l.Title = "${NOMBRE} — elige una accion"
l.Styles.Title = titleStyle
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = selectedStyle
return Model{
spinner: s,
list: l,
status: "listo",
}
}
func (m Model) Init() tea.Cmd {
return tea.Batch(m.spinner.Tick)
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
m.quit = true
return m, tea.Quit
case "enter":
if it, ok := m.list.SelectedItem().(item); ok {
if it.title == "Exit" {
m.quit = true
return m, tea.Quit
}
m.status = fmt.Sprintf("seleccionado: %s", it.title)
}
}
case tea.WindowSizeMsg:
m.list.SetSize(msg.Width, msg.Height-4)
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
if m.quit {
return "adios!\n"
}
return fmt.Sprintf(
"%s\n\n%s %s\n%s",
m.list.View(),
m.spinner.View(),
m.status,
helpStyle.Render("enter: seleccionar · q: salir"),
)
}
EOF
# main.go con TUI
cat > "$APP_DIR/main.go" <<EOF
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
if len(os.Args) < 2 {
runTUI()
return
}
switch os.Args[1] {
case "tui":
runTUI()
case "version":
cmdVersion()
case "status":
cmdStatus()
case "help", "-h", "--help":
printUsage()
default:
fmt.Fprintf(os.Stderr, "comando desconocido: %s\n", os.Args[1])
printUsage()
os.Exit(1)
}
}
func runTUI() {
p := tea.NewProgram(NewModel(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "tui error: %v\n", err)
os.Exit(1)
}
}
func printUsage() {
fmt.Println(\`${NOMBRE} — CLI tool
Uso:
${NOMBRE} [comando]
Sin comando arranca la TUI fullscreen.
Comandos:
tui Arranca la TUI fullscreen (default)
version Imprime la version
status Muestra info del sistema
help Muestra esta ayuda\`)
}
EOF
else
# main.go sin TUI — subcommand routing clasico
cat > "$APP_DIR/main.go" <<EOF
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
switch os.Args[1] {
case "version":
cmdVersion()
case "status":
cmdStatus()
case "help", "-h", "--help":
printUsage()
default:
fmt.Fprintf(os.Stderr, "comando desconocido: %s\n", os.Args[1])
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println(\`${NOMBRE} — CLI tool
Uso:
${NOMBRE} <comando>
Comandos:
version Imprime la version
status Muestra info del sistema
help Muestra esta ayuda\`)
}
EOF
fi
# ── 4. Makefile, .gitignore, app.md ─────────────────────────
echo "[4/5] Escribiendo Makefile, .gitignore, app.md..."
cat > "$APP_DIR/Makefile" <<EOF
.PHONY: build run install test vet clean
BIN=${NOMBRE}
build:
CGO_ENABLED=1 go build -tags fts5 -o \$(BIN) .
run: build
./\$(BIN) \$(ARGS)
install: build
install -m755 \$(BIN) \$(HOME)/.local/bin/\$(BIN)
@echo "instalado en \$(HOME)/.local/bin/\$(BIN)"
test:
CGO_ENABLED=1 go test -tags fts5 -v ./...
vet:
CGO_ENABLED=1 go vet -tags fts5 ./...
clean:
rm -f \$(BIN)
EOF
cat > "$APP_DIR/.gitignore" <<EOF
# Binario
${NOMBRE}
# IDE
.idea/
.vscode/
*.swp
EOF
if [ "$WITH_TUI" = "true" ]; then
FRAMEWORK="bubbletea"
USES_FUNCTIONS=' - new_base_model_go_tui
- dark_styles_go_tui
- run_fullscreen_go_tui
- new_spinner_go_tui
- new_filtered_list_go_tui'
TAGS='[cli, tui, bubbletea]'
else
FRAMEWORK=""
USES_FUNCTIONS=' []'
TAGS='[cli]'
fi
cat > "$APP_DIR/app.md" <<EOF
---
name: ${NOMBRE}
lang: go
domain: tools
description: "CLI app generada por init_cli_app."
tags: ${TAGS}
uses_functions:
${USES_FUNCTIONS}
uses_types: []
framework: "${FRAMEWORK}"
entry_point: "main.go"
dir_path: "apps/${NOMBRE}"
---
## Notas
CLI con routing de subcomandos (\`os.Args\` + switch). Sin cobra/urfave —
consistente con las apps del registry.
Ejecutar: \`make run ARGS="version"\` o \`./\${NOMBRE} status\`.
EOF
# ── 5. go mod tidy + go vet ─────────────────────────────────
echo "[5/5] Verificacion..."
(
cd "$APP_DIR"
if CGO_ENABLED=1 go mod tidy 2>&1 | tail -5; then
:
fi
if CGO_ENABLED=1 go vet -tags fts5 ./... 2>&1; then
echo " go vet OK"
else
echo " WARN: go vet fallo" >&2
fi
)
echo ""
echo "════════════════════════════════════════════════════════════"
echo " CLI APP '${NOMBRE}' LISTA"
echo "════════════════════════════════════════════════════════════"
echo ""
echo " Pasos siguientes:"
echo " cd apps/${NOMBRE}"
echo " make build"
if [ "$WITH_TUI" = "true" ]; then
echo " ./${NOMBRE} # arranca la TUI fullscreen"
echo " ./${NOMBRE} version # comando CLI"
else
echo " ./${NOMBRE} version"
echo " ./${NOMBRE} status"
fi
echo ""