merge: issue/0022-init-pipelines — scaffolding pipelines bash (api, web, desktop, cli)
This commit is contained in:
@@ -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 <nombre> [--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.
|
||||||
Executable
+603
@@ -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 <nombre> [--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 <nombre> [--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" <<EOF
|
||||||
|
module ${NOMBRE}
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
fn-registry v0.0.0-00010101000000-000000000000
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.37
|
||||||
|
)
|
||||||
|
|
||||||
|
replace fn-registry => ${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" <<EOF
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
${MAIN_IMPORTS}
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := LoadConfig()
|
||||||
|
|
||||||
|
// DB
|
||||||
|
db, err := sql.Open("sqlite3", cfg.DBPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
${MAIN_MIGRATION}
|
||||||
|
// Routes
|
||||||
|
routes := []infra.Route{
|
||||||
|
{Method: "GET", Path: "/health", Handler: http.HandlerFunc(healthHandler)},
|
||||||
|
{Method: "GET", Path: "/api/v1/status", Handler: http.HandlerFunc(statusHandler)},
|
||||||
|
}
|
||||||
|
mux := infra.HTTPRouter(routes)
|
||||||
|
|
||||||
|
// Middleware chain (outer → inner)
|
||||||
|
chain := infra.HTTPMiddlewareChain(
|
||||||
|
infra.HTTPLoggerMiddleware(os.Stdout),
|
||||||
|
infra.HTTPCORSMiddleware(cfg.CORSOrigins, []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
|
||||||
|
)
|
||||||
|
handler := chain(mux)
|
||||||
|
|
||||||
|
// Graceful shutdown via context
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
addr := ":" + cfg.Port
|
||||||
|
log.Printf("starting %s on %s", cfg.AppName, addr)
|
||||||
|
if err := infra.HTTPServe(addr, handler, ctx); err != nil {
|
||||||
|
log.Fatalf("http serve: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("server stopped")
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# main.go necesita import net/http tambien
|
||||||
|
# rehacerlo con el import bien
|
||||||
|
cat > "$APP_DIR/main.go" <<EOF
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
|
"fn-registry/functions/infra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := LoadConfig()
|
||||||
|
|
||||||
|
// DB
|
||||||
|
db, err := sql.Open("sqlite3", cfg.DBPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
${MAIN_MIGRATION}
|
||||||
|
// Routes
|
||||||
|
routes := []infra.Route{
|
||||||
|
{Method: "GET", Path: "/health", Handler: http.HandlerFunc(healthHandler)},
|
||||||
|
{Method: "GET", Path: "/api/v1/status", Handler: http.HandlerFunc(statusHandler)},
|
||||||
|
}
|
||||||
|
mux := infra.HTTPRouter(routes)
|
||||||
|
|
||||||
|
// Middleware chain
|
||||||
|
chain := infra.HTTPMiddlewareChain(
|
||||||
|
infra.HTTPLoggerMiddleware(os.Stdout),
|
||||||
|
infra.HTTPCORSMiddleware(cfg.CORSOrigins, []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
|
||||||
|
)
|
||||||
|
handler := chain(mux)
|
||||||
|
|
||||||
|
// Graceful shutdown via signal-aware context
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
addr := ":" + cfg.Port
|
||||||
|
log.Printf("starting %s on %s", cfg.AppName, addr)
|
||||||
|
if err := infra.HTTPServe(addr, handler, ctx); err != nil {
|
||||||
|
log.Fatalf("http serve: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("server stopped")
|
||||||
|
_ = db
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Auth opcional ---
|
||||||
|
if [ "$WITH_AUTH" = "true" ]; then
|
||||||
|
cat > "$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" <<EOF
|
||||||
|
-- 001_initial — schema inicial para ${NOMBRE}
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- DOWN
|
||||||
|
DROP TABLE IF EXISTS items;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$WITH_AUTH" = "true" ]; then
|
||||||
|
cat > "$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" <<EOF
|
||||||
|
.PHONY: build run test vet clean dev
|
||||||
|
|
||||||
|
BIN=${NOMBRE}
|
||||||
|
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=1 go build -tags fts5 -o \$(BIN) .
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./\$(BIN)
|
||||||
|
|
||||||
|
dev:
|
||||||
|
CGO_ENABLED=1 go run .
|
||||||
|
|
||||||
|
test:
|
||||||
|
CGO_ENABLED=1 go test -tags fts5 -v ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
CGO_ENABLED=1 go vet -tags fts5 ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f \$(BIN) *.db *.db-shm *.db-wal
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$APP_DIR/.env.example" <<EOF
|
||||||
|
# Configuracion de ${NOMBRE}
|
||||||
|
APP_NAME=${NOMBRE}
|
||||||
|
PORT=${PORT}
|
||||||
|
DB_PATH=${NOMBRE}.db
|
||||||
|
CORS_ORIGINS=*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$WITH_AUTH" = "true" ]; then
|
||||||
|
cat >> "$APP_DIR/.env.example" <<EOF
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
JWT_SECRET=change-me-in-production
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$APP_DIR/.gitignore" <<EOF
|
||||||
|
# Binario
|
||||||
|
${NOMBRE}
|
||||||
|
|
||||||
|
# SQLite
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# app.md con frontmatter
|
||||||
|
USES_FUNCTIONS=' - http_serve_go_infra
|
||||||
|
- http_router_go_infra
|
||||||
|
- http_middleware_chain_go_infra
|
||||||
|
- http_cors_middleware_go_infra
|
||||||
|
- http_logger_middleware_go_infra
|
||||||
|
- http_json_response_go_infra
|
||||||
|
- http_error_response_go_infra
|
||||||
|
- migration_up_go_infra'
|
||||||
|
|
||||||
|
if [ "$WITH_AUTH" = "true" ]; then
|
||||||
|
USES_FUNCTIONS="${USES_FUNCTIONS}
|
||||||
|
- jwt_generate_go_infra
|
||||||
|
- password_hash_go_infra
|
||||||
|
- password_verify_go_infra"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$APP_DIR/app.md" <<EOF
|
||||||
|
---
|
||||||
|
name: ${NOMBRE}
|
||||||
|
lang: go
|
||||||
|
domain: tools
|
||||||
|
description: "API HTTP generada por init_api_app."
|
||||||
|
tags: [service]
|
||||||
|
uses_functions:
|
||||||
|
${USES_FUNCTIONS}
|
||||||
|
uses_types:
|
||||||
|
- Route_go_infra
|
||||||
|
- Middleware_go_infra
|
||||||
|
- HTTPError_go_infra
|
||||||
|
framework: "net/http"
|
||||||
|
entry_point: "main.go"
|
||||||
|
dir_path: "apps/${NOMBRE}"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
App scaffoldeada por \`init_api_app\`. Puerto ${PORT}. Health check en \`/health\`.
|
||||||
|
|
||||||
|
Ejecutar: \`make run\` o \`make dev\` para hot reload.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── 7. Verificar con go vet ──────────────────────────────────
|
||||||
|
|
||||||
|
echo "[7/7] Verificando con go vet..."
|
||||||
|
(
|
||||||
|
cd "$APP_DIR"
|
||||||
|
if go mod tidy 2>&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 ""
|
||||||
@@ -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 <nombre> [--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).
|
||||||
Executable
+460
@@ -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 <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 ""
|
||||||
@@ -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 <nombre> [--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.
|
||||||
Executable
+594
@@ -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 <nombre> [--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 <nombre> [--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" <<EOF
|
||||||
|
module ${NOMBRE}
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/wailsapp/wails/v2 v2.9.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
fn-registry v0.0.0-00010101000000-000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
replace fn-registry => ${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" <<EOF
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App struct — contexto de la app Wails y bindings al frontend.
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp crea una nueva App.
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startup se llama cuando la app arranca. Persistimos el ctx para usarlo en
|
||||||
|
// llamadas a runtime.EventsEmit, etc.
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greet retorna un saludo. Accesible desde el frontend como wails.Greet(name).
|
||||||
|
func (a *App) Greet(name string) string {
|
||||||
|
return fmt.Sprintf("Hola, %s!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersion retorna la version de la app y el Go runtime.
|
||||||
|
func (a *App) GetVersion() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"app": "0.1.0",
|
||||||
|
"name": "${NOMBRE}",
|
||||||
|
"goVer": runtime.Version(),
|
||||||
|
"goOS": runtime.GOOS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# wails.json
|
||||||
|
cat > "$APP_DIR/wails.json" <<EOF
|
||||||
|
{
|
||||||
|
"\$schema": "https://wails.io/schemas/config.v2.json",
|
||||||
|
"name": "${NOMBRE}",
|
||||||
|
"outputfilename": "${NOMBRE}",
|
||||||
|
"frontend:install": "pnpm install",
|
||||||
|
"frontend:build": "pnpm build",
|
||||||
|
"frontend:dev:watcher": "pnpm dev",
|
||||||
|
"frontend:dev:serverUrl": "auto",
|
||||||
|
"author": {
|
||||||
|
"name": "",
|
||||||
|
"email": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Store opcional
|
||||||
|
if [ "$WITH_DB" = "true" ]; then
|
||||||
|
cat > "$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" <<EOF
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App struct — contexto de la app Wails, DB y bindings al frontend.
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp crea una nueva App con la DB abierta.
|
||||||
|
func NewApp() *App {
|
||||||
|
db, err := openDB("${NOMBRE}.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
return &App{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Greet(name string) string {
|
||||||
|
return fmt.Sprintf("Hola, %s!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetVersion() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"app": "0.1.0",
|
||||||
|
"name": "${NOMBRE}",
|
||||||
|
"goVer": runtime.Version(),
|
||||||
|
"goOS": runtime.GOOS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
# Actualizar go.mod para incluir mattn/go-sqlite3
|
||||||
|
cat > "$APP_DIR/go.mod" <<EOF
|
||||||
|
module ${NOMBRE}
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/wailsapp/wails/v2 v2.9.2
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.37
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
fn-registry v0.0.0-00010101000000-000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
replace fn-registry => ${REGISTRY_ROOT}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Frontend ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo "[3/5] Generando frontend..."
|
||||||
|
|
||||||
|
cat > "$APP_DIR/frontend/package.json" <<EOF
|
||||||
|
{
|
||||||
|
"name": "${NOMBRE}-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^8.0.0",
|
||||||
|
"@mantine/hooks": "^8.0.0",
|
||||||
|
"@tabler/icons-react": "^3.30.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"typescript": "~5.7.0",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$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" <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${NOMBRE}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
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(
|
||||||
|
<React.StrictMode>
|
||||||
|
<MantineProvider theme={theme} defaultColorScheme="dark">
|
||||||
|
<App />
|
||||||
|
</MantineProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$APP_DIR/frontend/src/App.tsx" <<EOF
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Stack, Title, Text, Paper, Button, Group, Badge } from "@mantine/core";
|
||||||
|
|
||||||
|
// @ts-expect-error Wails genera este modulo al hacer build
|
||||||
|
import { Greet, GetVersion } from "../wailsjs/go/main/App";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [greeting, setGreeting] = useState("");
|
||||||
|
const [version, setVersion] = useState<Record<string, string> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
GetVersion().then(setVersion).catch(() => setVersion(null));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="xl">
|
||||||
|
<Title order={2}>${NOMBRE}</Title>
|
||||||
|
<Text c="dimmed">Desktop app scaffoldeada por init_desktop_app.</Text>
|
||||||
|
|
||||||
|
<Paper p="md" withBorder>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500}>Version</Text>
|
||||||
|
<Badge color="blue">
|
||||||
|
{version ? \`\${version.app} (\${version.goOS} \${version.goVer})\` : "cargando..."}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper p="md" withBorder>
|
||||||
|
<Stack>
|
||||||
|
<Button onClick={() => Greet("desktop").then(setGreeting)}>
|
||||||
|
Greet from Go
|
||||||
|
</Button>
|
||||||
|
{greeting && <Text>{greeting}</Text>}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$APP_DIR/frontend/.gitignore" <<'EOF'
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
wailsjs/
|
||||||
|
*.log
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# .gitignore raiz
|
||||||
|
cat > "$APP_DIR/.gitignore" <<EOF
|
||||||
|
# Wails build output
|
||||||
|
build/bin/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/wailsjs/
|
||||||
|
frontend/node_modules/
|
||||||
|
|
||||||
|
# SQLite
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── 4. app.md ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo "[4/5] Escribiendo app.md..."
|
||||||
|
|
||||||
|
USES_FUNCTIONS=' - scaffold_wails_app_go_infra
|
||||||
|
- install_wails_bash_infra'
|
||||||
|
|
||||||
|
cat > "$APP_DIR/app.md" <<EOF
|
||||||
|
---
|
||||||
|
name: ${NOMBRE}
|
||||||
|
lang: go
|
||||||
|
domain: tools
|
||||||
|
description: "Desktop app (Wails + React + Mantine) generada por init_desktop_app."
|
||||||
|
tags: [desktop, wails, frontend]
|
||||||
|
uses_functions:
|
||||||
|
${USES_FUNCTIONS}
|
||||||
|
uses_types: []
|
||||||
|
framework: "wails + vite + react + mantine"
|
||||||
|
entry_point: "main.go"
|
||||||
|
dir_path: "apps/${NOMBRE}"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Desktop app con backend Go (Wails v2) y frontend React. Bindings en
|
||||||
|
\`app.go\` son accesibles desde el frontend via \`wails dev\`/\`wails build\`
|
||||||
|
que genera \`frontend/wailsjs/go/main/App.ts\` automaticamente.
|
||||||
|
|
||||||
|
Desarrollo:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
cd apps/${NOMBRE}
|
||||||
|
cd frontend && pnpm install && cd ..
|
||||||
|
wails dev
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
wails build
|
||||||
|
./build/bin/${NOMBRE}
|
||||||
|
\`\`\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── 5. Verificacion ──────────────────────────────────────────
|
||||||
|
|
||||||
|
echo "[5/5] Verificacion..."
|
||||||
|
|
||||||
|
if [ "$SKIP_WAILS_BUILD" = "true" ]; then
|
||||||
|
echo " SKIP_WAILS_BUILD=true — saltando wails build"
|
||||||
|
elif command -v wails >/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 ""
|
||||||
@@ -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 <nombre> [--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
|
||||||
|
./<nombre> # 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.
|
||||||
Executable
+466
@@ -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 <nombre> [--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 <nombre> [--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" <<EOF
|
||||||
|
{
|
||||||
|
"name": "${NOMBRE}-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^8.0.0",
|
||||||
|
"@mantine/hooks": "^8.0.0",
|
||||||
|
"@mantine/notifications": "^8.0.0",
|
||||||
|
"@tabler/icons-react": "^3.30.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"typescript": "~5.7.0",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# vite.config.ts
|
||||||
|
cat > "$APP_DIR/frontend/vite.config.ts" <<EOF
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// Vite config: proxy API al backend Go + alias @fn_library a frontend/functions/ui del registry
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@fn_library": path.resolve(__dirname, "../../../frontend/functions/ui"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:${PORT}",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
"/health": {
|
||||||
|
target: "http://localhost:${PORT}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# tsconfig.json
|
||||||
|
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,
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@fn_library/*": ["../../../frontend/functions/ui/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# index.html
|
||||||
|
cat > "$APP_DIR/frontend/index.html" <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${NOMBRE}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
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" <<EOF
|
||||||
|
import { createTheme } from "@mantine/core";
|
||||||
|
|
||||||
|
// Tema Mantine de ${NOMBRE}. Mantine genera sus propias CSS variables.
|
||||||
|
export const theme = createTheme({
|
||||||
|
primaryColor: "blue",
|
||||||
|
defaultRadius: "md",
|
||||||
|
});
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# src/main.tsx
|
||||||
|
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(
|
||||||
|
<React.StrictMode>
|
||||||
|
<MantineProvider theme={theme} defaultColorScheme="auto">
|
||||||
|
<App />
|
||||||
|
</MantineProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# src/App.tsx
|
||||||
|
cat > "$APP_DIR/frontend/src/App.tsx" <<EOF
|
||||||
|
import { AppShell, Title, Burger, Group, NavLink, Stack } from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { IconHome, IconSettings } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { Home } from "./pages/Home";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [opened, { toggle }] = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 56 }}
|
||||||
|
navbar={{ width: 240, breakpoint: "sm", collapsed: { mobile: !opened } }}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||||
|
<Title order={4}>${NOMBRE}</Title>
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar p="sm">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<NavLink label="Home" leftSection={<IconHome size={16} />} active />
|
||||||
|
<NavLink label="Settings" leftSection={<IconSettings size={16} />} />
|
||||||
|
</Stack>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main>
|
||||||
|
<Home />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# src/pages/Home.tsx
|
||||||
|
cat > "$APP_DIR/frontend/src/pages/Home.tsx" <<EOF
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Stack, Title, Text, Paper, Group, Badge } from "@mantine/core";
|
||||||
|
|
||||||
|
type StatusResponse = {
|
||||||
|
app: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
const [status, setStatus] = useState<StatusResponse | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/v1/status")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then(setStatus)
|
||||||
|
.catch((e) => setError(String(e)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={2}>${NOMBRE}</Title>
|
||||||
|
<Text c="dimmed">Full-stack app scaffoldeada por init_web_app.</Text>
|
||||||
|
|
||||||
|
<Paper p="md" withBorder>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500}>API status</Text>
|
||||||
|
{status ? (
|
||||||
|
<Badge color="green">{status.app} v{status.version}</Badge>
|
||||||
|
) : error ? (
|
||||||
|
<Badge color="red">{error}</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge color="gray">loading...</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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" <<EOF
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "${PORT}:${PORT}"
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./migrations:/app/migrations
|
||||||
|
frontend:
|
||||||
|
image: node:20-alpine
|
||||||
|
working_dir: /app
|
||||||
|
command: sh -c "corepack enable && pnpm install && pnpm dev --host"
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Actualizar Makefile con targets frontend + dev
|
||||||
|
cat > "$APP_DIR/Makefile" <<EOF
|
||||||
|
.PHONY: build build-frontend run dev test vet clean install
|
||||||
|
|
||||||
|
BIN=${NOMBRE}
|
||||||
|
|
||||||
|
install:
|
||||||
|
cd frontend && pnpm install
|
||||||
|
|
||||||
|
build-frontend:
|
||||||
|
cd frontend && pnpm build
|
||||||
|
|
||||||
|
build: build-frontend
|
||||||
|
CGO_ENABLED=1 go build -tags fts5 -o \$(BIN) .
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./\$(BIN)
|
||||||
|
|
||||||
|
dev:
|
||||||
|
@echo "Arranca el backend (API en :${PORT}):"
|
||||||
|
@echo " CGO_ENABLED=1 go run ."
|
||||||
|
@echo "Arranca el frontend (Vite en :5173 con proxy):"
|
||||||
|
@echo " cd frontend && pnpm dev"
|
||||||
|
|
||||||
|
test:
|
||||||
|
CGO_ENABLED=1 go test -tags fts5 -v ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
CGO_ENABLED=1 go vet -tags fts5 ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f \$(BIN) *.db *.db-shm *.db-wal
|
||||||
|
rm -rf frontend/dist frontend/node_modules
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Actualizar app.md con framework + uses frontend
|
||||||
|
cat > "$APP_DIR/app.md" <<EOF
|
||||||
|
---
|
||||||
|
name: ${NOMBRE}
|
||||||
|
lang: go
|
||||||
|
domain: tools
|
||||||
|
description: "Full-stack app (Go API + React/Mantine frontend) generada por init_web_app."
|
||||||
|
tags: [service, web, frontend]
|
||||||
|
uses_functions:
|
||||||
|
- http_serve_go_infra
|
||||||
|
- http_router_go_infra
|
||||||
|
- http_middleware_chain_go_infra
|
||||||
|
- http_cors_middleware_go_infra
|
||||||
|
- http_logger_middleware_go_infra
|
||||||
|
- http_json_response_go_infra
|
||||||
|
- http_error_response_go_infra
|
||||||
|
- migration_up_go_infra
|
||||||
|
uses_types:
|
||||||
|
- Route_go_infra
|
||||||
|
- Middleware_go_infra
|
||||||
|
- HTTPError_go_infra
|
||||||
|
framework: "net/http + vite + react + mantine"
|
||||||
|
entry_point: "main.go"
|
||||||
|
dir_path: "apps/${NOMBRE}"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
App full-stack: backend en Go (\`main.go\`) + frontend en \`frontend/\` con
|
||||||
|
Vite + React + Mantine. El vite.config.ts hace proxy de \`/api\` y \`/health\`
|
||||||
|
al backend en :${PORT}, y usa alias \`@fn_library\` hacia \`frontend/functions/ui\`
|
||||||
|
del registry.
|
||||||
|
|
||||||
|
Dev: \`make install && make dev\` (dos terminales: backend con \`go run .\`,
|
||||||
|
frontend con \`cd frontend && pnpm dev\`).
|
||||||
|
|
||||||
|
Prod: \`make build\` genera \`frontend/dist\` + binario; el binario sirve los
|
||||||
|
static files del build embebido (pendiente embed en main.go).
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── 3. Verificar frontend si pnpm disponible ─────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[3/3] Verificacion..."
|
||||||
|
|
||||||
|
if [ "$SKIP_PNPM_BUILD" = "true" ]; then
|
||||||
|
echo " SKIP_PNPM_BUILD=true — saltando pnpm install/build"
|
||||||
|
elif command -v pnpm >/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 ""
|
||||||
@@ -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
|
||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user