一键导入
go-backend
Patterns and conventions for working on the devctl Go backend — adding API endpoints, handlers, SSE/WebSocket, service management, and error handling
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Patterns and conventions for working on the devctl Go backend — adding API endpoints, handlers, SSE/WebSocket, service management, and error handling
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
How to write and run devctl integration tests — where tests live, how to write failing tests first (TDD), and how to run them safely inside an Incus container without touching live host data.
Full workflow for tagging and publishing a devctl release and/or a PHP binaries release — versioning, release note generation from TODO.md, clearing TODO.md completed items, and the exact git/GitHub CLI steps for each release type.
How to add a new CLI command to devctl — writing the Cmd struct, registering it with init(), adding Client methods, using output helpers, and documenting it in README.md
How to add a new dashboard screenshot for a new or existing feature — wiring up scripts/screenshots.js, seeding data into the demo container via scripts/demo.sh, and adding the image reference to the README.
How to create, update, or maintain agent skills for this project — file structure, frontmatter rules, naming constraints, and where OpenCode discovers them
Full workflow for picking up and completing a TODO item from TODO.md — read the backlog, clarify, implement, install, browser-test, update docs, and move the item to Completed.
| name | go-backend |
| description | Patterns and conventions for working on the devctl Go backend — adding API endpoints, handlers, SSE/WebSocket, service management, and error handling |
| license | MIT |
| compatibility | opencode |
| metadata | {"layer":"backend","language":"go"} |
The backend is a single Go binary (main.go) that runs as a systemd system service (root). It uses net/http stdlib only — no third-party router. All subsystems are wired in main.go and passed into api.NewServer(...).
| Package | Responsibility |
|---|---|
api/ | HTTP handlers, route registration, SSE, WebSocket upgrade |
services/ | Static service registry, exec manager, status poller, process supervisor |
sites/ | Site CRUD (SQLite), Caddy Admin API client, fsnotify watcher |
php/ | PHP-FPM version detection, installer, php.ini read/write |
dumps/ | TCP dump receiver, WebSocket broadcast hub, SQLite store |
install/ | Idempotent APT-based service installers |
config/ | Config dir setup, static default service definitions |
db/ | SQLite open, goose migrations, sqlc-generated queries |
api/server.go inside registerRoutes() using Go 1.22+ method+path syntax:
s.mux.HandleFunc("GET /api/example/{id}", s.handleGetExample)
*Server in its own file (e.g. api/example.go).r.PathValue("id") (Go 1.22+) to read path parameters — never use a third-party router.writeJSON(w, payload) for success (defined in api/ as a small helper).http.Error(w, msg, statusCode) for errors — keep them terse.func (s *Server) handleGetExample(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
row, err := s.queries.GetExample(r.Context(), id)
if err == sql.ErrNoRows {
http.Error(w, "not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, row)
}
r.Context() to DB queries.*Server.Pattern used in api/services.go (handleServiceEvents) and api/services.go (handleServiceLogs):
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher := w.(http.Flusher)
for {
select {
case <-r.Context().Done():
return
case msg := <-ch:
fmt.Fprintf(w, "data: %s\n\n", msg)
flusher.Flush()
}
}
WebSocket upgrade lives in api/dumps.go. Uses github.com/gorilla/websocket. The hub pattern (register/unregister/broadcast) is in dumps/hub.go — follow the same pattern for any new WebSocket endpoint.
s.queries (*dbq.Queries from sqlc codegen in db/queries/).*sql.DB (s.db) is available for transactions or one-off exec.db/queries/*.sql and regenerate with make sqlc.Service definitions are static Go code in config/defaults.go — there is no services.yaml at runtime. services.NewRegistry(config.DefaultServices()) converts the slice to an in-memory *Registry at startup.
services/supervisor.go)Supervisor manages child processes for services with Managed: true (e.g. Laravel Reverb):
supervisor := services.NewSupervisor()
go supervisor.Run(runCtx) // auto-restart on crash, stop all on ctx done
supervisor.Start(def) // forks def.ManagedCmd + def.ManagedArgs in $HOME/sites/<id>
supervisor.Stop(id) // SIGTERM → 10s → SIGKILL
supervisor.Restart(def) // Stop + Start
supervisor.IsRunning(id) // bool
Start is a no-op if the process is already running.log.Printf with the service ID prefix.os.ExpandEnv("$HOME/sites/" + def.ID) by convention.GET /api/services/{id}/credentials reads $HOME/sites/<id>/.env, parses known keys, and returns a JSON map. Returns 404 if the file is missing. Currently returns REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET.
os.Getuid() == 0 check in main.go)./etc/devctl/ — database at /etc/devctl/devctl.db.log package — output goes to the systemd journal.make dev # go run . (no frontend rebuild)
make build # builds frontend then Go binary
make install # build + install binary + systemd unit (requires root)