diff --git a/functions/infra/scaffold_wails_app.go b/functions/infra/scaffold_wails_app.go new file mode 100644 index 00000000..63530d34 --- /dev/null +++ b/functions/infra/scaffold_wails_app.go @@ -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() +} diff --git a/functions/infra/scaffold_wails_app.md b/functions/infra/scaffold_wails_app.md new file mode 100644 index 00000000..a7e4bcb4 --- /dev/null +++ b/functions/infra/scaffold_wails_app.md @@ -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. diff --git a/functions/infra/wails_bind_crud.go b/functions/infra/wails_bind_crud.go new file mode 100644 index 00000000..7bbfc878 --- /dev/null +++ b/functions/infra/wails_bind_crud.go @@ -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() +} diff --git a/functions/infra/wails_bind_crud.md b/functions/infra/wails_bind_crud.md new file mode 100644 index 00000000..8dc73a1d --- /dev/null +++ b/functions/infra/wails_bind_crud.md @@ -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. diff --git a/functions/infra/wails_build.go b/functions/infra/wails_build.go new file mode 100644 index 00000000..13beeb89 --- /dev/null +++ b/functions/infra/wails_build.go @@ -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() +} diff --git a/functions/infra/wails_build.md b/functions/infra/wails_build.md new file mode 100644 index 00000000..3f75b8bd --- /dev/null +++ b/functions/infra/wails_build.md @@ -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). diff --git a/functions/infra/wails_emit_event.go b/functions/infra/wails_emit_event.go new file mode 100644 index 00000000..d6f86b43 --- /dev/null +++ b/functions/infra/wails_emit_event.go @@ -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 +} diff --git a/functions/infra/wails_emit_event.md b/functions/infra/wails_emit_event.md new file mode 100644 index 00000000..3c08d90d --- /dev/null +++ b/functions/infra/wails_emit_event.md @@ -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. diff --git a/functions/infra/wails_stream_data.go b/functions/infra/wails_stream_data.go new file mode 100644 index 00000000..f86880e4 --- /dev/null +++ b/functions/infra/wails_stream_data.go @@ -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 +} diff --git a/functions/infra/wails_stream_data.md b/functions/infra/wails_stream_data.md new file mode 100644 index 00000000..333dfb78 --- /dev/null +++ b/functions/infra/wails_stream_data.md @@ -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.