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>
314 lines
8.9 KiB
Go
314 lines
8.9 KiB
Go
// Package main provides the entry point for the rdev API server.
|
|
//
|
|
// rdev (Remote Developer) provides a REST API for controlling Claude Code
|
|
// instances running in Kubernetes pods. External clients (Discord bots,
|
|
// CLI tools, etc.) connect via this API.
|
|
//
|
|
// Endpoints:
|
|
// - GET /health - Health check
|
|
// - GET /ready - Readiness check
|
|
// - GET /docs - Scalar API documentation
|
|
// - GET /openapi.json - OpenAPI 3.0 specification
|
|
// - GET /projects - List available projects
|
|
// - GET /projects/{id} - Get project details
|
|
// - POST /projects/{id}/claude - Run Claude command
|
|
// - POST /projects/{id}/shell - Run shell command
|
|
// - POST /projects/{id}/git - Run git command
|
|
// - GET /projects/{id}/events - SSE stream for output
|
|
package main
|
|
|
|
import (
|
|
"github.com/orchard9/rdev/internal/handlers"
|
|
"github.com/orchard9/rdev/pkg/api"
|
|
)
|
|
|
|
func main() {
|
|
app := api.New("rdev-api", api.WithPort(8080))
|
|
|
|
// Initialize handlers
|
|
projectsHandler := handlers.NewProjectsHandler()
|
|
|
|
// Register routes
|
|
projectsHandler.Mount(app.Router())
|
|
|
|
// Enable API documentation
|
|
app.EnableDocs(buildOpenAPISpec())
|
|
|
|
app.Run()
|
|
}
|
|
|
|
// buildOpenAPISpec creates the OpenAPI specification for the rdev API.
|
|
func buildOpenAPISpec() *api.OpenAPISpec {
|
|
spec := api.NewOpenAPISpec("rdev API", "0.4.0").
|
|
WithDescription(`Remote Developer API for controlling Claude Code instances in Kubernetes.
|
|
|
|
rdev runs Claude Code CLI in isolated pods, controlled via this REST API with SSE streaming.
|
|
External clients (Discord bots, Slack bots, CLI tools) connect here to interact with projects.
|
|
|
|
## Architecture
|
|
|
|
- **rdev-api**: This Go service (what you're looking at)
|
|
- **claudebox pods**: Isolated pods running Claude Code CLI
|
|
- **Projects**: Each project has its own claudebox pod with mounted workspace
|
|
|
|
## Authentication
|
|
|
|
Authentication is planned for v0.6. Currently the API is internal-only.
|
|
|
|
## Streaming
|
|
|
|
Command output is streamed via Server-Sent Events (SSE) at the /projects/{id}/events endpoint.
|
|
`).
|
|
WithServer("http://localhost:8080", "Local development").
|
|
WithServer("http://rdev-api.rdev.svc:8080", "Kubernetes internal")
|
|
|
|
// Tags
|
|
spec.WithTag("Projects", "Project management and discovery")
|
|
spec.WithTag("Commands", "Command execution (claude, shell, git)")
|
|
spec.WithTag("Events", "Server-Sent Events for real-time output")
|
|
spec.WithTag("System", "Health and readiness endpoints")
|
|
|
|
// System endpoints (auto-added by chassis, but document them)
|
|
spec.AddPath("/health", "get", api.Op(
|
|
"Health check",
|
|
"Returns the health status of the rdev-api service",
|
|
"System",
|
|
))
|
|
spec.AddPath("/ready", "get", api.Op(
|
|
"Readiness check",
|
|
"Returns whether the service is ready to accept traffic",
|
|
"System",
|
|
))
|
|
|
|
// Projects - List and Get
|
|
spec.AddPath("/projects", "get", withResponse(
|
|
"List projects",
|
|
"Returns all available projects (claudebox pods) in the cluster",
|
|
"Projects",
|
|
`[
|
|
{
|
|
"id": "pantheon",
|
|
"name": "Pantheon",
|
|
"description": "Go API backend",
|
|
"pod": "claudebox-pantheon-0",
|
|
"status": "running"
|
|
}
|
|
]`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}", "get", withParams(
|
|
"Get project",
|
|
"Returns details for a specific project including its configuration",
|
|
"Projects",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID (e.g., 'pantheon')", Required: true}},
|
|
))
|
|
|
|
// Commands
|
|
spec.AddPath("/projects/{id}/claude", "post", withRequestBody(
|
|
"Run Claude command",
|
|
"Executes a Claude Code prompt in the project's claudebox pod. Returns a command ID for tracking via SSE.",
|
|
"Commands",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{
|
|
"prompt": "fix the bug in auth handler",
|
|
"stream_id": "optional-correlation-id"
|
|
}`,
|
|
`{
|
|
"id": "cmd-pantheon-001",
|
|
"project": "pantheon",
|
|
"type": "claude",
|
|
"status": "queued",
|
|
"stream_url": "/projects/pantheon/events?stream_id=cmd-pantheon-001"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/shell", "post", withRequestBody(
|
|
"Run shell command",
|
|
"Executes a shell command in the project's claudebox pod. Use for running tests, builds, etc.",
|
|
"Commands",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{
|
|
"command": "go test ./...",
|
|
"stream_id": "optional-correlation-id"
|
|
}`,
|
|
`{
|
|
"id": "cmd-pantheon-002",
|
|
"project": "pantheon",
|
|
"type": "shell",
|
|
"status": "queued",
|
|
"stream_url": "/projects/pantheon/events?stream_id=cmd-pantheon-002"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/git", "post", withRequestBody(
|
|
"Run git command",
|
|
"Executes a git command in the project's claudebox pod. Use for commits, pushes, pulls, etc.",
|
|
"Commands",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{
|
|
"args": ["status"],
|
|
"stream_id": "optional-correlation-id"
|
|
}`,
|
|
`{
|
|
"id": "cmd-pantheon-003",
|
|
"project": "pantheon",
|
|
"type": "git",
|
|
"status": "queued",
|
|
"stream_url": "/projects/pantheon/events?stream_id=cmd-pantheon-003"
|
|
}`,
|
|
))
|
|
|
|
// SSE Events
|
|
spec.AddPath("/projects/{id}/events", "get", map[string]any{
|
|
"summary": "Stream events",
|
|
"description": `Server-Sent Events stream for real-time command output.
|
|
|
|
Connect to this endpoint to receive output as commands execute.
|
|
|
|
## Event Types
|
|
|
|
- **connected**: Initial connection confirmation
|
|
- **output**: Command output (stdout or stderr)
|
|
- **error**: Error occurred during execution
|
|
- **complete**: Command finished (includes exit_code and duration_ms)
|
|
- **heartbeat**: Keep-alive (sent every 30s)
|
|
|
|
## Example
|
|
|
|
` + "```javascript" + `
|
|
const events = new EventSource('/projects/pantheon/events?stream_id=cmd-001');
|
|
|
|
events.addEventListener('output', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
console.log(data.line);
|
|
});
|
|
|
|
events.addEventListener('complete', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
console.log('Done:', data.exit_code);
|
|
events.close();
|
|
});
|
|
` + "```",
|
|
"tags": []string{"Events"},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"description": "Project ID",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
{
|
|
"name": "stream_id",
|
|
"in": "query",
|
|
"description": "Command ID to filter events (optional)",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "SSE stream",
|
|
"content": map[string]any{
|
|
"text/event-stream": map[string]any{
|
|
"schema": map[string]any{
|
|
"type": "string",
|
|
"example": "event: output\ndata: {\"line\": \"Building...\", \"stream\": \"stdout\"}\n\n",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
return spec
|
|
}
|
|
|
|
// param represents an OpenAPI parameter.
|
|
type param struct {
|
|
Name string
|
|
In string
|
|
Description string
|
|
Required bool
|
|
}
|
|
|
|
// withResponse creates an operation with example response.
|
|
func withResponse(summary, description, tag, example string) map[string]any {
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description,
|
|
"tags": []string{tag},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Success",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": example,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withParams creates an operation with path parameters.
|
|
func withParams(summary, description, tag string, params []param) map[string]any {
|
|
parameters := make([]map[string]any, len(params))
|
|
for i, p := range params {
|
|
parameters[i] = map[string]any{
|
|
"name": p.Name,
|
|
"in": p.In,
|
|
"description": p.Description,
|
|
"required": p.Required,
|
|
"schema": map[string]any{"type": "string"},
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description,
|
|
"tags": []string{tag},
|
|
"parameters": parameters,
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withRequestBody creates an operation with request body and example response.
|
|
func withRequestBody(summary, description, tag string, params []param, requestExample, responseExample string) map[string]any {
|
|
parameters := make([]map[string]any, len(params))
|
|
for i, p := range params {
|
|
parameters[i] = map[string]any{
|
|
"name": p.Name,
|
|
"in": p.In,
|
|
"description": p.Description,
|
|
"required": p.Required,
|
|
"schema": map[string]any{"type": "string"},
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description,
|
|
"tags": []string{tag},
|
|
"parameters": parameters,
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": requestExample,
|
|
},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"201": map[string]any{
|
|
"description": "Command queued",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": responseExample,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|