diff --git a/bash/functions/pipelines/init_desktop_app.md b/bash/functions/pipelines/init_desktop_app.md new file mode 100644 index 00000000..2987da27 --- /dev/null +++ b/bash/functions/pipelines/init_desktop_app.md @@ -0,0 +1,118 @@ +--- +name: init_desktop_app +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "init_desktop_app(nombre: string, [--with-db]) -> void" +description: "Scaffold de Wails desktop app: Go backend + React frontend con Mantine y @fn_library. Genera main.go (Wails con embed frontend), app.go (bindings Greet/GetVersion), wails.json, go.mod con replace a fn-registry y frontend/ con vite + react + mantine." +tags: [init, scaffold, desktop, wails, pipeline, bash, launcher] +uses_functions: + - assert_command_exists_bash_shell + - scaffold_wails_app_go_infra + - install_wails_bash_infra + - wails_bind_crud_go_infra + - wails_build_go_infra +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: nombre + desc: "nombre de la app Wails (apps/{nombre}/)" + - name: "--with-db" + desc: "anade store.go con SQLite + bindings CRUD (Item, ListItems, CreateItem)" +output: "app Wails en apps/{nombre}/ con main.go + app.go + wails.json + frontend/ Vite+React. Ejecutar 'wails dev' o 'wails build' dentro del directorio." +tested: false +tests: [] +test_file_path: "" +example: "fn run init_desktop_app my_tool" +file_path: "bash/functions/pipelines/init_desktop_app.sh" +--- + +## Sinopsis + +```bash +fn run init_desktop_app [--with-db] +``` + +## Ejemplo rapido + +```bash +fn run init_desktop_app data_explorer --with-db +cd apps/data_explorer/frontend && pnpm install && cd .. +wails dev # dev con hot reload +# o bien: +wails build && ./build/bin/data_explorer +``` + +## Archivos generados + +| Archivo | Descripcion | +|---------|-------------| +| `main.go` | Entry Wails con embed de frontend/dist, options.App, Bind: []interface{}{app} | +| `app.go` | Struct App con bindings `Greet(name)`, `GetVersion()` (y con `--with-db`: `ListItems`, `CreateItem`) | +| `wails.json` | Config Wails: name, outputfilename, scripts frontend (pnpm) | +| `go.mod` | Modulo Go con `github.com/wailsapp/wails/v2` + replace `fn-registry` | +| `frontend/package.json` | pnpm + vite + react + @mantine/core + @tabler/icons-react | +| `frontend/vite.config.ts` | Alias `@fn_library`, outDir `dist` para embed | +| `frontend/tsconfig.json` | TS strict con paths `@fn_library/*` | +| `frontend/src/main.tsx` | Root con `MantineProvider` (defaultColorScheme: dark) | +| `frontend/src/App.tsx` | Componente que llama bindings `Greet` y `GetVersion` via wailsjs | +| `frontend/postcss.config.cjs` | postcss-preset-mantine | +| `app.md` | Framework: `wails + vite + react + mantine` | + +Con `--with-db` anade ademas: +- `store.go` — `openDB`, tipo `Item`, bindings CRUD `ListItems`, `CreateItem` + +## Flags + +| Flag | Efecto | +|------|--------| +| `--with-db` | SQLite con schema `items` + bindings CRUD como ejemplo | + +## Post-setup + +```bash +# 1. Instalar deps del frontend (una vez) +cd apps/{nombre}/frontend && pnpm install && cd .. + +# 2. Desarrollo +wails dev # Ventana desktop con hot reload del frontend + +# 3. Produccion +wails build # binario en build/bin/{nombre} +./build/bin/{nombre} +``` + +## Requisitos + +El scaffold funciona sin Wails CLI, pero `wails dev`/`wails build` requiere: + +- **Wails CLI:** `go install github.com/wailsapp/wails/v2/cmd/wails@latest` +- **Deps del sistema (Linux):** GTK3 + WebKit2GTK — usa + `install_wails_bash_infra` para instalarlas: + ```bash + source bash/functions/infra/install_wails.sh && install_wails + ``` + +## Notas + +Pipeline impuro: genera archivos via heredocs, ejecuta `go mod tidy` al +final como verificacion. Si Wails CLI no esta disponible, reporta el warning +y continua — el scaffold es valido, solo `wails build` falla hasta instalar +el CLI. + +El frontend importa `@fn_library` via alias en vite.config.ts apuntando a +`../../../frontend/functions/ui/` (los componentes del registry sin +duplicarlos). + +Los bindings de Wails (funciones del struct App) se regeneran en +`frontend/wailsjs/go/main/App.ts` automaticamente cuando corres `wails dev` +o `wails build`. + +Abort si `apps/{nombre}/` ya existe. + +El tag `launcher` permite que aparezca en el Pipeline Launcher TUI. diff --git a/bash/functions/pipelines/init_desktop_app.sh b/bash/functions/pipelines/init_desktop_app.sh new file mode 100755 index 00000000..0a0b164d --- /dev/null +++ b/bash/functions/pipelines/init_desktop_app.sh @@ -0,0 +1,594 @@ +#!/usr/bin/env bash +# init_desktop_app +# ---------------- +# Scaffold de Wails desktop app: Go backend + React frontend con @mantine y +# @fn_library. Genera main.go (Wails con embed del frontend), app.go (struct +# App con bindings base), wails.json, go.mod y frontend/ con vite+react+mantine. +# +# USO: +# ./init_desktop_app.sh [--with-db] +# +# FLAGS: +# --with-db Anade store.go con SQLite + bindings CRUD de ejemplo +# +# EJEMPLO: +# ./init_desktop_app.sh my_tool +# ./init_desktop_app.sh data_explorer --with-db + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh" + +NOMBRE="" +WITH_DB="false" +SKIP_WAILS_BUILD="${SKIP_WAILS_BUILD:-false}" + +while [ $# -gt 0 ]; do + case "$1" in + --with-db) WITH_DB="true"; shift ;; + --skip-wails-build) SKIP_WAILS_BUILD="true"; shift ;; + -h|--help) grep "^#" "$0" | sed 's/^# \?//' ; exit 0 ;; + -*) echo "Flag desconocido: $1" >&2 ; exit 1 ;; + *) + if [ -z "$NOMBRE" ]; then NOMBRE="$1" + else echo "Argumento extra ignorado: $1" >&2 + fi + shift ;; + esac +done + +if [ -z "$NOMBRE" ]; then + echo "Uso: $0 [--with-db]" >&2 + exit 1 +fi + +APP_DIR="${REGISTRY_ROOT}/apps/${NOMBRE}" +if [ -d "$APP_DIR" ]; then + echo "ERROR: ${APP_DIR} ya existe. Abortando." >&2 + exit 1 +fi + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " INIT DESKTOP APP (Wails): ${NOMBRE}" +echo " Directorio: ${APP_DIR}" +echo " DB: ${WITH_DB}" +echo "════════════════════════════════════════════════════════════" +echo "" + +# ── 1. Verificar Go y Wails ────────────────────────────────── + +echo "[1/5] Verificando herramientas..." +assert_command_exists go +echo " Go: $(go version)" + +if command -v wails >/dev/null 2>&1; then + echo " Wails: $(wails version 2>/dev/null || echo detectado)" +else + echo " WARN: Wails CLI no detectado." + echo " Instalar: source ${REGISTRY_ROOT}/bash/functions/infra/install_wails.sh && install_wails" + echo " (Continuando con scaffold; \`wails build\` requiere el CLI.)" +fi + +# ── 2. Crear estructura + archivos Go ──────────────────────── + +echo "[2/5] Creando estructura y archivos Go..." +mkdir -p "$APP_DIR/frontend/src/pages" +mkdir -p "$APP_DIR/build/bin" + +# go.mod con replace a fn-registry +cat > "$APP_DIR/go.mod" < ${REGISTRY_ROOT} +EOF + +# main.go: entry point Wails con embed frontend/dist +cat > "$APP_DIR/main.go" <<'EOF' +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + app := NewApp() + + err := wails.Run(&options.App{ + Title: "__APP_NAME__", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +EOF +sed -i "s/__APP_NAME__/${NOMBRE}/g" "$APP_DIR/main.go" + +# app.go: struct App con bindings base +cat > "$APP_DIR/app.go" < "$APP_DIR/wails.json" < "$APP_DIR/store.go" <<'EOF' +package main + +import ( + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" +) + +// Item es un registro de ejemplo. +type Item struct { + ID int64 `json:"id"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` +} + +// openDB abre (o crea) la base de datos SQLite en dbPath e inicializa el schema. +func openDB(dbPath string) (*sql.DB, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("open db: %w", err) + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +)`); err != nil { + return nil, fmt.Errorf("init schema: %w", err) + } + return db, nil +} + +// ListItems devuelve todos los items. Binding accesible desde el frontend. +func (a *App) ListItems() ([]Item, error) { + rows, err := a.db.Query(`SELECT id, name, created_at FROM items ORDER BY id DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + + var items []Item + for rows.Next() { + var it Item + if err := rows.Scan(&it.ID, &it.Name, &it.CreatedAt); err != nil { + return nil, err + } + items = append(items, it) + } + return items, nil +} + +// CreateItem inserta un nuevo item. +func (a *App) CreateItem(name string) (Item, error) { + res, err := a.db.Exec(`INSERT INTO items (name) VALUES (?)`, name) + if err != nil { + return Item{}, err + } + id, _ := res.LastInsertId() + return Item{ID: id, Name: name}, nil +} +EOF + # Cuando hay DB, actualizamos app.go para incluir db field + cat > "$APP_DIR/app.go" < "$APP_DIR/go.mod" < ${REGISTRY_ROOT} +EOF +fi + +# ── 3. Frontend ────────────────────────────────────────────── + +echo "[3/5] Generando frontend..." + +cat > "$APP_DIR/frontend/package.json" < "$APP_DIR/frontend/vite.config.ts" <<'EOF' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +// Vite config para Wails: build → dist/, alias @fn_library a frontend/functions/ui +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@fn_library": path.resolve(__dirname, "../../../frontend/functions/ui"), + }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + }, +}); +EOF + +cat > "$APP_DIR/frontend/tsconfig.json" <<'EOF' +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@fn_library/*": ["../../../frontend/functions/ui/*"] + } + }, + "include": ["src"] +} +EOF + +cat > "$APP_DIR/frontend/index.html" < + + + + + ${NOMBRE} + + +
+ + + +EOF + +cat > "$APP_DIR/frontend/postcss.config.cjs" <<'EOF' +module.exports = { + plugins: { + "postcss-preset-mantine": {}, + }, +}; +EOF + +cat > "$APP_DIR/frontend/src/theme.ts" <<'EOF' +import { createTheme } from "@mantine/core"; +export const theme = createTheme({ + primaryColor: "blue", + defaultRadius: "md", +}); +EOF + +cat > "$APP_DIR/frontend/src/main.tsx" <<'EOF' +import React from "react"; +import ReactDOM from "react-dom/client"; +import { MantineProvider } from "@mantine/core"; +import "@mantine/core/styles.css"; + +import { theme } from "./theme"; +import { App } from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); +EOF + +cat > "$APP_DIR/frontend/src/App.tsx" < | null>(null); + + useEffect(() => { + GetVersion().then(setVersion).catch(() => setVersion(null)); + }, []); + + return ( + + ${NOMBRE} + Desktop app scaffoldeada por init_desktop_app. + + + + Version + + {version ? \`\${version.app} (\${version.goOS} \${version.goVer})\` : "cargando..."} + + + + + + + + {greeting && {greeting}} + + + + ); +} +EOF + +cat > "$APP_DIR/frontend/.gitignore" <<'EOF' +node_modules/ +dist/ +wailsjs/ +*.log +EOF + +# .gitignore raiz +cat > "$APP_DIR/.gitignore" < "$APP_DIR/app.md" </dev/null 2>&1; then + echo " wails detectado. Ejecutar manualmente 'wails build' en ${APP_DIR}" + echo " (el build real requiere pnpm install previo en frontend/)" +else + echo " wails no disponible — saltando build." + echo " El scaffold esta listo. Para builds: instalar Wails primero." +fi + +# Al menos verificar que go mod tidy funciona (que el main.go/app.go es Go valido) +( + cd "$APP_DIR" + if CGO_ENABLED=1 go mod tidy 2>&1 | tail -5; then + echo " go mod tidy OK" + else + echo " WARN: go mod tidy fallo — revisa main.go/app.go/go.mod" >&2 + fi +) + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " DESKTOP APP '${NOMBRE}' LISTA" +echo "════════════════════════════════════════════════════════════" +echo "" +echo " Pasos siguientes:" +echo " cd apps/${NOMBRE}/frontend && pnpm install && cd .." +echo " wails dev # modo desarrollo con hot reload" +echo " wails build # binario de produccion en build/bin/" +echo ""