#!/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 ""