feat: funciones Wails — scaffold, CRUD bindings, build, eventos y streaming

Funciones Go para crear apps Wails: scaffold estructura, bind CRUD genérico,
build multiplataforma, emit eventos y stream de datos al frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:55:24 +02:00
parent a75170cbc6
commit e02a950ee0
10 changed files with 676 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
package infra
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
)
// ScaffoldWailsAppConfig configura la generación del proyecto Wails.
type ScaffoldWailsAppConfig struct {
Name string // Nombre del proyecto
Dir string // Directorio destino
Title string // Título de la ventana
Width int // Ancho de la ventana (default 1024)
Height int // Alto de la ventana (default 768)
Author string // Nombre del autor
FrontendLib string // Path a la frontend library (default ~/.local_agentes/frontend/frontend)
}
const mainGoTpl = `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: "{{.Title}}",
Width: {{.Width}},
Height: {{.Height}},
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())
}
}
`
const appGoTpl = `package main
import (
"context"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct — cada método público se expone como binding IPC al frontend.
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// EmitEvent emite un evento al frontend.
func (a *App) EmitEvent(eventName string, data interface{}) {
runtime.EventsEmit(a.ctx, eventName, data)
}
// Ping verifica que el IPC funciona.
func (a *App) Ping() string {
return "pong"
}
`
const wailsJSONTpl = `{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "{{.Name}}",
"outputfilename": "{{.Name}}",
"frontend:dir": "./frontend",
"frontend:install": "pnpm install",
"frontend:build": "pnpm run build",
"frontend:dev:watcher": "pnpm run dev",
"frontend:dev:serverUrl": "auto",
"wailsjsdir": "./frontend/src/wailsjs",
"author": {
"name": "{{.Author}}"
}
}
`
const goModTpl = `module {{.Name}}
go 1.23
require github.com/wailsapp/wails/v2 v2.11.0
`
// ScaffoldWailsApp genera la estructura base de un proyecto Wails con frontend vinculado.
func ScaffoldWailsApp(ctx context.Context, cfg ScaffoldWailsAppConfig) error {
if cfg.Name == "" {
return fmt.Errorf("name is required")
}
if cfg.Dir == "" {
cfg.Dir = cfg.Name
}
if cfg.Title == "" {
cfg.Title = cfg.Name
}
if cfg.Width == 0 {
cfg.Width = 1024
}
if cfg.Height == 0 {
cfg.Height = 768
}
if cfg.Author == "" {
cfg.Author = "Agent"
}
if cfg.FrontendLib == "" {
home, _ := os.UserHomeDir()
cfg.FrontendLib = filepath.Join(home, ".local_agentes", "frontend", "frontend")
}
// Crear directorio
if err := os.MkdirAll(cfg.Dir, 0755); err != nil {
return fmt.Errorf("creating dir: %w", err)
}
// Generar archivos Go
files := map[string]string{
"main.go": mainGoTpl,
"app.go": appGoTpl,
"wails.json": wailsJSONTpl,
"go.mod": goModTpl,
}
for name, tpl := range files {
path := filepath.Join(cfg.Dir, name)
t, err := template.New(name).Parse(tpl)
if err != nil {
return fmt.Errorf("parsing template %s: %w", name, err)
}
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating %s: %w", name, err)
}
if err := t.Execute(f, cfg); err != nil {
f.Close()
return fmt.Errorf("executing template %s: %w", name, err)
}
f.Close()
}
// Crear frontend como link simbólico o copiar template
frontendDir := filepath.Join(cfg.Dir, "frontend")
createProjectScript := filepath.Join(filepath.Dir(cfg.FrontendLib), "..", "scripts", "create-project.sh")
if _, err := os.Stat(createProjectScript); err == nil {
// Usar el script de Frontend_Library
cmd := exec.CommandContext(ctx, "bash", createProjectScript, cfg.Name, frontendDir, "--wails")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("creating frontend: %w", err)
}
} else {
// Fallback: crear frontend mínimo
if err := os.MkdirAll(filepath.Join(frontendDir, "src"), 0755); err != nil {
return fmt.Errorf("creating frontend dir: %w", err)
}
pkgJSON := fmt.Sprintf(`{"name":"%s-frontend","private":true,"scripts":{"dev":"vite","build":"vite build"}}`, cfg.Name)
os.WriteFile(filepath.Join(frontendDir, "package.json"), []byte(pkgJSON), 0644)
}
// go mod tidy
cmd := exec.CommandContext(ctx, "go", "mod", "tidy")
cmd.Dir = cfg.Dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run() // No fatal si falla
fmt.Printf("Wails app scaffolded at %s\n", cfg.Dir)
return nil
}
// GenerateAppBinding genera el código Go para un método de binding.
func GenerateAppBinding(name string, params []string, returnType string, body string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("func (a *App) %s(", name))
sb.WriteString(strings.Join(params, ", "))
sb.WriteString(")")
if returnType != "" {
sb.WriteString(" " + returnType)
}
sb.WriteString(" {\n")
sb.WriteString("\t" + body + "\n")
sb.WriteString("}\n")
return sb.String()
}
+38
View File
@@ -0,0 +1,38 @@
---
name: scaffold_wails_app
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "ScaffoldWailsApp(ctx context.Context, cfg ScaffoldWailsAppConfig) error"
description: "Genera proyecto Wails completo: main.go con embed, app.go con bindings base, wails.json, go.mod, y frontend vinculado a Frontend_Library."
tags: [wails, scaffold, desktop, project, generator]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [os, os/exec, path/filepath, text/template, fmt, strings, context]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/scaffold_wails_app.go"
---
## Ejemplo
```go
ScaffoldWailsApp(ctx, ScaffoldWailsAppConfig{
Name: "my-dashboard",
Dir: "/home/user/projects/my-dashboard",
Title: "My Dashboard",
Width: 1280,
Height: 800,
Author: "Lucas",
})
```
## Notas
Pipeline que compone: generación de templates Go + creación de frontend via create-project.sh de Frontend_Library + go mod tidy. Incluye GenerateAppBinding para crear métodos de binding programáticamente.
+85
View File
@@ -0,0 +1,85 @@
package infra
import (
"fmt"
"strings"
)
// WailsCRUDSpec define la especificación para generar bindings CRUD.
type WailsCRUDSpec struct {
EntityName string // Nombre de la entidad (PascalCase, ej: "User")
Fields []string // Campos de la entidad (ej: ["Name string", "Email string"])
WithList bool // Generar List method
WithGet bool // Generar Get method
WithCreate bool // Generar Create method
WithUpdate bool // Generar Update method
WithDelete bool // Generar Delete method
}
// GenerateWailsCRUD genera código Go de bindings CRUD para una entidad.
// Retorna el código Go como string — función pura.
func GenerateWailsCRUD(spec WailsCRUDSpec) string {
var sb strings.Builder
lower := strings.ToLower(spec.EntityName)
plural := lower + "s"
// Tipo
sb.WriteString(fmt.Sprintf("// %s entity\n", spec.EntityName))
sb.WriteString(fmt.Sprintf("type %s struct {\n", spec.EntityName))
sb.WriteString("\tID string `json:\"id\"`\n")
for _, field := range spec.Fields {
parts := strings.SplitN(field, " ", 2)
if len(parts) == 2 {
jsonTag := strings.ToLower(parts[0])
sb.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n", parts[0], parts[1], jsonTag))
}
}
sb.WriteString("}\n\n")
// List
if spec.WithList {
sb.WriteString(fmt.Sprintf("// List%s retorna todos los %s.\n", spec.EntityName+"s", plural))
sb.WriteString(fmt.Sprintf("func (a *App) List%s() ([]%s, error) {\n", spec.EntityName+"s", spec.EntityName))
sb.WriteString(fmt.Sprintf("\t// TODO: implement List%s\n", spec.EntityName+"s"))
sb.WriteString(fmt.Sprintf("\treturn nil, fmt.Errorf(\"not implemented\")\n"))
sb.WriteString("}\n\n")
}
// Get
if spec.WithGet {
sb.WriteString(fmt.Sprintf("// Get%s retorna un %s por ID.\n", spec.EntityName, lower))
sb.WriteString(fmt.Sprintf("func (a *App) Get%s(id string) (%s, error) {\n", spec.EntityName, spec.EntityName))
sb.WriteString(fmt.Sprintf("\t// TODO: implement Get%s\n", spec.EntityName))
sb.WriteString(fmt.Sprintf("\treturn %s{}, fmt.Errorf(\"not implemented\")\n", spec.EntityName))
sb.WriteString("}\n\n")
}
// Create
if spec.WithCreate {
sb.WriteString(fmt.Sprintf("// Create%s crea un nuevo %s.\n", spec.EntityName, lower))
sb.WriteString(fmt.Sprintf("func (a *App) Create%s(%s %s) (%s, error) {\n", spec.EntityName, lower, spec.EntityName, spec.EntityName))
sb.WriteString(fmt.Sprintf("\t// TODO: implement Create%s\n", spec.EntityName))
sb.WriteString(fmt.Sprintf("\treturn %s, fmt.Errorf(\"not implemented\")\n", lower))
sb.WriteString("}\n\n")
}
// Update
if spec.WithUpdate {
sb.WriteString(fmt.Sprintf("// Update%s actualiza un %s existente.\n", spec.EntityName, lower))
sb.WriteString(fmt.Sprintf("func (a *App) Update%s(%s %s) (%s, error) {\n", spec.EntityName, lower, spec.EntityName, spec.EntityName))
sb.WriteString(fmt.Sprintf("\t// TODO: implement Update%s\n", spec.EntityName))
sb.WriteString(fmt.Sprintf("\treturn %s, fmt.Errorf(\"not implemented\")\n", lower))
sb.WriteString("}\n\n")
}
// Delete
if spec.WithDelete {
sb.WriteString(fmt.Sprintf("// Delete%s elimina un %s por ID.\n", spec.EntityName, lower))
sb.WriteString(fmt.Sprintf("func (a *App) Delete%s(id string) error {\n", spec.EntityName))
sb.WriteString(fmt.Sprintf("\t// TODO: implement Delete%s\n", spec.EntityName))
sb.WriteString("\treturn fmt.Errorf(\"not implemented\")\n")
sb.WriteString("}\n\n")
}
return sb.String()
}
+40
View File
@@ -0,0 +1,40 @@
---
name: wails_bind_crud
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "GenerateWailsCRUD(spec WailsCRUDSpec) string"
description: "Genera código Go de bindings CRUD para Wails: struct + métodos List/Get/Create/Update/Delete con stubs not-implemented."
tags: [wails, crud, generator, bindings, codegen, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [fmt, strings]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/wails_bind_crud.go"
---
## Ejemplo
```go
code := GenerateWailsCRUD(WailsCRUDSpec{
EntityName: "User",
Fields: []string{"Name string", "Email string", "Role string"},
WithList: true,
WithGet: true,
WithCreate: true,
WithUpdate: true,
WithDelete: true,
})
// Genera: type User struct + ListUsers + GetUser + CreateUser + UpdateUser + DeleteUser
```
## Notas
Función pura — genera código como string. Los métodos son stubs con `return fmt.Errorf("not implemented")`. Un agente puede generar el código, insertarlo en app.go, y luego implementar los TODOs.
+65
View File
@@ -0,0 +1,65 @@
package infra
import (
"context"
"fmt"
"os/exec"
"strings"
)
// WailsBuildConfig configura la compilación de un proyecto Wails.
type WailsBuildConfig struct {
Dir string // Directorio del proyecto Wails
Platform string // linux, windows, darwin (default: current)
Output string // Nombre del binario de salida
Debug bool // Build con debug info
}
// WailsBuild compila un proyecto Wails para la plataforma especificada.
func WailsBuild(ctx context.Context, cfg WailsBuildConfig) error {
if cfg.Dir == "" {
return fmt.Errorf("dir is required")
}
args := []string{"build"}
if cfg.Platform != "" {
args = append(args, "-platform", cfg.Platform)
}
if cfg.Output != "" {
args = append(args, "-o", cfg.Output)
}
if cfg.Debug {
args = append(args, "-debug")
}
cmd := exec.CommandContext(ctx, "wails", args...)
cmd.Dir = cfg.Dir
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("wails build failed: %w\n%s", err, string(output))
}
fmt.Printf("Built successfully: %s\n", strings.TrimSpace(string(output)))
return nil
}
// WailsDev lanza un proyecto Wails en modo desarrollo con hot reload.
func WailsDev(ctx context.Context, dir string, browser bool) error {
if dir == "" {
return fmt.Errorf("dir is required")
}
args := []string{"dev"}
if browser {
args = append(args, "-browser")
}
cmd := exec.CommandContext(ctx, "wails", args...)
cmd.Dir = dir
cmd.Stdout = nil // inherit
cmd.Stderr = nil
return cmd.Run()
}
+38
View File
@@ -0,0 +1,38 @@
---
name: wails_build
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "WailsBuild(ctx context.Context, cfg WailsBuildConfig) error"
description: "Compila un proyecto Wails para linux/windows/darwin. Incluye WailsDev para modo desarrollo con hot reload."
tags: [wails, build, compile, desktop, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [os/exec, fmt, strings, context]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/wails_build.go"
---
## Ejemplo
```go
// Build para linux
WailsBuild(ctx, WailsBuildConfig{
Dir: "/home/user/my-app",
Platform: "linux",
})
// Desarrollo con hot reload + browser
WailsDev(ctx, "/home/user/my-app", true)
```
## Notas
Requiere `wails` CLI instalado. WailsDev bloquea el proceso (es un servidor de desarrollo).
+40
View File
@@ -0,0 +1,40 @@
//go:build ignore
// NOTE: requires wails dependency in target project — use this file by copying into a Wails project
package infra
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// WailsEventPayload es un envelope para eventos tipados.
type WailsEventPayload struct {
Type string `json:"type"`
Data interface{} `json:"data"`
Timestamp time.Time `json:"timestamp"`
}
// WailsEmitEvent emite un evento tipado al frontend.
func WailsEmitEvent(ctx context.Context, eventName string, data interface{}) {
payload := WailsEventPayload{
Type: eventName,
Data: data,
Timestamp: time.Now(),
}
runtime.EventsEmit(ctx, eventName, payload)
}
// WailsEmitJSON emite datos como JSON string al frontend.
func WailsEmitJSON(ctx context.Context, eventName string, data interface{}) error {
b, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("marshaling event data: %w", err)
}
runtime.EventsEmit(ctx, eventName, string(b))
return nil
}
+35
View File
@@ -0,0 +1,35 @@
---
name: wails_emit_event
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "WailsEmitEvent(ctx context.Context, eventName string, data interface{})"
description: "Emite eventos tipados de Go al frontend con timestamp automático. Incluye WailsEmitJSON para serialización explícita."
tags: [wails, event, emit, ipc, realtime, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [context, encoding/json, fmt, time, github.com/wailsapp/wails/v2/pkg/runtime]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/wails_emit_event.go"
---
## Ejemplo
```go
// Emitir evento tipado
WailsEmitEvent(ctx, "price:update", PriceData{Symbol: "BTC", Price: 67500.0})
// El frontend lo recibe con useWailsEvent:
// useWailsEvent({ eventName: 'price:update', onEvent: (data) => ... })
```
## Notas
El evento llega al frontend como WailsEventPayload con `type`, `data` y `timestamp`. Complementa `use_wails_event` del lado TS. Requiere la dependencia `github.com/wailsapp/wails/v2` en el proyecto destino.
+70
View File
@@ -0,0 +1,70 @@
//go:build ignore
// NOTE: requires wails dependency in target project — use this file by copying into a Wails project
package infra
import (
"context"
"fmt"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// WailsStreamConfig configura un stream de datos hacia el frontend.
type WailsStreamConfig struct {
StreamName string // Nombre del stream (evento base)
ChunkDelay time.Duration // Delay entre chunks (default 0)
}
// WailsStreamData envía un slice de datos como stream al frontend.
// Emite cada elemento como chunk en {streamName}, y {streamName}:complete al terminar.
func WailsStreamData[T any](ctx context.Context, cfg WailsStreamConfig, data []T) error {
if cfg.StreamName == "" {
return fmt.Errorf("stream name is required")
}
for _, chunk := range data {
select {
case <-ctx.Done():
runtime.EventsEmit(ctx, cfg.StreamName+":error", map[string]string{
"message": "stream cancelled",
})
return ctx.Err()
default:
runtime.EventsEmit(ctx, cfg.StreamName, chunk)
if cfg.ChunkDelay > 0 {
time.Sleep(cfg.ChunkDelay)
}
}
}
runtime.EventsEmit(ctx, cfg.StreamName+":complete", nil)
return nil
}
// WailsStreamFunc ejecuta una función generadora y envía resultados como stream.
// La función generadora envía datos por el canal, y esta función los retransmite al frontend.
func WailsStreamFunc[T any](ctx context.Context, streamName string, generator func(ctx context.Context, ch chan<- T) error) error {
ch := make(chan T, 100)
errCh := make(chan error, 1)
go func() {
defer close(ch)
errCh <- generator(ctx, ch)
}()
for chunk := range ch {
runtime.EventsEmit(ctx, streamName, chunk)
}
if err := <-errCh; err != nil {
runtime.EventsEmit(ctx, streamName+":error", map[string]string{
"message": err.Error(),
})
return err
}
runtime.EventsEmit(ctx, streamName+":complete", nil)
return nil
}
+50
View File
@@ -0,0 +1,50 @@
---
name: wails_stream_data
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "WailsStreamData[T any](ctx context.Context, cfg WailsStreamConfig, data []T) error"
description: "Envía datos como stream Go→TS con protocolo {name}/{name}:complete/{name}:error. Incluye WailsStreamFunc para generadores."
tags: [wails, stream, ipc, realtime, chunks, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [context, fmt, time, github.com/wailsapp/wails/v2/pkg/runtime]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/wails_stream_data.go"
---
## Ejemplo
```go
// Stream de slice
WailsStreamData(ctx, WailsStreamConfig{
StreamName: "logs",
ChunkDelay: 10 * time.Millisecond,
}, logLines)
// Stream con generador
WailsStreamFunc(ctx, "metrics", func(ctx context.Context, ch chan<- Metric) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(time.Second):
ch <- collectMetric()
}
}
})
// Frontend lo recibe con useWailsStream:
// useWailsStream({ streamName: 'metrics', autoStart: true })
```
## Notas
Protocolo: chunks en `{streamName}`, fin en `{streamName}:complete`, error en `{streamName}:error`. Compatible con `use_wails_stream` del lado TS. WailsStreamFunc usa goroutine + canal para datos asíncronos. Requiere Go 1.18+ (generics) y la dependencia `github.com/wailsapp/wails/v2` en el proyecto destino.