Implements a fully documented API server following the aeries chassis pattern:
- pkg/api: Simplified chassis with App, Response helpers, and OpenAPI builder
- cmd/rdev-api: Entry point with full OpenAPI spec for all v0.4 endpoints
- internal/handlers: Stubbed project handlers (list, get, claude, shell, git, events)
Endpoints:
- GET /health, /ready - Health checks
- GET /docs, /openapi.json - Scalar API docs
- GET /projects - List projects
- GET /projects/{id} - Get project
- POST /projects/{id}/claude, shell, git - Run commands
- GET /projects/{id}/events - SSE streaming
Uses Scalar for dark-mode API documentation at /docs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
193 lines
5.0 KiB
Go
193 lines
5.0 KiB
Go
// Package handlers provides HTTP handlers for the rdev API.
|
|
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/orchard9/rdev/pkg/api"
|
|
)
|
|
|
|
// ProjectsHandler handles project-related endpoints.
|
|
type ProjectsHandler struct{}
|
|
|
|
// NewProjectsHandler creates a new projects handler.
|
|
func NewProjectsHandler() *ProjectsHandler {
|
|
return &ProjectsHandler{}
|
|
}
|
|
|
|
// Mount registers the projects routes.
|
|
func (h *ProjectsHandler) Mount(r api.Router) {
|
|
r.Route("/projects", func(r chi.Router) {
|
|
r.Get("/", h.List)
|
|
r.Get("/{id}", h.Get)
|
|
r.Post("/{id}/claude", h.RunClaude)
|
|
r.Post("/{id}/shell", h.RunShell)
|
|
r.Post("/{id}/git", h.RunGit)
|
|
r.Get("/{id}/events", h.Events)
|
|
})
|
|
}
|
|
|
|
// List returns all available projects.
|
|
// GET /projects
|
|
func (h *ProjectsHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
// TODO: Implement project discovery from K8s
|
|
projects := []map[string]any{
|
|
{
|
|
"id": "pantheon",
|
|
"name": "Pantheon",
|
|
"description": "Go API backend",
|
|
"pod": "claudebox-pantheon-0",
|
|
"status": "running",
|
|
},
|
|
{
|
|
"id": "aeries",
|
|
"name": "Aeries",
|
|
"description": "Note community platform",
|
|
"pod": "claudebox-aeries-0",
|
|
"status": "running",
|
|
},
|
|
}
|
|
|
|
api.WriteSuccess(w, r, projects)
|
|
}
|
|
|
|
// Get returns a specific project by ID.
|
|
// GET /projects/{id}
|
|
func (h *ProjectsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
|
|
// TODO: Look up project from registry
|
|
project := map[string]any{
|
|
"id": id,
|
|
"name": id,
|
|
"description": "Project description",
|
|
"pod": "claudebox-" + id + "-0",
|
|
"status": "running",
|
|
"workspace": "/workspace",
|
|
"config": map[string]any{
|
|
"claude_auth": true,
|
|
"git_enabled": true,
|
|
"last_command": nil,
|
|
},
|
|
}
|
|
|
|
api.WriteSuccess(w, r, project)
|
|
}
|
|
|
|
// RunClaude executes a Claude command in the project's claudebox.
|
|
// POST /projects/{id}/claude
|
|
func (h *ProjectsHandler) RunClaude(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
|
|
// TODO: Parse request body for prompt
|
|
// TODO: Execute kubectl exec -n rdev claudebox-{id}-0 -- claude "{prompt}"
|
|
|
|
result := map[string]any{
|
|
"id": "cmd-" + id + "-001",
|
|
"project": id,
|
|
"type": "claude",
|
|
"status": "queued",
|
|
"stream_url": "/projects/" + id + "/events?stream_id=cmd-" + id + "-001",
|
|
}
|
|
|
|
api.WriteCreated(w, r, result)
|
|
}
|
|
|
|
// RunShell executes a shell command in the project's claudebox.
|
|
// POST /projects/{id}/shell
|
|
func (h *ProjectsHandler) RunShell(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
|
|
// TODO: Parse request body for command
|
|
// TODO: Execute kubectl exec -n rdev claudebox-{id}-0 -- bash -c "{command}"
|
|
|
|
result := map[string]any{
|
|
"id": "cmd-" + id + "-002",
|
|
"project": id,
|
|
"type": "shell",
|
|
"status": "queued",
|
|
"stream_url": "/projects/" + id + "/events?stream_id=cmd-" + id + "-002",
|
|
}
|
|
|
|
api.WriteCreated(w, r, result)
|
|
}
|
|
|
|
// RunGit executes a git command in the project's claudebox.
|
|
// POST /projects/{id}/git
|
|
func (h *ProjectsHandler) RunGit(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
|
|
// TODO: Parse request body for git command
|
|
// TODO: Execute kubectl exec -n rdev claudebox-{id}-0 -- git {args}
|
|
|
|
result := map[string]any{
|
|
"id": "cmd-" + id + "-003",
|
|
"project": id,
|
|
"type": "git",
|
|
"status": "queued",
|
|
"stream_url": "/projects/" + id + "/events?stream_id=cmd-" + id + "-003",
|
|
}
|
|
|
|
api.WriteCreated(w, r, result)
|
|
}
|
|
|
|
// Events streams command output via Server-Sent Events.
|
|
// GET /projects/{id}/events
|
|
func (h *ProjectsHandler) Events(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
streamID := r.URL.Query().Get("stream_id")
|
|
|
|
// Set SSE headers
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "SSE not supported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// TODO: Stream actual command output
|
|
// For now, send mock events
|
|
|
|
// Send initial connected event
|
|
writeSSE(w, flusher, "connected", map[string]any{
|
|
"project": id,
|
|
"stream_id": streamID,
|
|
})
|
|
|
|
// Send mock output
|
|
writeSSE(w, flusher, "output", map[string]any{
|
|
"line": "Starting command execution...",
|
|
"stream": "stdout",
|
|
})
|
|
|
|
writeSSE(w, flusher, "output", map[string]any{
|
|
"line": "Command completed successfully.",
|
|
"stream": "stdout",
|
|
})
|
|
|
|
// Send complete event
|
|
writeSSE(w, flusher, "complete", map[string]any{
|
|
"exit_code": 0,
|
|
"duration_ms": 1234,
|
|
})
|
|
}
|
|
|
|
// writeSSE writes a Server-Sent Event.
|
|
func writeSSE(w http.ResponseWriter, flusher http.Flusher, event string, data map[string]any) {
|
|
dataBytes, _ := jsonMarshal(data)
|
|
w.Write([]byte("event: " + event + "\n"))
|
|
w.Write([]byte("data: " + string(dataBytes) + "\n\n"))
|
|
flusher.Flush()
|
|
}
|
|
|
|
// jsonMarshal is a simple JSON marshal helper.
|
|
func jsonMarshal(v any) ([]byte, error) {
|
|
return json.Marshal(v)
|
|
}
|