feat(phase-5): WS5 — Developer Experience
Implements scaffold ZIP generator, Stoplight Elements API explorer, and CLI scaffold command: Scaffold API: - 25 template files for TypeScript/Python/Go/Java/Rust in src/templates/scaffold/ - ScaffoldService: in-memory ZIP via archiver, variable injection (AGENT_ID/NAME/CLIENT_ID/API_URL) - ScaffoldController: tenant ownership check (403), language validation (400), ZIP stream response - Route GET /sdk/scaffold/:agentId with rate limiter (10 req/min per tenant) - Prometheus: scaffold_generated_total + scaffold_generation_duration_ms histogram Portal: - Replaced swagger-ui-react with @stoplight/elements API component - Dynamic import (ssr: false) for browser-only DOM dependency - Type declarations for @stoplight/elements and CSS module CLI: - sentryagent scaffold --agent-id <id> [--language typescript] [--out .] - Raw fetch for binary ZIP stream → unzipper.Extract() → prints next steps - Human-readable 400/403/404 error messages Tests: 19 tests (unit + integration), ScaffoldService 80%+ branch coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
73
src/templates/scaffold/go/main.go.tmpl
Normal file
73
src/templates/scaffold/go/main.go.tmpl
Normal file
@@ -0,0 +1,73 @@
|
||||
// {{AGENT_NAME}} — SentryAgent.ai agent starter
|
||||
// Agent ID: {{AGENT_ID}}
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
apiURL := getEnv("AGENTIDP_API_URL", "{{API_URL}}")
|
||||
clientID := os.Getenv("AGENTIDP_CLIENT_ID")
|
||||
clientSecret := os.Getenv("AGENTIDP_CLIENT_SECRET")
|
||||
|
||||
if clientID == "" || clientSecret == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: AGENTIDP_CLIENT_ID and AGENTIDP_CLIENT_SECRET must be set")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Issuing token for agent {{AGENT_ID}} at %s ...\n", apiURL)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("grant_type", "client_credentials")
|
||||
form.Set("client_id", clientID)
|
||||
form.Set("client_secret", clientSecret)
|
||||
form.Set("scope", "agents:read")
|
||||
|
||||
resp, err := http.Post(apiURL+"/api/v1/token", "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Fprintf(os.Stderr, "Token issuance failed: HTTP %d\n", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var token TokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to decode response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Token issued successfully!")
|
||||
fmt.Printf(" Expires in: %ds\n", token.ExpiresIn)
|
||||
truncated := token.AccessToken
|
||||
if len(truncated) > 20 {
|
||||
truncated = truncated[:20]
|
||||
}
|
||||
fmt.Printf(" Token (first 20 chars): %s...\n", truncated)
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
Reference in New Issue
Block a user