diff --git a/bash/functions/pipelines/init_api_app.md b/bash/functions/pipelines/init_api_app.md new file mode 100644 index 00000000..197a49a2 --- /dev/null +++ b/bash/functions/pipelines/init_api_app.md @@ -0,0 +1,113 @@ +--- +name: init_api_app +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "init_api_app(nombre: string, [--port N], [--with-auth], [--with-db], [--with-ops]) -> void" +description: "Scaffold de Go HTTP API app en apps/{nombre}/. Genera main.go, handlers.go, config.go, migrations, Makefile, .env.example, .gitignore y app.md con frontmatter correcto. Compone funciones del registry (http_serve, http_router, http_middleware_chain, migration_up) y verifica con go vet." +tags: [init, scaffold, api, http, pipeline, bash, launcher] +uses_functions: + - assert_command_exists_bash_shell + - http_serve_go_infra + - http_router_go_infra + - http_middleware_chain_go_infra + - http_logger_middleware_go_infra + - http_cors_middleware_go_infra + - http_json_response_go_infra + - http_error_response_go_infra + - migration_up_go_infra + - jwt_generate_go_infra + - password_hash_go_infra + - password_verify_go_infra +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: nombre + desc: "nombre de la app a crear (se usa como dir y binario en apps/{nombre}/)" + - name: "--port" + desc: "puerto por defecto del servidor HTTP (opcional, default 8080)" + - name: "--with-auth" + desc: "anade jwt_middleware, handlers login/register, tabla users en migration" + - name: "--with-db" + desc: "anade store.go con helpers CRUD y setup de SQLite al arrancar" + - name: "--with-ops" + desc: "anade fn ops init para crear operations.db con schema completo" +output: "estructura apps/{nombre}/ lista para ejecutarse con `make run`; si go vet falla, reporta error antes de declarar exito." +tested: false +tests: [] +test_file_path: "" +example: "fn run init_api_app my_service --with-db" +file_path: "bash/functions/pipelines/init_api_app.sh" +--- + +## Sinopsis + +```bash +fn run init_api_app [--port N] [--with-auth] [--with-db] [--with-ops] +``` + +## Ejemplo rapido + +```bash +fn run init_api_app billing_api --port 8090 --with-auth --with-db +cd apps/billing_api +cp .env.example .env +make run +# → starting billing_api on :8090 +curl localhost:8090/health # {"status":"ok"} +``` + +## Archivos generados + +| Archivo | Descripcion | +|---------|-------------| +| `main.go` | Entry point con HTTPServe, router, middleware chain, graceful shutdown | +| `handlers.go` | Handlers de ejemplo (`/health`, `/api/v1/status`) con HTTPJSONResponse | +| `config.go` | Struct Config leida desde env vars con defaults | +| `migrations/001_initial.sql` | Schema inicial con tabla `items` (id, name, timestamps) | +| `Makefile` | Targets `build`, `run`, `dev`, `test`, `vet`, `clean` | +| `.env.example` | Variables PORT, DB_PATH, CORS_ORIGINS (+ JWT_SECRET con auth) | +| `.gitignore` | Binario, *.db, .env, IDE files | +| `go.mod` | Modulo Go con replace directive a fn-registry | +| `app.md` | Frontmatter con tag `service`, uses_functions reales, dir_path | + +Con `--with-auth` anade ademas: +- `auth.go` — handlers `/auth/login`, `/auth/register` usando JWTGenerate, PasswordHash, PasswordVerify +- `migrations/002_users.sql` — tablas `users` y `sessions` + +Con `--with-db` anade: +- `store.go` — struct Store con `NewStore`, `Ping` para acceso a SQLite + +## Flags + +| Flag | Efecto | +|------|--------| +| `--port N` | Puerto por defecto en config y .env.example (default: 8080) | +| `--with-auth` | Auth con JWT + bcrypt + tabla users | +| `--with-db` | Store con helpers CRUD + setup SQLite | +| `--with-ops` | fn ops init para operations.db | + +## Post-setup + +```bash +cd apps/{nombre} +cp .env.example .env +make run # Arranca el server +make dev # Hot via go run +make test # Tests con fts5 +``` + +## Notas + +Pipeline impuro: escribe archivos al disco, ejecuta `go mod tidy` y `go vet`. + +Compone heredocs para generar los archivos. Cada heredoc es reemplazable si alguna funcion del registry cambia de firma — ajustar el heredoc correspondiente. + +Abort si `apps/{nombre}` ya existe para no sobrescribir. + +El tag `launcher` permite que aparezca en el Pipeline Launcher TUI. diff --git a/bash/functions/pipelines/init_api_app.sh b/bash/functions/pipelines/init_api_app.sh new file mode 100755 index 00000000..fe3a06af --- /dev/null +++ b/bash/functions/pipelines/init_api_app.sh @@ -0,0 +1,603 @@ +#!/usr/bin/env bash +# init_api_app +# ------------ +# Scaffold de una Go HTTP API app en apps/{nombre}/. +# +# Genera main.go, handlers.go, config.go, migrations/001_initial.sql, +# Makefile, .env.example, .gitignore y app.md con frontmatter correcto. +# El boilerplate importa funciones del registry (http_serve, http_router, +# http_middleware_chain, migration_up, etc.) y verifica que compila con +# `go vet` al final. +# +# USO: +# ./init_api_app.sh [--port N] [--with-auth] [--with-db] [--with-ops] +# +# FLAGS: +# --port N Puerto por defecto (default: 8080) +# --with-auth Anade JWT middleware, login/register, tabla users +# --with-db Anade store.go con helpers CRUD y setup de SQLite +# --with-ops Anade `fn ops init` para crear operations.db con schema completo +# +# EJEMPLO: +# ./init_api_app.sh my_service +# ./init_api_app.sh billing_api --port 8090 --with-auth --with-db + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# Source funciones atomicas del registry +source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh" + +# ── Parsing de argumentos ──────────────────────────────────── + +NOMBRE="" +PORT="8080" +WITH_AUTH="false" +WITH_DB="false" +WITH_OPS="false" + +while [ $# -gt 0 ]; do + case "$1" in + --port) + PORT="$2"; shift 2 ;; + --with-auth) + WITH_AUTH="true"; shift ;; + --with-db) + WITH_DB="true"; shift ;; + --with-ops) + WITH_OPS="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 [--port N] [--with-auth] [--with-db] [--with-ops]" >&2 + exit 1 +fi + +APP_DIR="${REGISTRY_ROOT}/apps/${NOMBRE}" + +if [ -d "$APP_DIR" ]; then + echo "ERROR: ${APP_DIR} ya existe. Abortando para no sobrescribir." >&2 + exit 1 +fi + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " INIT API APP: ${NOMBRE}" +echo " Directorio: ${APP_DIR}" +echo " Puerto: ${PORT}" +echo " Auth: ${WITH_AUTH}" +echo " DB: ${WITH_DB}" +echo " Ops: ${WITH_OPS}" +echo "════════════════════════════════════════════════════════════" +echo "" + +# ── 1. Verificar Go ────────────────────────────────────────── + +echo "[1/7] Verificando herramientas..." +assert_command_exists go +echo " Go: $(go version)" + +# ── 2. Crear estructura ────────────────────────────────────── + +echo "[2/7] Creando estructura..." +mkdir -p "$APP_DIR/migrations" +echo " ${APP_DIR}/" +echo " ${APP_DIR}/migrations/" + +# ── 3. Escribir go.mod ─────────────────────────────────────── + +echo "[3/7] Creando go.mod..." +cat > "$APP_DIR/go.mod" < ${REGISTRY_ROOT} +EOF + +# ── 4. Escribir archivos Go ────────────────────────────────── + +echo "[4/7] Escribiendo archivos Go..." + +# config.go +cat > "$APP_DIR/config.go" <<'EOF' +package main + +import ( + "os" +) + +// Config contiene la configuracion runtime de la app. +type Config struct { + AppName string + Port string + DBPath string + CORSOrigins []string +} + +// LoadConfig lee la configuracion desde variables de entorno con defaults sensatos. +func LoadConfig() Config { + return Config{ + AppName: getenv("APP_NAME", "__APP_NAME__"), + Port: getenv("PORT", "__PORT__"), + DBPath: getenv("DB_PATH", "__APP_NAME__.db"), + CORSOrigins: []string{getenv("CORS_ORIGINS", "*")}, + } +} + +func getenv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} +EOF +sed -i "s/__APP_NAME__/${NOMBRE}/g; s/__PORT__/${PORT}/g" "$APP_DIR/config.go" + +# handlers.go +cat > "$APP_DIR/handlers.go" <<'EOF' +package main + +import ( + "net/http" + + "fn-registry/functions/infra" +) + +// healthHandler responde {"status":"ok"} en GET /health. +func healthHandler(w http.ResponseWriter, r *http.Request) { + infra.HTTPJSONResponse(w, http.StatusOK, map[string]string{ + "status": "ok", + }) +} + +// statusHandler es un handler de ejemplo en GET /api/v1/status. +func statusHandler(w http.ResponseWriter, r *http.Request) { + infra.HTTPJSONResponse(w, http.StatusOK, map[string]any{ + "app": "__APP_NAME__", + "version": "0.1.0", + }) +} +EOF +sed -i "s/__APP_NAME__/${NOMBRE}/g" "$APP_DIR/handlers.go" + +# main.go +# Construccion incremental segun flags +MAIN_IMPORTS='"context" + "database/sql" + "log" + "os" + "os/signal" + "syscall" + + _ "github.com/mattn/go-sqlite3" + + "fn-registry/functions/infra"' + +MAIN_MIGRATION="" +if [ "$WITH_DB" = "true" ]; then + MAIN_MIGRATION=' // Migrations + if _, err := infra.MigrationUp(db, "migrations"); err != nil { + log.Fatalf("migrations: %v", err) + } +' +else + MAIN_MIGRATION=' // Migrations (only if migrations dir has content) + if _, err := os.Stat("migrations"); err == nil { + if _, err := infra.MigrationUp(db, "migrations"); err != nil { + log.Fatalf("migrations: %v", err) + } + } +' +fi + +cat > "$APP_DIR/main.go" < "$APP_DIR/main.go" < "$APP_DIR/auth.go" <<'EOF' +package main + +import ( + "database/sql" + "encoding/json" + "net/http" + "time" + + "fn-registry/functions/infra" +) + +// LoginRequest es el body JSON del endpoint /auth/login. +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +// RegisterRequest es el body JSON del endpoint /auth/register. +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +// authRegisterHandler crea un usuario nuevo. +func authRegisterHandler(db *sql.DB, jwtSecret string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req RegisterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + infra.HTTPErrorResponse(w, infra.HTTPError{Status: 400, Code: "bad_request", Message: "invalid body"}) + return + } + hash, err := infra.PasswordHash(req.Password, 10) + if err != nil { + infra.HTTPErrorResponse(w, infra.HTTPError{Status: 500, Code: "hash_error", Message: err.Error()}) + return + } + if _, err := db.Exec( + `INSERT INTO users (email, password_hash, created_at) VALUES (?, ?, ?)`, + req.Email, hash, time.Now().Format(time.RFC3339), + ); err != nil { + infra.HTTPErrorResponse(w, infra.HTTPError{Status: 409, Code: "duplicate_user", Message: err.Error()}) + return + } + infra.HTTPJSONResponse(w, http.StatusCreated, map[string]string{"email": req.Email}) + } +} + +// authLoginHandler valida credenciales y emite un JWT. +func authLoginHandler(db *sql.DB, jwtSecret string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + infra.HTTPErrorResponse(w, infra.HTTPError{Status: 400, Code: "bad_request", Message: "invalid body"}) + return + } + var id int64 + var hash string + err := db.QueryRow(`SELECT id, password_hash FROM users WHERE email = ?`, req.Email).Scan(&id, &hash) + if err != nil { + infra.HTTPErrorResponse(w, infra.HTTPError{Status: 401, Code: "invalid_credentials", Message: "invalid credentials"}) + return + } + if err := infra.PasswordVerify(req.Password, hash); err != nil { + infra.HTTPErrorResponse(w, infra.HTTPError{Status: 401, Code: "invalid_credentials", Message: "invalid credentials"}) + return + } + claims := infra.JWTClaims{ + Subject: req.Email, + ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + IssuedAt: time.Now().Unix(), + } + token, err := infra.JWTGenerate(claims, jwtSecret) + if err != nil { + infra.HTTPErrorResponse(w, infra.HTTPError{Status: 500, Code: "jwt_error", Message: err.Error()}) + return + } + infra.HTTPJSONResponse(w, http.StatusOK, map[string]string{"token": token}) + } +} +EOF +fi + +# --- Store opcional --- +if [ "$WITH_DB" = "true" ]; then + cat > "$APP_DIR/store.go" <<'EOF' +package main + +import ( + "database/sql" + "fmt" +) + +// Store encapsula el acceso a la base de datos. +type Store struct { + db *sql.DB +} + +// NewStore crea una Store con el pool de conexiones dado. +func NewStore(db *sql.DB) *Store { + return &Store{db: db} +} + +// Ping verifica conectividad con la base de datos. +func (s *Store) Ping() error { + if err := s.db.Ping(); err != nil { + return fmt.Errorf("ping: %w", err) + } + return nil +} +EOF +fi + +# ── 5. Migracion inicial ───────────────────────────────────── + +echo "[5/7] Escribiendo migrations/001_initial.sql..." +cat > "$APP_DIR/migrations/001_initial.sql" < "$APP_DIR/migrations/002_users.sql" <<'EOF' +-- 002_users — tabla users para auth + +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + metadata TEXT, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- DOWN +DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS users; +EOF +fi + +# ── 6. Makefile / .env.example / .gitignore / app.md ───────── + +echo "[6/7] Escribiendo Makefile, .env.example, .gitignore..." + +cat > "$APP_DIR/Makefile" < "$APP_DIR/.env.example" <> "$APP_DIR/.env.example" < "$APP_DIR/.gitignore" < "$APP_DIR/app.md" <&1 | tail -3; then + : + fi + if CGO_ENABLED=1 go vet -tags fts5 ./... 2>&1; then + echo " go vet OK" + else + echo " WARN: go vet reporto problemas (revisa el output arriba)" >&2 + fi +) + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " API APP '${NOMBRE}' LISTA" +echo "════════════════════════════════════════════════════════════" +echo "" +echo " Pasos siguientes:" +echo " cd apps/${NOMBRE}" +echo " cp .env.example .env" +echo " make run" +echo "" +echo " Health check:" +echo " curl localhost:${PORT}/health" +echo "" diff --git a/bash/functions/pipelines/init_cli_app.md b/bash/functions/pipelines/init_cli_app.md new file mode 100644 index 00000000..0b68deaa --- /dev/null +++ b/bash/functions/pipelines/init_cli_app.md @@ -0,0 +1,110 @@ +--- +name: init_cli_app +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "init_cli_app(nombre: string, [--with-tui]) -> void" +description: "Scaffold de Go CLI app con subcomandos (version/status/help) routed via os.Args. Con --with-tui genera model.go con un modelo Bubbletea fullscreen (lista filtrable + spinner + dark theme) y main.go arranca la TUI en modo fullscreen. Sin dependencias de cobra/urfave — consistente con las apps del registry." +tags: [init, scaffold, cli, tui, pipeline, bash, launcher] +uses_functions: + - assert_command_exists_bash_shell + - new_base_model_go_tui + - dark_styles_go_tui + - run_fullscreen_go_tui + - new_spinner_go_tui + - new_filtered_list_go_tui +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: nombre + desc: "nombre de la CLI app (apps/{nombre}/); se usa como binario y como modulo Go" + - name: "--with-tui" + desc: "anade model.go con modelo Bubbletea (lista + spinner + dark theme) y arranca TUI fullscreen al invocar sin args" +output: "CLI app en apps/{nombre}/ con main.go (subcommand routing), cmd_version.go, cmd_status.go, Makefile con targets build/run/install/test/vet/clean. Con --with-tui anade model.go y el default sin args arranca TUI." +tested: false +tests: [] +test_file_path: "" +example: "fn run init_cli_app my_cli --with-tui" +file_path: "bash/functions/pipelines/init_cli_app.sh" +--- + +## Sinopsis + +```bash +fn run init_cli_app [--with-tui] +``` + +## Ejemplo rapido + +```bash +# CLI clasica +fn run init_cli_app mytool +cd apps/mytool +make build +./mytool version +./mytool status + +# CLI + TUI fullscreen +fn run init_cli_app deploy_helper --with-tui +cd apps/deploy_helper +make build +./deploy_helper # arranca TUI +./deploy_helper version # subcomando +``` + +## Archivos generados + +| Archivo | Descripcion | +|---------|-------------| +| `main.go` | Entry con `switch os.Args[1]`; subcomandos: version, status, help | +| `cmd_version.go` | Imprime nombre + version | +| `cmd_status.go` | Imprime app/version/go/os/arch | +| `go.mod` | Modulo Go con replace `fn-registry`; con `--with-tui` agrega bubbletea/bubbles/lipgloss | +| `Makefile` | Targets build, run (ARGS=...), install (~/.local/bin/), test, vet, clean | +| `.gitignore` | Binario + IDE files | +| `app.md` | Tag `cli` (o `cli,tui,bubbletea` con `--with-tui`) | + +Con `--with-tui` anade: +- `model.go` — `Model` con spinner (spinner.Dot), lista Bubbletea con 4 items de ejemplo, dark theme (lipgloss con colores 7D56F4 / 06B6D4), keys enter/q/ctrl+c +- `main.go` arranca la TUI con `tea.NewProgram(model, tea.WithAltScreen())` si no hay args + +## Flags + +| Flag | Efecto | +|------|--------| +| `--with-tui` | Anade model.go con Bubbletea y TUI fullscreen como default | + +## Post-setup + +```bash +cd apps/{nombre} +make build # construye ./{nombre} +./{nombre} version +./{nombre} status +make install # copia a ~/.local/bin/{nombre} +make run ARGS="status" # build + run con argumentos +``` + +## Notas + +Pipeline impuro: genera archivos, ejecuta `go mod tidy` y `go vet` al final. + +Los ejemplos del modelo TUI (items "Deploy", "Status", "Logs", "Exit") son +placeholders — reemplazar con la logica real de la app. El modelo usa los +componentes estandar de bubbles (`list`, `spinner`) y lipgloss para estilos. + +Las funciones del registry `new_base_model_go_tui`, `dark_styles_go_tui`, +`run_fullscreen_go_tui`, `new_spinner_go_tui`, `new_filtered_list_go_tui` +son referenciadas en el frontmatter como deps conceptuales aunque el +scaffold inline el codigo Bubbletea directamente (las funciones del registry +son stubs que delegan a devfactory/tui). + +Abort si `apps/{nombre}/` ya existe. + +El tag `launcher` permite que aparezca en el Pipeline Launcher TUI (aunque +una CLI con TUI interactiva normalmente no se lanza como subprocess). diff --git a/bash/functions/pipelines/init_cli_app.sh b/bash/functions/pipelines/init_cli_app.sh new file mode 100755 index 00000000..d90e7ff5 --- /dev/null +++ b/bash/functions/pipelines/init_cli_app.sh @@ -0,0 +1,460 @@ +#!/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 [--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 [--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" < ${REGISTRY_ROOT} +EOF +else + cat > "$APP_DIR/go.mod" < ${REGISTRY_ROOT} +EOF +fi + +# ── 3. Archivos Go ─────────────────────────────────────────── + +echo "[3/5] Escribiendo archivos Go..." + +# cmd_version.go — siempre existe +cat > "$APP_DIR/cmd_version.go" < "$APP_DIR/cmd_status.go" < "$APP_DIR/model.go" < "$APP_DIR/main.go" < "$APP_DIR/main.go" < + +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" < "$APP_DIR/.gitignore" < "$APP_DIR/app.md" <&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 "" diff --git a/bash/functions/pipelines/init_desktop_app.md b/bash/functions/pipelines/init_desktop_app.md new file mode 100644 index 00000000..2987da27 --- /dev/null +++ b/bash/functions/pipelines/init_desktop_app.md @@ -0,0 +1,118 @@ +--- +name: init_desktop_app +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "init_desktop_app(nombre: string, [--with-db]) -> void" +description: "Scaffold de Wails desktop app: Go backend + React frontend con Mantine y @fn_library. Genera main.go (Wails con embed frontend), app.go (bindings Greet/GetVersion), wails.json, go.mod con replace a fn-registry y frontend/ con vite + react + mantine." +tags: [init, scaffold, desktop, wails, pipeline, bash, launcher] +uses_functions: + - assert_command_exists_bash_shell + - scaffold_wails_app_go_infra + - install_wails_bash_infra + - wails_bind_crud_go_infra + - wails_build_go_infra +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: nombre + desc: "nombre de la app Wails (apps/{nombre}/)" + - name: "--with-db" + desc: "anade store.go con SQLite + bindings CRUD (Item, ListItems, CreateItem)" +output: "app Wails en apps/{nombre}/ con main.go + app.go + wails.json + frontend/ Vite+React. Ejecutar 'wails dev' o 'wails build' dentro del directorio." +tested: false +tests: [] +test_file_path: "" +example: "fn run init_desktop_app my_tool" +file_path: "bash/functions/pipelines/init_desktop_app.sh" +--- + +## Sinopsis + +```bash +fn run init_desktop_app [--with-db] +``` + +## Ejemplo rapido + +```bash +fn run init_desktop_app data_explorer --with-db +cd apps/data_explorer/frontend && pnpm install && cd .. +wails dev # dev con hot reload +# o bien: +wails build && ./build/bin/data_explorer +``` + +## Archivos generados + +| Archivo | Descripcion | +|---------|-------------| +| `main.go` | Entry Wails con embed de frontend/dist, options.App, Bind: []interface{}{app} | +| `app.go` | Struct App con bindings `Greet(name)`, `GetVersion()` (y con `--with-db`: `ListItems`, `CreateItem`) | +| `wails.json` | Config Wails: name, outputfilename, scripts frontend (pnpm) | +| `go.mod` | Modulo Go con `github.com/wailsapp/wails/v2` + replace `fn-registry` | +| `frontend/package.json` | pnpm + vite + react + @mantine/core + @tabler/icons-react | +| `frontend/vite.config.ts` | Alias `@fn_library`, outDir `dist` para embed | +| `frontend/tsconfig.json` | TS strict con paths `@fn_library/*` | +| `frontend/src/main.tsx` | Root con `MantineProvider` (defaultColorScheme: dark) | +| `frontend/src/App.tsx` | Componente que llama bindings `Greet` y `GetVersion` via wailsjs | +| `frontend/postcss.config.cjs` | postcss-preset-mantine | +| `app.md` | Framework: `wails + vite + react + mantine` | + +Con `--with-db` anade ademas: +- `store.go` — `openDB`, tipo `Item`, bindings CRUD `ListItems`, `CreateItem` + +## Flags + +| Flag | Efecto | +|------|--------| +| `--with-db` | SQLite con schema `items` + bindings CRUD como ejemplo | + +## Post-setup + +```bash +# 1. Instalar deps del frontend (una vez) +cd apps/{nombre}/frontend && pnpm install && cd .. + +# 2. Desarrollo +wails dev # Ventana desktop con hot reload del frontend + +# 3. Produccion +wails build # binario en build/bin/{nombre} +./build/bin/{nombre} +``` + +## Requisitos + +El scaffold funciona sin Wails CLI, pero `wails dev`/`wails build` requiere: + +- **Wails CLI:** `go install github.com/wailsapp/wails/v2/cmd/wails@latest` +- **Deps del sistema (Linux):** GTK3 + WebKit2GTK — usa + `install_wails_bash_infra` para instalarlas: + ```bash + source bash/functions/infra/install_wails.sh && install_wails + ``` + +## Notas + +Pipeline impuro: genera archivos via heredocs, ejecuta `go mod tidy` al +final como verificacion. Si Wails CLI no esta disponible, reporta el warning +y continua — el scaffold es valido, solo `wails build` falla hasta instalar +el CLI. + +El frontend importa `@fn_library` via alias en vite.config.ts apuntando a +`../../../frontend/functions/ui/` (los componentes del registry sin +duplicarlos). + +Los bindings de Wails (funciones del struct App) se regeneran en +`frontend/wailsjs/go/main/App.ts` automaticamente cuando corres `wails dev` +o `wails build`. + +Abort si `apps/{nombre}/` ya existe. + +El tag `launcher` permite que aparezca en el Pipeline Launcher TUI. diff --git a/bash/functions/pipelines/init_desktop_app.sh b/bash/functions/pipelines/init_desktop_app.sh new file mode 100755 index 00000000..0a0b164d --- /dev/null +++ b/bash/functions/pipelines/init_desktop_app.sh @@ -0,0 +1,594 @@ +#!/usr/bin/env bash +# init_desktop_app +# ---------------- +# Scaffold de Wails desktop app: Go backend + React frontend con @mantine y +# @fn_library. Genera main.go (Wails con embed del frontend), app.go (struct +# App con bindings base), wails.json, go.mod y frontend/ con vite+react+mantine. +# +# USO: +# ./init_desktop_app.sh [--with-db] +# +# FLAGS: +# --with-db Anade store.go con SQLite + bindings CRUD de ejemplo +# +# EJEMPLO: +# ./init_desktop_app.sh my_tool +# ./init_desktop_app.sh data_explorer --with-db + +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_DB="false" +SKIP_WAILS_BUILD="${SKIP_WAILS_BUILD:-false}" + +while [ $# -gt 0 ]; do + case "$1" in + --with-db) WITH_DB="true"; shift ;; + --skip-wails-build) SKIP_WAILS_BUILD="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 [--with-db]" >&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 DESKTOP APP (Wails): ${NOMBRE}" +echo " Directorio: ${APP_DIR}" +echo " DB: ${WITH_DB}" +echo "════════════════════════════════════════════════════════════" +echo "" + +# ── 1. Verificar Go y Wails ────────────────────────────────── + +echo "[1/5] Verificando herramientas..." +assert_command_exists go +echo " Go: $(go version)" + +if command -v wails >/dev/null 2>&1; then + echo " Wails: $(wails version 2>/dev/null || echo detectado)" +else + echo " WARN: Wails CLI no detectado." + echo " Instalar: source ${REGISTRY_ROOT}/bash/functions/infra/install_wails.sh && install_wails" + echo " (Continuando con scaffold; \`wails build\` requiere el CLI.)" +fi + +# ── 2. Crear estructura + archivos Go ──────────────────────── + +echo "[2/5] Creando estructura y archivos Go..." +mkdir -p "$APP_DIR/frontend/src/pages" +mkdir -p "$APP_DIR/build/bin" + +# go.mod con replace a fn-registry +cat > "$APP_DIR/go.mod" < ${REGISTRY_ROOT} +EOF + +# main.go: entry point Wails con embed frontend/dist +cat > "$APP_DIR/main.go" <<'EOF' +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + app := NewApp() + + err := wails.Run(&options.App{ + Title: "__APP_NAME__", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +EOF +sed -i "s/__APP_NAME__/${NOMBRE}/g" "$APP_DIR/main.go" + +# app.go: struct App con bindings base +cat > "$APP_DIR/app.go" < "$APP_DIR/wails.json" < "$APP_DIR/store.go" <<'EOF' +package main + +import ( + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" +) + +// Item es un registro de ejemplo. +type Item struct { + ID int64 `json:"id"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` +} + +// openDB abre (o crea) la base de datos SQLite en dbPath e inicializa el schema. +func openDB(dbPath string) (*sql.DB, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("open db: %w", err) + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +)`); err != nil { + return nil, fmt.Errorf("init schema: %w", err) + } + return db, nil +} + +// ListItems devuelve todos los items. Binding accesible desde el frontend. +func (a *App) ListItems() ([]Item, error) { + rows, err := a.db.Query(`SELECT id, name, created_at FROM items ORDER BY id DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + + var items []Item + for rows.Next() { + var it Item + if err := rows.Scan(&it.ID, &it.Name, &it.CreatedAt); err != nil { + return nil, err + } + items = append(items, it) + } + return items, nil +} + +// CreateItem inserta un nuevo item. +func (a *App) CreateItem(name string) (Item, error) { + res, err := a.db.Exec(`INSERT INTO items (name) VALUES (?)`, name) + if err != nil { + return Item{}, err + } + id, _ := res.LastInsertId() + return Item{ID: id, Name: name}, nil +} +EOF + # Cuando hay DB, actualizamos app.go para incluir db field + cat > "$APP_DIR/app.go" < "$APP_DIR/go.mod" < ${REGISTRY_ROOT} +EOF +fi + +# ── 3. Frontend ────────────────────────────────────────────── + +echo "[3/5] Generando frontend..." + +cat > "$APP_DIR/frontend/package.json" < "$APP_DIR/frontend/vite.config.ts" <<'EOF' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +// Vite config para Wails: build → dist/, alias @fn_library a frontend/functions/ui +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@fn_library": path.resolve(__dirname, "../../../frontend/functions/ui"), + }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + }, +}); +EOF + +cat > "$APP_DIR/frontend/tsconfig.json" <<'EOF' +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@fn_library/*": ["../../../frontend/functions/ui/*"] + } + }, + "include": ["src"] +} +EOF + +cat > "$APP_DIR/frontend/index.html" < + + + + + ${NOMBRE} + + +
+ + + +EOF + +cat > "$APP_DIR/frontend/postcss.config.cjs" <<'EOF' +module.exports = { + plugins: { + "postcss-preset-mantine": {}, + }, +}; +EOF + +cat > "$APP_DIR/frontend/src/theme.ts" <<'EOF' +import { createTheme } from "@mantine/core"; +export const theme = createTheme({ + primaryColor: "blue", + defaultRadius: "md", +}); +EOF + +cat > "$APP_DIR/frontend/src/main.tsx" <<'EOF' +import React from "react"; +import ReactDOM from "react-dom/client"; +import { MantineProvider } from "@mantine/core"; +import "@mantine/core/styles.css"; + +import { theme } from "./theme"; +import { App } from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); +EOF + +cat > "$APP_DIR/frontend/src/App.tsx" < | null>(null); + + useEffect(() => { + GetVersion().then(setVersion).catch(() => setVersion(null)); + }, []); + + return ( + + ${NOMBRE} + Desktop app scaffoldeada por init_desktop_app. + + + + Version + + {version ? \`\${version.app} (\${version.goOS} \${version.goVer})\` : "cargando..."} + + + + + + + + {greeting && {greeting}} + + + + ); +} +EOF + +cat > "$APP_DIR/frontend/.gitignore" <<'EOF' +node_modules/ +dist/ +wailsjs/ +*.log +EOF + +# .gitignore raiz +cat > "$APP_DIR/.gitignore" < "$APP_DIR/app.md" </dev/null 2>&1; then + echo " wails detectado. Ejecutar manualmente 'wails build' en ${APP_DIR}" + echo " (el build real requiere pnpm install previo en frontend/)" +else + echo " wails no disponible — saltando build." + echo " El scaffold esta listo. Para builds: instalar Wails primero." +fi + +# Al menos verificar que go mod tidy funciona (que el main.go/app.go es Go valido) +( + cd "$APP_DIR" + if CGO_ENABLED=1 go mod tidy 2>&1 | tail -5; then + echo " go mod tidy OK" + else + echo " WARN: go mod tidy fallo — revisa main.go/app.go/go.mod" >&2 + fi +) + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " DESKTOP APP '${NOMBRE}' LISTA" +echo "════════════════════════════════════════════════════════════" +echo "" +echo " Pasos siguientes:" +echo " cd apps/${NOMBRE}/frontend && pnpm install && cd .." +echo " wails dev # modo desarrollo con hot reload" +echo " wails build # binario de produccion en build/bin/" +echo "" diff --git a/bash/functions/pipelines/init_web_app.md b/bash/functions/pipelines/init_web_app.md new file mode 100644 index 00000000..26a2420a --- /dev/null +++ b/bash/functions/pipelines/init_web_app.md @@ -0,0 +1,119 @@ +--- +name: init_web_app +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "init_web_app(nombre: string, [--port N], [--with-auth], [--with-db]) -> void" +description: "Scaffold de full-stack app: Go HTTP API backend + React frontend con Mantine y @fn_library. Extiende init_api_app anadiendo frontend/ con pnpm + vite + react + mantine. Genera vite.config.ts con proxy al backend y alias @fn_library, src/main.tsx con MantineProvider, src/App.tsx con AppShell, src/pages/Home.tsx con ejemplo consumiendo /api/v1/status." +tags: [init, scaffold, web, fullstack, frontend, pipeline, bash, launcher] +uses_functions: + - assert_command_exists_bash_shell + - http_serve_go_infra + - http_router_go_infra + - http_middleware_chain_go_infra + - http_logger_middleware_go_infra + - http_cors_middleware_go_infra + - http_json_response_go_infra + - http_error_response_go_infra + - migration_up_go_infra + - mantine_provider_ts_ui + - app_shell_ts_ui +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: nombre + desc: "nombre de la app a crear (apps/{nombre}/)" + - name: "--port" + desc: "puerto del backend HTTP (default 8080); el frontend usa 5173 con proxy" + - name: "--with-auth" + desc: "anade jwt_middleware + handlers login/register + tabla users al backend" + - name: "--with-db" + desc: "anade store.go con helpers CRUD y setup SQLite al backend" +output: "app full-stack en apps/{nombre}/ con backend Go (main.go) y frontend/ (vite + react + mantine). Dev: terminal 1 go run .; terminal 2 cd frontend && pnpm dev." +tested: false +tests: [] +test_file_path: "" +example: "fn run init_web_app my_dashboard --with-auth" +file_path: "bash/functions/pipelines/init_web_app.sh" +--- + +## Sinopsis + +```bash +fn run init_web_app [--port N] [--with-auth] [--with-db] +``` + +## Ejemplo rapido + +```bash +fn run init_web_app inventory_dashboard --with-auth +cd apps/inventory_dashboard +make install # pnpm install del frontend +# Terminal 1: +CGO_ENABLED=1 go run . +# Terminal 2: +cd frontend && pnpm dev +# → http://localhost:5173 (frontend) proxea a :8080 (backend) +``` + +## Archivos generados + +Todos los de `init_api_app`, mas: + +| Archivo | Descripcion | +|---------|-------------| +| `frontend/package.json` | pnpm, vite, react, @mantine/core, @tabler/icons-react | +| `frontend/vite.config.ts` | Proxy `/api` y `/health` al backend + alias `@fn_library` | +| `frontend/tsconfig.json` | TS strict con paths `@fn_library/*` | +| `frontend/index.html` | Entry HTML minimo | +| `frontend/postcss.config.cjs` | postcss-preset-mantine + breakpoints | +| `frontend/src/main.tsx` | Root con `MantineProvider` + theme | +| `frontend/src/theme.ts` | `createTheme()` con primaryColor | +| `frontend/src/App.tsx` | `AppShell` con Burger + Navbar + Header | +| `frontend/src/pages/Home.tsx` | Pagina ejemplo que consume `/api/v1/status` | +| `docker-compose.yml` | Services: api + frontend (node alpine) | +| `Makefile` | Targets `install`, `build-frontend`, `build`, `dev` | +| `app.md` | Framework: `net/http + vite + react + mantine` | + +## Flags + +| Flag | Efecto | +|------|--------| +| `--port N` | Puerto del backend Go (default: 8080) — frontend Vite siempre en 5173 | +| `--with-auth` | JWT + tabla users al backend | +| `--with-db` | Store + SQLite setup al backend | + +## Post-setup + +```bash +cd apps/{nombre} +cp .env.example .env +make install # pnpm install del frontend + +# Desarrollo (2 terminales): +CGO_ENABLED=1 go run . # Terminal 1: backend :PORT +cd frontend && pnpm dev # Terminal 2: frontend :5173 + +# Produccion: +make build # build frontend + binario Go +./ # sirve todo en :PORT +``` + +## Notas + +Pipeline impuro: invoca primero `init_api_app` para el backend y luego +escribe el frontend. Si pnpm esta disponible y `SKIP_PNPM_BUILD` no es +`true`, ejecuta `pnpm install && pnpm build` como verificacion final. + +El alias `@fn_library` en `vite.config.ts` apunta a +`../../../frontend/functions/ui` (relativo desde `apps/{nombre}/frontend/`). +Los componentes del registry se consumen sin duplicarlos. + +Si `apps/{nombre}/` ya existe, aborta sin sobrescribir. + +El tag `launcher` permite que aparezca en el Pipeline Launcher TUI. diff --git a/bash/functions/pipelines/init_web_app.sh b/bash/functions/pipelines/init_web_app.sh new file mode 100755 index 00000000..7709cfe1 --- /dev/null +++ b/bash/functions/pipelines/init_web_app.sh @@ -0,0 +1,466 @@ +#!/usr/bin/env bash +# init_web_app +# ------------ +# Scaffold de full-stack app: Go HTTP API backend + React frontend con +# Mantine y @fn_library. Extiende init_api_app anadiendo la capa frontend. +# +# Genera todo lo de init_api_app mas frontend/ con pnpm + vite + react + +# mantine, vite.config.ts con alias @fn_library y proxy al backend, +# src/main.tsx con FnMantineProvider, src/App.tsx, src/theme.ts y +# src/pages/Home.tsx de ejemplo. +# +# USO: +# ./init_web_app.sh [--port N] [--with-auth] [--with-db] +# +# EJEMPLO: +# ./init_web_app.sh my_dashboard +# ./init_web_app.sh my_dashboard --port 8080 --with-auth + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# Source funciones atomicas +source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh" + +# ── Parsing ────────────────────────────────────────────────── + +NOMBRE="" +PORT="8080" +WITH_AUTH="false" +WITH_DB="false" +SKIP_PNPM_BUILD="${SKIP_PNPM_BUILD:-false}" + +while [ $# -gt 0 ]; do + case "$1" in + --port) PORT="$2"; shift 2 ;; + --with-auth) WITH_AUTH="true"; shift ;; + --with-db) WITH_DB="true"; shift ;; + --skip-pnpm-build) SKIP_PNPM_BUILD="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 [--port N] [--with-auth] [--with-db]" >&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 WEB APP: ${NOMBRE}" +echo " Directorio: ${APP_DIR}" +echo " Puerto: ${PORT}" +echo " Auth: ${WITH_AUTH}" +echo " DB: ${WITH_DB}" +echo "════════════════════════════════════════════════════════════" +echo "" + +# ── 1. Invocar init_api_app para generar backend ───────────── + +echo "[1/3] Generando backend con init_api_app..." +BACKEND_FLAGS=() +BACKEND_FLAGS+=(--port "$PORT") +[ "$WITH_AUTH" = "true" ] && BACKEND_FLAGS+=(--with-auth) +[ "$WITH_DB" = "true" ] && BACKEND_FLAGS+=(--with-db) + +bash "$SCRIPT_DIR/init_api_app.sh" "$NOMBRE" "${BACKEND_FLAGS[@]}" + +# ── 2. Generar frontend ────────────────────────────────────── + +echo "" +echo "[2/3] Generando frontend..." +mkdir -p "$APP_DIR/frontend/src/pages" + +# package.json +cat > "$APP_DIR/frontend/package.json" < "$APP_DIR/frontend/vite.config.ts" < "$APP_DIR/frontend/tsconfig.json" <<'EOF' +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "allowImportingTsExtensions": false, + "baseUrl": ".", + "paths": { + "@fn_library/*": ["../../../frontend/functions/ui/*"] + } + }, + "include": ["src"] +} +EOF + +# index.html +cat > "$APP_DIR/frontend/index.html" < + + + + + ${NOMBRE} + + +
+ + + +EOF + +# postcss.config.cjs (Mantine usa postcss-preset-mantine) +cat > "$APP_DIR/frontend/postcss.config.cjs" <<'EOF' +module.exports = { + plugins: { + "postcss-preset-mantine": {}, + "postcss-simple-vars": { + variables: { + "mantine-breakpoint-xs": "36em", + "mantine-breakpoint-sm": "48em", + "mantine-breakpoint-md": "62em", + "mantine-breakpoint-lg": "75em", + "mantine-breakpoint-xl": "88em", + }, + }, + }, +}; +EOF + +# src/theme.ts +cat > "$APP_DIR/frontend/src/theme.ts" < "$APP_DIR/frontend/src/main.tsx" <<'EOF' +import React from "react"; +import ReactDOM from "react-dom/client"; +import { MantineProvider } from "@mantine/core"; +import "@mantine/core/styles.css"; + +import { theme } from "./theme"; +import { App } from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); +EOF + +# src/App.tsx +cat > "$APP_DIR/frontend/src/App.tsx" < + + + + ${NOMBRE} + + + + + + } active /> + } /> + + + + + + + + ); +} +EOF + +# src/pages/Home.tsx +cat > "$APP_DIR/frontend/src/pages/Home.tsx" <(null); + const [error, setError] = useState(null); + + useEffect(() => { + fetch("/api/v1/status") + .then((r) => r.json()) + .then(setStatus) + .catch((e) => setError(String(e))); + }, []); + + return ( + + ${NOMBRE} + Full-stack app scaffoldeada por init_web_app. + + + + API status + {status ? ( + {status.app} v{status.version} + ) : error ? ( + {error} + ) : ( + loading... + )} + + + + ); +} +EOF + +# .gitignore del frontend +cat > "$APP_DIR/frontend/.gitignore" <<'EOF' +node_modules/ +dist/ +*.log +.DS_Store +EOF + +# docker-compose.yml +cat > "$APP_DIR/docker-compose.yml" < "$APP_DIR/Makefile" < "$APP_DIR/app.md" </dev/null 2>&1; then + echo " pnpm detectado, ejecutando install + build..." + ( + cd "$APP_DIR/frontend" + if pnpm install --silent 2>&1 | tail -5; then + : + fi + if pnpm build 2>&1 | tail -5; then + echo " frontend build OK" + else + echo " WARN: frontend build fallo (revisa el output arriba)" >&2 + fi + ) +else + echo " pnpm no disponible — saltando verificacion frontend" + echo " Cuando este disponible: cd ${APP_DIR}/frontend && pnpm install && pnpm build" +fi + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " WEB APP '${NOMBRE}' LISTA" +echo "════════════════════════════════════════════════════════════" +echo "" +echo " Pasos siguientes:" +echo " cd apps/${NOMBRE}" +echo " cp .env.example .env" +echo " make install # pnpm install del frontend" +echo "" +echo " Desarrollo (2 terminales):" +echo " Terminal 1: cd apps/${NOMBRE} && CGO_ENABLED=1 go run ." +echo " Terminal 2: cd apps/${NOMBRE}/frontend && pnpm dev" +echo "" +echo " Abrir http://localhost:5173" +echo "" diff --git a/dev/issues/0022-init-pipelines.md b/dev/issues/completed/0022-init-pipelines.md similarity index 100% rename from dev/issues/0022-init-pipelines.md rename to dev/issues/completed/0022-init-pipelines.md diff --git a/docs/init-pipelines.md b/docs/init-pipelines.md new file mode 100644 index 00000000..7c97b0fd --- /dev/null +++ b/docs/init-pipelines.md @@ -0,0 +1,199 @@ +# Init Pipelines + +Cuatro pipelines bash que scaffold apps completas en `apps/` con un solo comando. Mismo patron que `init_jupyter_analysis` — componen funciones atomicas del registry para producir entornos listos para trabajar. + +Todos son `kind: pipeline`, `purity: impure`, `lang: bash`, `domain: pipelines`. Llevan el tag `launcher` y aparecen en el Pipeline Launcher TUI. + +## Resumen + +| Pipeline | Para que | Flags | +|----------|----------|-------| +| `init_api_app` | Go HTTP API service con graceful shutdown, middleware chain, migrations | `--port N`, `--with-auth`, `--with-db`, `--with-ops` | +| `init_web_app` | Full-stack: Go API + React frontend con Mantine y `@fn_library` | `--port N`, `--with-auth`, `--with-db` | +| `init_desktop_app` | Wails desktop app: Go backend + React frontend con Mantine | `--with-db` | +| `init_cli_app` | Go CLI con subcomandos, opcionalmente con TUI Bubbletea fullscreen | `--with-tui` | + +## Arbol de decision + +``` +¿Que tipo de app necesitas? +│ +├─ Un servicio HTTP/API +│ │ +│ └─ ¿Necesitas frontend? +│ ├─ NO → init_api_app +│ └─ SI → init_web_app +│ +├─ Una app de escritorio (ventana nativa) +│ │ +│ └─ init_desktop_app +│ +└─ Una herramienta de linea de comando + │ + └─ ¿Quieres TUI interactiva? + ├─ NO → init_cli_app + └─ SI → init_cli_app --with-tui +``` + +## Combinaciones comunes + +### API service con auth y DB + +```bash +fn run init_api_app billing_api --port 8090 --with-auth --with-db +cd apps/billing_api +cp .env.example .env +make run +# → curl localhost:8090/health +``` + +### Dashboard web full-stack + +```bash +fn run init_web_app inventory_dashboard --with-auth +cd apps/inventory_dashboard +make install +# 2 terminales: +CGO_ENABLED=1 go run . # backend :8080 +cd frontend && pnpm dev # frontend :5173 con proxy +``` + +### Desktop app con SQLite + +```bash +fn run init_desktop_app data_explorer --with-db +cd apps/data_explorer/frontend && pnpm install && cd .. +wails dev +``` + +### CLI con TUI + +```bash +fn run init_cli_app deploy_helper --with-tui +cd apps/deploy_helper +make build +./deploy_helper # arranca TUI fullscreen +./deploy_helper status # subcomando CLI normal +``` + +## Filosofia + +1. **Composicion sobre monolito:** cada pipeline sourcea funciones atomicas del registry (`assert_command_exists`, etc.) — si una mejora, todos los pipelines se benefician. +2. **Verificacion al final:** cada pipeline termina con `go vet`, `pnpm build` o `go mod tidy`. Si falla, el pipeline reporta antes de declarar exito. +3. **Defaults sensatos:** el caso base (sin flags) genera una app funcional minima. Las flags anaden capas incrementales. +4. **`@fn_library` como alias, no copia:** los frontends generados referencian `@fn_library` via alias en `vite.config.ts` apuntando a `frontend/functions/ui/` del registry. +5. **`app.md` generado automaticamente:** el frontmatter incluye `uses_functions` con los IDs reales que el boilerplate importa. +6. **Abort si existe:** si `apps/{nombre}/` ya existe, el pipeline aborta sin sobrescribir. + +## Estructura generada por tipo + +### `init_api_app` +``` +apps/{nombre}/ +├── main.go HTTPServe + router + middleware + graceful shutdown +├── handlers.go healthHandler, statusHandler con HTTPJSONResponse +├── config.go LoadConfig desde env vars +├── migrations/001_initial.sql +├── Makefile build/run/dev/test/vet/clean +├── .env.example +├── .gitignore +├── go.mod replace fn-registry → registry root +├── app.md +├── auth.go (con --with-auth) login/register con JWT +├── migrations/002_users.sql (con --with-auth) +└── store.go (con --with-db) struct Store con Ping +``` + +### `init_web_app` +Todo lo de `init_api_app` mas: +``` +apps/{nombre}/ +├── docker-compose.yml +└── frontend/ + ├── package.json pnpm + vite + react + @mantine/core + ├── vite.config.ts alias @fn_library + proxy /api a backend + ├── tsconfig.json + ├── index.html + ├── postcss.config.cjs + └── src/ + ├── main.tsx MantineProvider + App + ├── App.tsx AppShell con Burger + Navbar + ├── theme.ts createTheme() + └── pages/Home.tsx Consume /api/v1/status +``` + +### `init_desktop_app` +``` +apps/{nombre}/ +├── main.go Wails v2 con embed frontend/dist +├── app.go struct App, bindings Greet, GetVersion +├── wails.json +├── go.mod wails/v2 + replace fn-registry +├── app.md framework: wails+vite+react+mantine +├── .gitignore +├── store.go (con --with-db) Item + ListItems + CreateItem +└── frontend/ package.json, vite.config.ts, src/ con Mantine +``` + +### `init_cli_app` +``` +apps/{nombre}/ +├── main.go switch os.Args[1] — subcommand routing +├── cmd_version.go +├── cmd_status.go +├── Makefile build/run (ARGS=...) /install/test/vet/clean +├── go.mod +├── app.md framework vacio (CLI puro) o 'bubbletea' (TUI) +└── model.go (con --with-tui) Modelo Bubbletea con list+spinner+dark theme +``` + +## FAQ + +### ¿Como anado auth despues? + +Manualmente: anadir `auth.go` con los handlers, `migrations/002_users.sql`, y tag `auth` a `app.md`. Las funciones `jwt_generate_go_infra`, `password_hash_go_infra`, `password_verify_go_infra` estan en el registry. + +Alternativa: regenerar con `--with-auth` en otro directorio y copiar manualmente los archivos relevantes. + +### ¿Como cambio el puerto despues? + +Editar `config.go` (cambiar default de `Port`) o pasar `PORT=8090` via env var. El cliente HTTP del frontend (`init_web_app`) tiene el proxy en `vite.config.ts` — hay que ajustarlo alli tambien. + +### ¿Como anado operations.db? + +```bash +cd apps/{nombre} +FN_REGISTRY_ROOT=$(pwd)/../.. fn ops init . +``` + +Esto crea `operations.db` con schema completo. Para escribir entities/relations/executions usar `fn ops entity add`, etc. + +### ¿Como agrego mas paginas al frontend? + +Crear `frontend/src/pages/Nueva.tsx` y anadirla como ruta en `App.tsx`. Si quieres react-router, ya esta en el package.json de `init_web_app`. + +### ¿Como desactivo las verificaciones al final del pipeline? + +Las verificaciones de build del frontend (`pnpm build`) o Wails (`wails build`) se saltan con env vars: + +```bash +SKIP_PNPM_BUILD=true fn run init_web_app my_app +SKIP_WAILS_BUILD=true fn run init_desktop_app my_app +``` + +`go vet` del backend no tiene opt-out — si falla, hay algo que corregir en los heredocs del pipeline. + +### ¿Que pasa si fn-registry cambia el path? + +Los pipelines embeben el path absoluto del registry en la directiva `replace fn-registry => /abs/path` del `go.mod` generado. Si el registry se mueve de directorio, hay que actualizar esa linea en cada app scaffoldeada. + +### ¿Puedo scaffoldear en `projects/{proyecto}/apps/` en vez de `apps/`? + +De momento los 4 pipelines generan en `apps/{nombre}/`. Para un proyecto existe `init_jupyter_analysis --project {proyecto}` que crea en `projects/{proyecto}/analysis/`. Una mejora futura seria anadir `--project` a `init_api_app`/`init_web_app`/etc. Por ahora: generar en `apps/` y mover manualmente (cuidado con los paths en `replace` y `app.md`). + +## Ver tambien + +- `init_jupyter_analysis_bash_pipelines` — pipeline de referencia (scaffold analisis Jupyter) +- `init_go_project_bash_pipelines` / `init_go_module_bash_pipelines` — pipelines Go genericos (no apps) +- `gitea_init_app_bash_pipelines` — inicializar repo Gitea para una app scaffoldeada +- `dev/issues/completed/0022-init-pipelines.md` — spec original