diff --git a/marketplaces/default.json b/marketplaces/default.json index 60c03b7..28c7d75 100644 --- a/marketplaces/default.json +++ b/marketplaces/default.json @@ -483,6 +483,22 @@ "verification", "sample" ] + }, + { + "name": "openapi-to-frontend", + "source": "./plugins/openapi-to-frontend", + "description": "Generate a full TypeScript client, React component library, and frontend app from an OpenAPI spec. Includes tests and CI/CD workflows.", + "category": "development", + "keywords": [ + "openapi", + "swagger", + "typescript", + "react", + "codegen", + "frontend", + "api", + "client" + ] } ] } diff --git a/plugins/openapi-to-frontend/README.md b/plugins/openapi-to-frontend/README.md new file mode 100644 index 0000000..29f0552 --- /dev/null +++ b/plugins/openapi-to-frontend/README.md @@ -0,0 +1,513 @@ +# openapi-to-frontend + +Generate a full TypeScript client, React component library, and frontend app from an OpenAPI spec. + +## Overview + +This plugin converts an OpenAPI specification into three layers of output: + +1. **TypeScript Client** — one class per schema, one method per API endpoint +2. **React Component Library** — one component per schema, wired to the TS client +3. **React Frontend App** — a purpose-built UI inferred from the API's description +4. **Comprehensive Tests** — unit, integration, and e2e tests +5. **GitHub Actions CI/CD** — build/test on PR, deploy to GitHub Pages, publish to npm + +The plugin also handles **incremental updates**: given a change to the OpenAPI spec, it produces targeted changes rather than regenerating everything. + +## Quick Start + +**Initial generation** — generate a complete frontend from an OpenAPI spec: + +``` +/openapi-to-frontend path/to/openapi.yaml +``` + +**Incremental update** — update existing code when the spec changes: + +``` +/openapi-to-frontend new-spec.json old-spec.json +``` + +This generates/updates: + +1. **TypeScript client** — types and API class +2. **React components** — Form, Detail, List per schema +3. **Frontend app** — routing, context, pages +4. **Tests** — unit, integration, and e2e +5. **CI workflows** — GitHub Actions for build/test/deploy + +## Plugin Contents + +``` +plugins/openapi-to-frontend/ +├── README.md # This file +├── commands/ +│ └── openapi-to-frontend.md # Main command: generate or update +├── skills/ +│ ├── generate-client/ +│ │ └── SKILL.md # Phase 1: OpenAPI → TypeScript client +│ ├── generate-components/ +│ │ └── SKILL.md # Phase 2: TS client → React components +│ ├── generate-frontend/ +│ │ └── SKILL.md # Phase 3: Components → full app +│ ├── generate-tests/ +│ │ └── SKILL.md # Phase 4: Tests +│ ├── generate-ci/ +│ │ └── SKILL.md # Phase 5: GitHub Actions +│ └── update-from-spec/ +│ └── SKILL.md # Incremental updates +├── agents/ +│ └── spec-differ.md # Subagent: diffs two spec versions +├── hooks/ +│ └── hooks.json # (reserved for future use) +├── scripts/ +│ ├── parse-openapi.py # Extract schemas, endpoints, auth from spec +│ ├── lint-generated.sh # Run eslint + tsc on generated code +│ ├── verify-coverage.py # Cross-reference spec against client +│ └── verify-components.py # Cross-reference components against client +├── references/ +│ ├── auth-patterns.md # Bearer, API key, OAuth2 handling +│ ├── naming-conventions.md # Spec→TS→React naming rules +│ └── change-taxonomy.md # Enumerated change types + handling +└── .mcp.json # (reserved for future use) +``` + +## Features + +- **Full Stack Generation** — From spec to deployable frontend in one workflow +- **Type Safety** — TypeScript throughout, with proper generics and inference +- **Auth Support** — API Key, Bearer Token, and OAuth2 flows +- **Component Library** — Reusable Form, Detail, and List components per schema +- **Tailored UIs** — Frontend design inferred from API purpose (CRUD, analytics, workflow) +- **Incremental Updates** — Surgical edits when the spec changes +- **Test Coverage** — Unit, integration, and e2e tests with mock factories +- **CI/CD Ready** — GitHub Actions for build, test, deploy, and publish + +## Prerequisites + +- Node.js 18+ with npm or yarn +- TypeScript 5+ +- React 18+ +- OpenAPI 3.0+ specification (JSON or YAML) + +## Output Structure + +After running all phases: + +``` +your-project/ +├── client/ +│ ├── types.ts # TypeScript interfaces for all schemas +│ ├── api.ts # API class with async methods +│ ├── auth.ts # Auth configuration +│ └── index.ts # Barrel export +├── components/ +│ ├── / +│ │ ├── Form.tsx +│ │ ├── Detail.tsx +│ │ ├── List.tsx +│ │ └── index.ts +│ ├── shared/ +│ │ ├── LoadingSpinner.tsx +│ │ ├── ErrorDisplay.tsx +│ │ └── Pagination.tsx +│ └── index.ts +├── app/ +│ ├── App.tsx +│ ├── pages/ +│ ├── context/ +│ ├── hooks/ +│ └── utils/ +├── tests/ +│ ├── unit/ +│ ├── integration/ +│ ├── e2e/ +│ └── setup/ +└── .github/ + └── workflows/ + ├── ci.yml + ├── deploy.yml + └── publish.yml +``` + +## Workflow Phases + +### Phase 1: Generate Client + +Creates the TypeScript API client: + +- **types.ts** — Interface for every schema in `components/schemas` +- **api.ts** — Class with async methods for every endpoint +- **auth.ts** — Auth handlers based on `securitySchemes` + +See [skills/generate-client/SKILL.md](skills/generate-client/SKILL.md) + +### Phase 2: Generate Components + +Creates React components for each schema: + +- **Form** — Create/edit with validation +- **Detail** — Read-only view +- **List** — Table with pagination + +See [skills/generate-components/SKILL.md](skills/generate-components/SKILL.md) + +### Phase 3: Generate Frontend + +Creates the application shell: + +- Routing based on API resources +- Auth context and guards +- UI tailored to API purpose (CRUD, analytics, workflow, search) + +See [skills/generate-frontend/SKILL.md](skills/generate-frontend/SKILL.md) + +### Phase 4: Generate Tests + +Creates comprehensive test coverage: + +- **Unit** — Client methods, type guards, component rendering +- **Integration** — Form→Client flows, auth context +- **E2E** — Smoke tests, CRUD flows, auth flows + +See [skills/generate-tests/SKILL.md](skills/generate-tests/SKILL.md) + +### Phase 5: Generate CI + +Creates GitHub Actions workflows: + +- **ci.yml** — Build + test on push/PR +- **deploy.yml** — Deploy to GitHub Pages +- **publish.yml** — Publish packages to npm + +See [skills/generate-ci/SKILL.md](skills/generate-ci/SKILL.md) + +## Incremental Updates + +When the OpenAPI spec changes: + +1. The spec-differ agent compares old and new specs +2. Changes are classified (new schema, removed endpoint, etc.) +3. Surgical edits are applied to existing files + +See [skills/update-from-spec/SKILL.md](skills/update-from-spec/SKILL.md) + +## Auth Patterns + +The plugin supports three auth styles: + +| Style | Client Handling | Frontend UI | +|-------|-----------------|-------------| +| API Key | Attached as header/query per spec | Settings page for key input | +| Bearer Token | `Authorization: Bearer ` | Token input or login form | +| OAuth2 | Full flow with PKCE, auto-refresh | Login/callback/logout pages | + +See [references/auth-patterns.md](references/auth-patterns.md) + +## Naming Conventions + +| OpenAPI | TypeScript | React Component | +|---------|------------|-----------------| +| `UserProfile` schema | `interface UserProfile` | `UserProfileForm`, `UserProfileDetail` | +| `GET /users/{id}` | `getUser(id: string)` | Used in `UserDetail` | +| `POST /users` | `createUser(data)` | Used in `UserForm` | +| `snake_case` field | `camelCase` property | "Title Case" label | + +See [references/naming-conventions.md](references/naming-conventions.md) + +## Automated Generation with GitHub Actions + +You can set up a GitHub Action that automatically generates and updates your frontend codebase from a remote OpenAPI spec. The workflow: + +1. **First run**: Fetches the spec, generates the full codebase, and commits the spec as a snapshot +2. **Subsequent runs**: Fetches the new spec, compares with the snapshot, applies incremental updates + +### Sample Workflow + +Create `.github/workflows/sync-openapi.yml` in your repository: + +```yaml +name: Sync OpenAPI Frontend + +on: + # Run on demand + workflow_dispatch: + # Or on a schedule (e.g., daily at midnight) + schedule: + - cron: '0 0 * * *' + # Or when the workflow file itself changes + push: + paths: + - '.github/workflows/sync-openapi.yml' + +env: + # ⚠️ CONFIGURE THIS: URL to your OpenAPI specification + OPENAPI_SPEC_URL: 'https://api.example.com/openapi.json' + # Where to store the spec snapshot + SPEC_SNAPSHOT_PATH: '.openapi/spec.json' + # OpenHands extensions branch (use 'main' for stable, or a feature branch) + OPENHANDS_EXTENSIONS_BRANCH: ${{ vars.OPENHANDS_EXTENSIONS_BRANCH || 'main' }} + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install OpenHands CLI + run: | + pip install uv + uv tool install openhands --python 3.12 + + - name: Install openapi-to-frontend plugin + run: | + # Clone the OpenHands extensions repository + git clone --depth 1 --branch "$OPENHANDS_EXTENSIONS_BRANCH" \ + https://github.com/OpenHands/extensions.git \ + /tmp/openhands-extensions + + # Install the plugin to the OpenHands skills directory + mkdir -p ~/.openhands/plugins + cp -r /tmp/openhands-extensions/plugins/openapi-to-frontend ~/.openhands/plugins/ + + echo "✅ Installed openapi-to-frontend plugin from branch: $OPENHANDS_EXTENSIONS_BRANCH" + + - name: Create spec directory + run: mkdir -p $(dirname $SPEC_SNAPSHOT_PATH) + + - name: Download current OpenAPI spec + run: | + curl -fSL "$OPENAPI_SPEC_URL" -o new-spec.json + echo "Downloaded spec from $OPENAPI_SPEC_URL" + + - name: Check if this is initial generation or update + id: check-mode + run: | + if [ -f "$SPEC_SNAPSHOT_PATH" ]; then + echo "mode=update" >> $GITHUB_OUTPUT + echo "📝 Existing spec found - will perform incremental update" + else + echo "mode=initial" >> $GITHUB_OUTPUT + echo "🆕 No existing spec - will perform initial generation" + fi + + - name: Check for spec changes (update mode only) + id: check-changes + if: steps.check-mode.outputs.mode == 'update' + run: | + if diff -q "$SPEC_SNAPSHOT_PATH" new-spec.json > /dev/null 2>&1; then + echo "changed=false" >> $GITHUB_OUTPUT + echo "✅ Spec unchanged - no update needed" + else + echo "changed=true" >> $GITHUB_OUTPUT + echo "🔄 Spec changed - update required" + # Keep the old spec for diff + cp "$SPEC_SNAPSHOT_PATH" old-spec.json + fi + + - name: Generate initial codebase + if: steps.check-mode.outputs.mode == 'initial' + env: + LLM_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LLM_MODEL: ${{ vars.LLM_MODEL || 'anthropic/claude-opus-4-20250514' }} + LLM_BASE_URL: ${{ vars.LLM_BASE_URL || '' }} + run: | + openhands --headless --override-with-envs -t "Read ~/.openhands/plugins/openapi-to-frontend/commands/openapi-to-frontend.md and execute: /openapi-to-frontend new-spec.json" + + - name: Apply incremental updates + if: steps.check-mode.outputs.mode == 'update' && steps.check-changes.outputs.changed == 'true' + env: + LLM_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LLM_MODEL: ${{ vars.LLM_MODEL || 'anthropic/claude-opus-4-20250514' }} + LLM_BASE_URL: ${{ vars.LLM_BASE_URL || '' }} + run: | + openhands --headless --override-with-envs -t "Read ~/.openhands/plugins/openapi-to-frontend/commands/openapi-to-frontend.md and execute: /openapi-to-frontend new-spec.json old-spec.json" + + - name: Update spec snapshot + if: steps.check-mode.outputs.mode == 'initial' || steps.check-changes.outputs.changed == 'true' + run: | + cp new-spec.json "$SPEC_SNAPSHOT_PATH" + echo "📸 Spec snapshot updated" + + - name: Revert changes to .github directory + if: steps.check-mode.outputs.mode == 'initial' || steps.check-changes.outputs.changed == 'true' + run: | + git checkout HEAD -- .github/ 2>/dev/null || true + echo "🔄 Reverted any changes to .github/ directory" + + - name: Create Pull Request + if: steps.check-mode.outputs.mode == 'initial' || steps.check-changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + ${{ steps.check-mode.outputs.mode == 'initial' && 'chore: initial frontend generation from OpenAPI spec' || 'chore: update frontend from OpenAPI spec changes' }} + title: | + ${{ steps.check-mode.outputs.mode == 'initial' && '🆕 Initial frontend generation from OpenAPI' || '🔄 Update frontend from OpenAPI changes' }} + body: | + ## Summary + + ${{ steps.check-mode.outputs.mode == 'initial' && 'This PR contains the initial frontend codebase generated from the OpenAPI specification.' || 'This PR contains incremental updates based on changes to the OpenAPI specification.' }} + + **Spec URL:** `${{ env.OPENAPI_SPEC_URL }}` + **Plugin branch:** `${{ env.OPENHANDS_EXTENSIONS_BRANCH }}` + + ## What's included + + ${{ steps.check-mode.outputs.mode == 'initial' && '- TypeScript API client (`client/`) + - React components (`components/`) + - Frontend application (`app/`) + - Test suite (`tests/`) + - CI/CD workflows (`.github/workflows/`) + - OpenAPI spec snapshot (`.openapi/spec.json`)' || '- Updated TypeScript types and API methods + - Updated React components + - Updated tests + - New OpenAPI spec snapshot' }} + + ## Review checklist + + - [ ] Generated code compiles without errors + - [ ] Tests pass + - [ ] UI renders correctly + - [ ] API integration works as expected + branch: openapi-frontend-sync + delete-branch: true + + - name: Skip message + if: steps.check-mode.outputs.mode == 'update' && steps.check-changes.outputs.changed == 'false' + run: echo "✅ No changes detected in OpenAPI spec - nothing to do" +``` + +### Configuration + +1. **Set your OpenAPI URL**: Update the `OPENAPI_SPEC_URL` environment variable to point to your API's spec +2. **Configure triggers**: Adjust the `on:` section for your needs (manual, scheduled, or event-based) +3. **Set up secrets** (in repository Settings → Secrets and variables → Actions): + - `ANTHROPIC_API_KEY` (required): Your Anthropic API key +4. **Optional variables** (in repository Settings → Secrets and variables → Actions → Variables): + - `LLM_MODEL`: Model to use (defaults to `anthropic/claude-opus-4-20250514`) + - `LLM_BASE_URL`: Custom API base URL (optional, for proxies) + - `OPENHANDS_EXTENSIONS_BRANCH`: Branch of OpenHands/extensions to use (defaults to `main`) + +**Using a different LLM provider:** + +To use OpenAI or another provider, update the `env` sections in the workflow: +```yaml +env: + LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }} + LLM_MODEL: ${{ vars.LLM_MODEL || 'openai/gpt-4o' }} +``` + +**Environment Variables:** + +The workflow uses `--override-with-envs` to configure OpenHands via environment variables: +- `LLM_API_KEY`: Your LLM provider API key (required) +- `LLM_MODEL`: The model to use, e.g., `anthropic/claude-opus-4-20250514` (required) +- `LLM_BASE_URL`: Custom API endpoint (optional) + +### How the Snapshot Works + +The workflow maintains a snapshot of the OpenAPI spec at `.openapi/spec.json`: + +``` +your-repo/ +├── .openapi/ +│ └── spec.json # Committed snapshot of the last processed spec +├── client/ +├── components/ +├── app/ +└── ... +``` + +- **Initial run**: No snapshot exists, so full generation runs and the spec is committed +- **Subsequent runs**: The snapshot is compared with the freshly downloaded spec + - If unchanged: workflow exits early + - If changed: incremental update runs, using `old-spec.json` (the snapshot) and `new-spec.json` (the download) + +This ensures the agent always knows the exact state of the codebase relative to the spec it was generated from. + +### Alternative: Local Spec File + +If your OpenAPI spec is checked into the repository instead of fetched from a URL: + +```yaml +env: + # Path to spec in your repo + OPENAPI_SPEC_PATH: 'api/openapi.yaml' + SPEC_SNAPSHOT_PATH: '.openapi/last-processed-spec.yaml' + +# ... in steps: +- name: Check for spec changes + run: | + if [ -f "$SPEC_SNAPSHOT_PATH" ]; then + if diff -q "$OPENAPI_SPEC_PATH" "$SPEC_SNAPSHOT_PATH" > /dev/null; then + echo "No changes" + else + cp "$SPEC_SNAPSHOT_PATH" old-spec.yaml + cp "$OPENAPI_SPEC_PATH" new-spec.yaml + # ... trigger update + fi + else + cp "$OPENAPI_SPEC_PATH" new-spec.yaml + # ... trigger initial generation + fi +``` + +## Scripts + +### parse-openapi.py + +```bash +python scripts/parse-openapi.py openapi.yaml > spec-summary.json +``` + +Outputs structured JSON with schemas, endpoints, and auth schemes. + +### lint-generated.sh + +```bash +./scripts/lint-generated.sh +``` + +Runs eslint and tsc on generated code. Returns non-zero on errors. + +### verify-coverage.py + +```bash +python scripts/verify-coverage.py openapi.yaml client/ +``` + +Ensures every schema and endpoint has corresponding TypeScript code. + +### verify-components.py + +```bash +python scripts/verify-components.py client/ components/ +``` + +Ensures every type has corresponding React components. + +## Contributing + +See the main [extensions repository](https://github.com/OpenHands/extensions) for contribution guidelines. + +## License + +This plugin is part of the OpenHands extensions repository. See [LICENSE](../../LICENSE) for details. diff --git a/plugins/openapi-to-frontend/agents/spec-differ.md b/plugins/openapi-to-frontend/agents/spec-differ.md new file mode 100644 index 0000000..8f1e399 --- /dev/null +++ b/plugins/openapi-to-frontend/agents/spec-differ.md @@ -0,0 +1,357 @@ +# Spec Differ Agent + +A subagent that compares two OpenAPI specification versions and produces a structured diff. + +## Purpose + +This agent analyzes the differences between two OpenAPI specs and classifies each change into a type that can be acted upon by the update-from-spec skill. + +## Invocation + +``` +@spec-differ +``` + +## Output Format + +The agent produces a JSON structure: + +```json +{ + "summary": { + "schemas_added": 2, + "schemas_removed": 0, + "schemas_modified": 3, + "endpoints_added": 4, + "endpoints_removed": 1, + "endpoints_modified": 2, + "auth_changed": false + }, + "changes": [ + { + "type": "schema_added", + "path": "components/schemas/Order", + "details": { + "name": "Order", + "fields": [ + { "name": "id", "type": "string", "required": true }, + { "name": "userId", "type": "string", "required": true }, + { "name": "items", "type": "array", "items": "OrderItem", "required": true }, + { "name": "status", "type": "OrderStatus", "required": true }, + { "name": "createdAt", "type": "string", "format": "date-time", "required": true } + ] + } + }, + { + "type": "field_added", + "path": "components/schemas/User.properties.phoneNumber", + "details": { + "schema": "User", + "field": "phoneNumber", + "type": "string", + "required": false + } + }, + { + "type": "endpoint_added", + "path": "paths./orders.get", + "details": { + "method": "GET", + "path": "/orders", + "operationId": "listOrders", + "parameters": [ + { "name": "page", "in": "query", "type": "integer" }, + { "name": "status", "in": "query", "type": "OrderStatus" } + ], + "response": "OrderListResponse" + } + } + ], + "breaking_changes": [ + { + "type": "field_removed", + "path": "components/schemas/User.properties.legacyId", + "severity": "high", + "migration": "Ensure no client code references User.legacyId before removing" + } + ] +} +``` + +## Change Types + +### Schema Changes + +| Type | Description | +|------|-------------| +| `schema_added` | New schema in components/schemas | +| `schema_removed` | Schema deleted | +| `schema_renamed` | Schema name changed (detected by field similarity) | +| `field_added` | New property added to schema | +| `field_removed` | Property removed from schema | +| `field_type_changed` | Property type changed | +| `field_required_changed` | Required status changed | + +### Endpoint Changes + +| Type | Description | +|------|-------------| +| `endpoint_added` | New path+method combination | +| `endpoint_removed` | Path+method deleted | +| `endpoint_method_changed` | HTTP method changed (rare) | +| `param_added` | New parameter (path, query, header, body) | +| `param_removed` | Parameter removed | +| `param_type_changed` | Parameter type changed | +| `response_type_changed` | Response schema changed | +| `response_code_added` | New response code | +| `response_code_removed` | Response code removed | + +### Auth Changes + +| Type | Description | +|------|-------------| +| `auth_scheme_added` | New security scheme | +| `auth_scheme_removed` | Security scheme removed | +| `auth_scheme_modified` | Scheme configuration changed | +| `security_requirement_changed` | Endpoint auth requirements changed | + +### Metadata Changes + +| Type | Description | +|------|-------------| +| `info_changed` | title, version, description changed | +| `server_added` | New server URL | +| `server_removed` | Server URL removed | +| `tag_added` | New tag | +| `tag_removed` | Tag removed | + +## Diff Algorithm + +### Step 1: Normalize Specs + +1. Parse both specs (JSON or YAML) +2. Resolve all `$ref` references +3. Sort keys for consistent comparison +4. Handle nullable and oneOf/anyOf unions + +### Step 2: Compare Schemas + +For each schema in old spec: +- If not in new spec → `schema_removed` +- If in new spec → compare fields + +For each schema in new spec: +- If not in old spec → `schema_added` + +For each field: +- Compare type, format, required, enum values + +### Step 3: Compare Endpoints + +Build a key for each endpoint: `{method} {path}` + +For each endpoint in old spec: +- If not in new spec → `endpoint_removed` +- If in new spec → compare parameters and responses + +For each endpoint in new spec: +- If not in old spec → `endpoint_added` + +### Step 4: Compare Auth + +Compare `components/securitySchemes`: +- Added/removed schemes +- Changed scheme configurations + +Compare `security` requirements on paths + +### Step 5: Identify Breaking Changes + +Flag changes that may break existing clients: + +- `schema_removed` +- `field_removed` +- `endpoint_removed` +- `param_added` (required) +- `field_type_changed` +- `auth_scheme_removed` + +## Example Usage + +### Input: old-spec.yaml + +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.0.0 +components: + schemas: + User: + type: object + required: [id, email] + properties: + id: + type: string + email: + type: string + legacyId: + type: string +paths: + /users: + get: + operationId: listUsers + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' +``` + +### Input: new-spec.yaml + +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.1.0 +components: + schemas: + User: + type: object + required: [id, email] + properties: + id: + type: string + email: + type: string + phoneNumber: + type: string + Order: + type: object + required: [id, userId] + properties: + id: + type: string + userId: + type: string +paths: + /users: + get: + operationId: listUsers + parameters: + - name: search + in: query + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + /orders: + get: + operationId: listOrders + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Order' +``` + +### Output + +```json +{ + "summary": { + "schemas_added": 1, + "schemas_removed": 0, + "schemas_modified": 1, + "endpoints_added": 1, + "endpoints_removed": 0, + "endpoints_modified": 1, + "auth_changed": false + }, + "changes": [ + { + "type": "schema_added", + "path": "components/schemas/Order", + "details": { + "name": "Order", + "fields": [ + { "name": "id", "type": "string", "required": true }, + { "name": "userId", "type": "string", "required": true } + ] + } + }, + { + "type": "field_added", + "path": "components/schemas/User.properties.phoneNumber", + "details": { + "schema": "User", + "field": "phoneNumber", + "type": "string", + "required": false + } + }, + { + "type": "field_removed", + "path": "components/schemas/User.properties.legacyId", + "details": { + "schema": "User", + "field": "legacyId", + "type": "string" + } + }, + { + "type": "param_added", + "path": "paths./users.get.parameters.search", + "details": { + "endpoint": "GET /users", + "param": "search", + "in": "query", + "type": "string", + "required": false + } + }, + { + "type": "endpoint_added", + "path": "paths./orders.get", + "details": { + "method": "GET", + "path": "/orders", + "operationId": "listOrders", + "response": "Order[]" + } + } + ], + "breaking_changes": [ + { + "type": "field_removed", + "path": "components/schemas/User.properties.legacyId", + "severity": "medium", + "migration": "Remove references to User.legacyId in generated code" + } + ] +} +``` + +## Error Handling + +- If specs are invalid YAML/JSON, report parsing errors +- If specs are not valid OpenAPI 3.x, report validation errors +- If `$ref` cannot be resolved, report reference errors + +## See Also + +- [../skills/update-from-spec/SKILL.md](../skills/update-from-spec/SKILL.md) — Uses this agent's output +- [../references/change-taxonomy.md](../references/change-taxonomy.md) — Full change type definitions diff --git a/plugins/openapi-to-frontend/commands/openapi-to-frontend.md b/plugins/openapi-to-frontend/commands/openapi-to-frontend.md new file mode 100644 index 0000000..f05b7cc --- /dev/null +++ b/plugins/openapi-to-frontend/commands/openapi-to-frontend.md @@ -0,0 +1,166 @@ +# /openapi-to-frontend + +Generate or update a frontend codebase from an OpenAPI specification. + +## Usage + +``` +/openapi-to-frontend [old-spec] +``` + +## Arguments + +| Argument | Description | +|----------|-------------| +| `new-spec` | Path to the current/new OpenAPI specification (JSON or YAML) | +| `old-spec` | (Optional) Path to the previous OpenAPI spec for incremental updates | + +## Behavior + +**Initial generation** (no `old-spec` provided): +``` +/openapi-to-frontend openapi.json +``` +Generates a complete frontend codebase from scratch. + +**Incremental update** (both specs provided): +``` +/openapi-to-frontend new-spec.json old-spec.json +``` +Compares the specs and applies surgical updates to existing code. + +--- + +## What This Command Does + +### Initial Generation Mode + +When only `new-spec` is provided, run all generation phases: + +1. **Read the spec** at the provided path +2. **Generate TypeScript client** in `client/`: + - `types.ts` — interfaces for all schemas + - `api.ts` — API class with typed methods + - `auth.ts` — auth handlers based on securitySchemes + - `index.ts` — barrel export +3. **Generate React components** in `components/`: + - `/` directory per schema with Form, Detail, List components + - `shared/` — LoadingSpinner, ErrorDisplay, Pagination +4. **Generate React frontend** in `app/`: + - `App.tsx` with routing + - `pages/` — one page per resource + - `context/` — API client and auth providers + - `hooks/` — useResource, usePagination + - `utils/` — localStorage helpers +5. **Generate tests** in `tests/`: + - `setup/` — factories, type guards, config + - `unit/` — client and component tests + - `integration/` — flow tests + - `e2e/` — Playwright tests +6. **Generate CI workflows** in `.github/workflows/`: + - `ci.yml` — build and test on PR + - `deploy.yml` — deploy to GitHub Pages + - `publish.yml` — publish to npm +7. **Verify** the generated code compiles correctly + +### Incremental Update Mode + +When both `new-spec` and `old-spec` are provided: + +1. **Compare the specs** to identify changes: + - Schemas added/removed/modified + - Endpoints added/removed/modified + - Fields added/removed/type changed + - Auth schemes changed +2. **Apply surgical updates** to existing code: + - Add new interfaces for new schemas + - Update existing interfaces for changed fields + - Add/remove API methods for endpoint changes + - Update components to reflect schema changes + - Update tests and factories +3. **Do NOT regenerate from scratch** — preserve user customizations +4. **Verify** the updated code compiles and tests pass + +--- + +## Reference Skills + +This command orchestrates these skills (read them for detailed implementation guidance): + +| Phase | Skill | Description | +|-------|-------|-------------| +| Client | `skills/generate-client/SKILL.md` | TypeScript types and API class | +| Components | `skills/generate-components/SKILL.md` | React Form/Detail/List components | +| Frontend | `skills/generate-frontend/SKILL.md` | App shell, routing, context | +| Tests | `skills/generate-tests/SKILL.md` | Unit, integration, e2e tests | +| CI | `skills/generate-ci/SKILL.md` | GitHub Actions workflows | +| Updates | `skills/update-from-spec/SKILL.md` | Incremental change application | +| Diffing | `agents/spec-differ.md` | Spec comparison logic | + +## Reference Documents + +| Topic | Reference | +|-------|-----------| +| Auth patterns | `references/auth-patterns.md` | +| Naming rules | `references/naming-conventions.md` | +| Change types | `references/change-taxonomy.md` | + +--- + +## Output Structure + +``` +./ +├── client/ +│ ├── types.ts +│ ├── api.ts +│ ├── auth.ts +│ └── index.ts +├── components/ +│ ├── / +│ │ ├── Form.tsx +│ │ ├── Detail.tsx +│ │ ├── List.tsx +│ │ └── index.ts +│ ├── shared/ +│ └── index.ts +├── app/ +│ ├── App.tsx +│ ├── Layout.tsx +│ ├── main.tsx +│ ├── pages/ +│ ├── context/ +│ ├── hooks/ +│ └── utils/ +├── tests/ +│ ├── setup/ +│ ├── unit/ +│ ├── integration/ +│ └── e2e/ +└── .github/ + └── workflows/ + ├── ci.yml + ├── deploy.yml + └── publish.yml +``` + +## Examples + +### Generate from a local spec + +``` +/openapi-to-frontend ./api/openapi.yaml +``` + +### Update after spec changes + +``` +/openapi-to-frontend ./new-spec.json ./old-spec.json +``` + +### In CI/CD (GitHub Actions) + +```yaml +- name: Generate/update frontend + run: openhands --headless -t "/openapi-to-frontend new-spec.json old-spec.json" +``` diff --git a/plugins/openapi-to-frontend/hooks/hooks.json b/plugins/openapi-to-frontend/hooks/hooks.json new file mode 100644 index 0000000..89cfb38 --- /dev/null +++ b/plugins/openapi-to-frontend/hooks/hooks.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openhands.dev/schemas/hooks.json", + "version": "1.0.0", + "description": "Lifecycle hooks for openapi-to-frontend plugin (reserved for future use)", + "hooks": {} +} diff --git a/plugins/openapi-to-frontend/references/auth-patterns.md b/plugins/openapi-to-frontend/references/auth-patterns.md new file mode 100644 index 0000000..fa5ede7 --- /dev/null +++ b/plugins/openapi-to-frontend/references/auth-patterns.md @@ -0,0 +1,629 @@ +# Auth Patterns Reference + +This document describes how to handle different authentication methods found in OpenAPI specs. + +## Detecting Auth Type + +Read `components/securitySchemes` in the OpenAPI spec: + +```yaml +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + BearerAuth: + type: http + scheme: bearer + OAuth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://auth.example.com/authorize + tokenUrl: https://auth.example.com/token + scopes: + read: Read access + write: Write access +``` + +## Pattern 1: API Key + +### Detection + +```yaml +securitySchemes: + ApiKeyAuth: + type: apiKey + in: header # or "query" + name: X-API-Key # header/query param name +``` + +### Client Implementation + +```typescript +export interface ApiKeyAuth { + type: 'apiKey'; + key: string; + location: 'header' | 'query'; + name: string; +} + +export function attachApiKey( + headers: Record, + url: URL, + auth: ApiKeyAuth +): void { + if (auth.location === 'header') { + headers[auth.name] = auth.key; + } else { + url.searchParams.set(auth.name, auth.key); + } +} +``` + +### Client Constructor + +```typescript +const client = new ApiClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'apiKey', + key: 'your-api-key', + location: 'header', + name: 'X-API-Key', + }, +}); +``` + +### Frontend UI + +- Settings page with API key input +- Store key in localStorage +- No login flow required + +```tsx +function ApiKeySettings() { + const [key, setKey] = useState(localStorage.getItem('api_key') || ''); + + const handleSave = () => { + localStorage.setItem('api_key', key); + // Reinitialize API client + }; + + return ( +
+ + setKey(e.target.value)} + /> + +
+ ); +} +``` + +--- + +## Pattern 2: Bearer Token + +### Detection + +```yaml +securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT # optional +``` + +### Client Implementation + +```typescript +export interface BearerAuth { + type: 'bearer'; + token: string; +} + +export function attachBearer( + headers: Record, + auth: BearerAuth +): void { + headers['Authorization'] = `Bearer ${auth.token}`; +} +``` + +### Client Constructor + +```typescript +const client = new ApiClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'bearer', + token: 'your-jwt-token', + }, +}); +``` + +### Frontend UI + +Two approaches based on how tokens are obtained: + +#### Option A: Token Input + +If tokens are externally issued (e.g., service accounts): + +```tsx +function TokenSettings() { + const [token, setToken] = useState(''); + + return ( +
+ +