Source-verified agent skills for Go: Cobra command-line interfaces, Viper configuration, and Wails desktop apps, each backed by tested examples.
99
100%
Does it follow best practices?
Impact
99%
1.02xAverage score across 18 eval scenarios
Passed
No findings from the security scan
Wails v2 runs a Go backend and a web frontend in one native window. Go
methods on a bound struct become promise-returning JavaScript functions, the
runtime package talks to the window through the context Wails hands to
OnStartup, and the frontend build is embedded in the binary. Most bugs come
from the edges of that contract: a method exposed by accident, a runtime call
with the wrong context, a build without the right tags, or a missing system
library on Linux.
go.mod. If it requires github.com/wailsapp/wails/v3, stop and use
the wails3 skill, or read the pinned v3 module source if that skill is not
installed. v2 and v3 APIs do not mix, so do not migrate between majors
unless asked. On v2, use only APIs present in the pinned version.wails.Run(&options.App{...})), the bound structs in
Bind, the lifecycle hooks, the embedded asset directory, and how the
project builds: wails.json with the Wails CLI, or plain go build from a
Makefile or script. Keep the existing workflow.go build without the CLI, Linux libraries, and packaging,
see references/building.md.Keep the bound struct thin, its exported methods the whole frontend API, and everything native behind an interface the tests can replace:
// examples/wails2/app/app.go (trimmed: storage, the other methods, and helpers omitted)
type Native interface {
SaveFileDialog(runtime.SaveDialogOptions) (string, error)
Emit(event string, data ...any)
}
type App struct {
mu sync.Mutex // bound calls run concurrently on Linux and macOS
notes []Note
native Native
}
func NewOptions(a *App, assets fs.FS) *options.App {
return &options.App{
Title: "Notes",
AssetServer: &assetserver.Options{Assets: assets},
OnStartup: a.startup, // unexported, so it is not bound
Bind: []any{a}, // exported methods become JavaScript functions
}
}
func (a *App) startup(ctx context.Context) {
if a.native == nil {
a.native = wailsNative{ctx: ctx}
}
}
func (a *App) SaveNote(in NoteInput) (Note, error) {
in, err := validate(in)
if err != nil {
return Note{}, err // rejects the frontend promise
}
return a.store(Note(in)) // locks a.mu and emits EventNotesChanged
}
// wailsNative calls the Wails runtime with the context from OnStartup.
type wailsNative struct{ ctx context.Context }
func (n wailsNative) SaveFileDialog(o runtime.SaveDialogOptions) (string, error) {
return runtime.SaveFileDialog(n.ctx, o)
}// examples/wails2/main.go
//go:embed all:frontend/dist
var assets embed.FS
func main() {
if err := wails.Run(app.NewOptions(app.New(nil), assets)); err != nil {
log.Fatal(err)
}
}Bind takes pointers to structs. Every exported method on the pointer type
becomes callable from the frontend; unexported methods never are. The one
exception is a method passed as a lifecycle hook (OnStartup, OnDomReady,
OnShutdown, OnBeforeClose). An exported Shutdown that is not wired to
a hook is callable from JavaScript. Keep hooks unexported and build options
in a plain function, not a method.T, error, or (T, error). A non-nil error rejects the promise
with err.Error() as a string unless options.App.ErrorFormatter is set.
An error-only method resolves to void.json
tag. Generated TypeScript follows encoding/json: an untagged field keeps
its Go name and json:"-" drops it. Return an empty slice, not nil, for
arrays; the frontend receives nil as null.runtime.EventsEmit, runtime.SaveFileDialog, window
calls) need the context passed to OnStartup or OnDomReady. With
context.Background() or a test context, the runtime calls log.Fatalf
and the process exits. Store the lifecycle context once and wrap the calls
behind an interface like Native above.OnDomReady, not OnStartup.
OnBeforeClose returns true to keep the window open; OnShutdown runs
after the window closes. Cancel app-owned goroutines in OnShutdown.runtime.EventsOn returns a function that removes the
listener; call it when the owner goes away."" (or a nil slice for multiple files) when the user
cancels. Treat that as a normal outcome: return without writing and without
an error.//go:embed all:frontend/dist embeds the built frontend; all: keeps files
whose names start with . or _. The asset server looks for index.html
below the embed root, so fs.Sub is not needed. Build the frontend before
go build, in the same Makefile target or script that compiles. If frontend/dist has no index.html, the app exits at startup
with "no index.html could be found in your Assets fs.FS". A production
binary must start without a Vite dev server.
Before finishing, run go vet ./... and go test ./... for the changed
packages, and check:
index.html is served from the embedded
assets;When a check fails, match the symptom in references/building.md or the sections above, fix it, and rerun every check. A browser test with a mocked bridge does not test WebKitGTK, WebView2, native dialogs, or window lifecycle; where a desktop session is available, launch the built app as a normal user, and state in the handoff what native behavior was not exercised.
Verified against Wails v2.16.0. Each stated behavior has a test or a source citation in the examples module. When the project's version differs, trust its module source over this skill.