bcfe87af7f
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>
461 lines
10 KiB
Bash
Executable File
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 ""
|