## WS5: Developer Experience (DX) Improvements

### Purpose

Reduce time-to-first-successful-agent-call to under 5 minutes for a new developer. Three concrete improvements: (1) upgrade the developer portal's API explorer from Swagger UI v4 to Stoplight Elements — a modern, component-based API documentation experience with better navigation, code samples, and mock server support; (2) add a scaffold generator endpoint that returns a language-specific starter project pre-wired with the developer's agent credentials as a downloadable ZIP; (3) add a `sentryagent scaffold` CLI command that calls the scaffold endpoint and extracts the ZIP into the current directory.

### New Endpoint

#### `GET /sdk/scaffold/:agentId`

**Summary:** Generate and return a language-specific scaffold ZIP for the specified agent.

**Authentication:** Bearer token (tenant-scoped). The authenticated tenant must own the specified agent.

**Path Parameter:**

| Parameter | Type | Description |
|---|---|---|
| `agentId` | string (UUID) | The agent for which to generate the scaffold |

**Query Parameters:**

| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
| `language` | string | no | `typescript` | Enum: `typescript`, `python`, `go`, `java`, `rust` |

**Response 200:**
- Content-Type: `application/zip`
- Content-Disposition: `attachment; filename="sentryagent-scaffold-{agentName}-{language}.zip"`
- Body: Binary ZIP archive stream

**ZIP Archive Contents (TypeScript example):**

```
sentryagent-scaffold-my-agent-typescript/
├── package.json            (name: my-agent, version: 0.1.0, deps: sentryagent-idp-sdk)
├── tsconfig.json           (strict mode, ES2022 target)
├── .env.example            (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
├── .gitignore              (.env on first line)
├── src/
│   └── index.ts            (imports SDK, creates client from env, issues token, logs success)
└── README.md               (step-by-step: cp .env.example .env, fill secret, npm install, npm start)
```

**ZIP Archive Contents (Python example):**
```
sentryagent-scaffold-my-agent-python/
├── requirements.txt        (sentryagent-idp)
├── .env.example            (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
├── .gitignore              (.env on first line)
├── main.py                 (imports SDK, creates client from env, issues token, prints success)
└── README.md               (step-by-step: cp .env.example .env, fill secret, pip install -r requirements.txt, python main.py)
```

**ZIP Archive Contents (Go example):**
```
sentryagent-scaffold-my-agent-go/
├── go.mod                  (module: my-agent, dep: github.com/sentryagent/sentryagent-idp-go)
├── .env.example            (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
├── .gitignore              (.env on first line)
├── main.go                 (imports SDK, creates client from env, issues token, logs success)
└── README.md               (step-by-step instructions)
```

**Error Responses:**

| Status | Code | Description |
|---|---|---|
| 400 | `INVALID_LANGUAGE` | `language` query param is not one of the supported values |
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
| 403 | `FORBIDDEN` | Authenticated tenant does not own this agent |
| 404 | `AGENT_NOT_FOUND` | No agent with `agentId` found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |

**Business Rules:**
- `clientId` is pre-filled in `.env.example` — taken from the agent's credentials in the database
- `clientSecret` is always a `<your-client-secret>` placeholder — never returned in scaffold (credentials security policy)
- The ZIP is generated in memory using `archiver` — no disk writes on the server
- Scaffold generation is rate-limited to 10 requests per minute per tenant (separate from the main tier rate limit)
- An audit log entry is created with `event_type: "scaffold.generated"`, `metadata.language`

---

### Developer Portal: Elements API Explorer Upgrade

**File to modify:** `portal/app/api-explorer/page.tsx`

**Current state (Phase 4):** Embeds `swagger-ui-react` (Swagger UI v4) loaded from `NEXT_PUBLIC_API_URL/openapi.json`.

**New state (Phase 5):** Replaces `swagger-ui-react` with `@stoplight/elements` (`<API>` component). Stoplight Elements provides: three-panel layout (navigation, docs, try-it), built-in code samples in multiple languages, mock server support, and better mobile responsiveness.

**Implementation:**

```tsx
// portal/app/api-explorer/page.tsx (complete replacement)
'use client';

import { API } from '@stoplight/elements';
import '@stoplight/elements/styles.min.css';

export default function ApiExplorerPage() {
    return (
        <main className="h-screen w-full">
            <API
                apiDescriptionUrl={`${process.env.NEXT_PUBLIC_API_URL}/openapi.json`}
                router="hash"
                layout="sidebar"
                hideSchemas={false}
                tryItCredentialsPolicy="same-origin"
            />
        </main>
    );
}
```

**Files modified:**
- `portal/app/api-explorer/page.tsx` — replace Swagger UI component with Elements `<API>` component
- `portal/package.json` — replace `swagger-ui-react` with `@stoplight/elements`

---

### CLI: `sentryagent scaffold` Command

**File to create:** `cli/src/commands/scaffold.ts`

**Command syntax:**
```
sentryagent scaffold --agent-id <id> [--language typescript|python|go|java|rust] [--out <directory>]
```

**Options:**

| Option | Alias | Default | Description |
|---|---|---|---|
| `--agent-id <id>` | `-a` | (required) | Agent ID to scaffold for |
| `--language <lang>` | `-l` | `typescript` | Target language for scaffold |
| `--out <dir>` | `-o` | `.` (current dir) | Directory to extract scaffold ZIP into |

**Behavior:**
1. Load config from `~/.sentryagent/config.json` — fail with helpful message if not configured
2. Issue an API call: `GET /sdk/scaffold/{agentId}?language={language}` with Bearer token from `POST /oauth2/token`
3. Receive ZIP stream, pipe through `unzipper` to extract into `--out` directory
4. Print success message: `Scaffold generated at ./{agentName}-{language}/`
5. Print next steps:
   ```
   Next steps:
     1. cd {agentName}-{language}
     2. cp .env.example .env
     3. Add your AGENTIDP_CLIENT_SECRET to .env
     4. npm install  (or equivalent for your language)
     5. npm start
   ```

**Error handling:**
- Agent not found: print `Agent {agentId} not found.`
- Forbidden: print `You do not own agent {agentId}.`
- Invalid language: print `Unsupported language '{lang}'. Choose: typescript, python, go, java, rust`
- Output directory does not exist: create it (with user prompt for confirmation if non-empty)

**New CLI dependencies** (add to `cli/package.json`):
- `unzipper` — streaming ZIP extraction (pure JS, no native deps)

### New Source Files

| File | Description |
|---|---|
| `src/services/ScaffoldService.ts` | Business logic: build ZIP archive in memory using `archiver` |
| `src/controllers/ScaffoldController.ts` | HTTP handler: stream ZIP response |
| `src/routes/scaffold.ts` | Express router: `GET /sdk/scaffold/:agentId` |
| `src/types/scaffold.ts` | TypeScript interfaces: `ScaffoldLanguage`, `ScaffoldOptions`, `ScaffoldTemplate` |
| `src/templates/scaffold/typescript/` | Template files for TypeScript scaffold (package.json, tsconfig.json, index.ts, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/python/` | Template files for Python scaffold (requirements.txt, main.py, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/go/` | Template files for Go scaffold (go.mod, main.go, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/java/` | Template files for Java scaffold (pom.xml, Main.java, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/rust/` | Template files for Rust scaffold (Cargo.toml, src/main.rs, .env.example, .gitignore, README.md) |
| `cli/src/commands/scaffold.ts` | CLI scaffold command implementation |

### Modified Source Files

| File | Change |
|---|---|
| `src/routes/index.ts` | Register `scaffold` router |
| `src/app.ts` | No change needed (routes registered via index) |
| `package.json` (API) | Add `archiver` and `@types/archiver` |
| `portal/app/api-explorer/page.tsx` | Replace Swagger UI with Elements |
| `portal/package.json` | Replace `swagger-ui-react` with `@stoplight/elements` |
| `cli/src/index.ts` | Register `scaffold` command with Commander |
| `cli/package.json` | Add `unzipper` and `@types/unzipper` |
| `docs/openapi.yaml` | Add `GET /sdk/scaffold/:agentId` endpoint |

### `ScaffoldService` Interface

```typescript
interface IScaffoldService {
    /**
     * Generate an in-memory ZIP archive for the given agent and language.
     * Returns a Node.js Readable stream of the ZIP binary.
     * Template variables injected: {{AGENT_ID}}, {{AGENT_NAME}}, {{CLIENT_ID}}, {{API_URL}}
     */
    generateScaffold(
        agentId: string,
        language: ScaffoldLanguage,
        apiUrl: string
    ): Promise<{ stream: NodeJS.ReadableStream; filename: string }>;
}
```

### Prometheus Metrics

| Metric | Type | Labels | Description |
|---|---|---|---|
| `agentidp_scaffold_generated_total` | Counter | `language` | Scaffold ZIPs generated by language |
| `agentidp_scaffold_generation_duration_ms` | Histogram | `language` | Time to generate scaffold ZIP |

### Acceptance Criteria

- `GET /sdk/scaffold/:agentId?language=typescript` returns a valid ZIP with all 6 template files
- ZIP contains `.env.example` with `AGENTIDP_CLIENT_ID` pre-filled and `AGENTIDP_CLIENT_SECRET=<your-client-secret>` as placeholder
- ZIP never contains the actual client secret
- `GET /sdk/scaffold/:agentId?language=python` returns Python-specific template files
- All 5 languages (typescript, python, go, java, rust) return valid ZIPs
- HTTP 400 on unknown `language` query param
- HTTP 403 when authenticated tenant does not own the agent
- `sentryagent scaffold --agent-id abc123 --language go` extracts scaffold to current directory
- `sentryagent scaffold --agent-id abc123 --language python --out /tmp/myagent` extracts to `/tmp/myagent`
- Developer portal `/api-explorer` renders Elements v5 with sidebar layout — TypeScript build passes
- Unit tests cover: scaffold generation (each language), forbidden access, invalid language
- Integration tests cover: scaffold endpoint response type, content-disposition header, ZIP validity
