Sync the ap CLI with an OpenAPI spec
Keep the Go CLI under cli/ in sync with a REST API contract after its OpenAPI spec changes. This covers two jobs:
- Update existing commands when an endpoint they already call changes shape (new/renamed parameter, changed request/response body, new required field, removed operation).
- Scaffold new commands for endpoints newly added to a spec that have no CLI coverage yet.
The linking pin is always the HTTP path + method: a spec operation maps to exactly one CLI call site. This skill works by discovering that call site (or its absence) and reconciling it — it does not assume a fixed spec→command table, because that drifts.
Invoking this skill
There are two ways to drive it. Both run the same workflow — they differ only in scope.
Mode A — sync everything that changed in a spec (diff-driven)
Use when a spec file changed (e.g. a git pull or merge brought in new/edited endpoints) and you want the CLI reconciled to match. You do not list the endpoints — the skill reads the diff and classifies every changed operation itself (Step 1).
Trigger prompts:
- "The devportal spec changed in the last pull — sync the CLI commands."
- "/sync-cli-with-openapi platform-api/src/resources/openapi.yaml"
After a pull/merge the change is already committed, so a plain git diff shows nothing — the skill must diff against the pre-change state (Step 1 lists the exact commands). If the user knows the base, they can say so: "…diff it against HEAD@{1}" or "…against main".
Mode B — convert a named set of endpoints into commands (scope-driven)
Use when you want CLI coverage for specific endpoints — regardless of what the diff says. You name the operations; the skill jumps straight to scaffolding them (Step 2 → Step 3, ADDED path) and ignores the rest.
Trigger prompts:
- "Add
apcommands forPOST /devportal/v1/webhook-subscribersandGET /devportal/v1/webhook-subscribers/{subscriberId}." - "From platform-api/openapi.yaml, only wire the
llm-provider-templatesendpoints (list, get, copy) into theai-workspaceCLI."
The skill still reads each named operation's real schema from the spec so flags/payloads/validation are correct — it just skips the diff-classification breadth.
In both modes, if the user hasn't named the spec, infer it from the paths/family they mention and confirm. If a named endpoint has no consumer and no clear family (e.g. a non-LLM platform-api path), report it and ask before inventing a new command family.
Spec → CLI-family map
Three specs feed the CLI. Each is consumed by a distinct command family with its own client and conventions:
| Spec file | CLI family | Root dir | Client package |
|---|---|---|---|
gateway/gateway-controller/api/management-openapi.yaml |
ap gateway ... |
cli/src/cmd/gateway/ |
internal/gateway |
portals/api-portal/docs/devportal-openapi-spec-v1.yaml |
ap devportal ... |
cli/src/cmd/devportal/ |
internal/devportal |
platform-api/src/resources/openapi.yaml |
ap ai-workspace ... (partial — see note) |
cli/src/cmd/aiworkspace/ |
internal/aiworkspace |
Note on platform-api/src/resources/openapi.yaml: the ap ai-workspace (AI workspace) family consumes this spec, but only its LLM/MCP subset — /llm-providers, /llm-proxies, /mcp-proxies (and the llm-provider-templates endpoints). The CLI reaches them under the /api-proxy/api/v0.9/ prefix via path constants in cli/src/utils/constants.go (e.g. AIWorkspaceLLMProvidersPath = "/api-proxy/api/v0.9/llm-providers"), so the spec path is unversioned (/llm-providers) but the CLI call is versioned — match on the resource segment, not the whole path. The spec's many other resources (/organizations, /projects, /rest-apis, /gateways, /applications, /subscription-plans, /subscriptions, …) have no ap consumer today — the ap gateway commands look similar but actually track the separate gateway management spec. So for a platform-api change: if it touches an LLM/MCP endpoint, sync the ai-workspace family; otherwise discovery (Step 2) will find no consumer — say so and ask the user whether they want a new command family, rather than inventing edits.
Full anatomy of a command, the exact helpers per family, and copy-ready templates live in references/cli-command-anatomy.md. Read that file before writing or editing any command — do not work from memory.
Workflow
Step 1 — Identify what changed in the spec
Mode B (named endpoints): skip the diff — take the operations the user listed as the ADDED set and go to Step 2. Still open the spec file to read each operation's schema.
Mode A (diff-driven): get the diff of the spec. Pick the command by where the change lives:
git diff -- <spec-path> # uncommitted local edits (you're mid-editing the spec)
git diff HEAD@{1}..HEAD -- <spec-path> # change arrived via a pull (compare to pre-pull HEAD)
git diff main...HEAD -- <spec-path> # everything new on this branch vs main (PR review)
git diff <old-sha>..<new-sha> -- <spec-path>
If none show a diff, the spec is unchanged relative to that base — confirm the base with the user (a pull needs HEAD@{1} or the merge commit, not a plain git diff).
If the user hasn't said which spec, infer from the path they name and confirm it's one of the three above. From the diff, build a list of changed operations, each classified as:
- ADDED — a new
pathor a new method under an existing path → candidate for a new command. - MODIFIED — an existing operation whose parameters,
requestBody, responses, required fields, oroperationId/scopes changed → update the existing command. - REMOVED — an operation deleted → remove or deprecate the command.
For each operation record: HTTP method, full path (e.g. POST /devportal/v1/subscriptions/{subId}/change-plan), the changed parameters/fields, and the OAuth2 scope if the spec declares one.
Step 2 — Locate the CLI consumer (or confirm there is none)
For each operation, find the call site. The path segment is the search key. Match against how each family stores paths:
- Gateway family — paths are constants in
cli/src/utils/constants.go(e.g.GatewaySubscriptionPlansPath = "/subscription-plans",%sfor IDs). Grep for the literal path, then for the constant name:grep -rn "subscription-plans" cli/src/utils/constants.go grep -rn "GatewaySubscriptionPlansPath" cli/src/cmd/gateway/ - DevPortal family — org-scoped paths are built with
internaldevportal.OrgScopedPath(orgID, "<resource>"), which produces/o/{orgId}/devportal/{APIVersion}/<resource>(APIVersion=v1, ininternal/devportal/helpers.go). Only org-management endpoints use the raw/organizations...path. Grep for the resource segment:grep -rn "OrgScopedPath\|\"/organizations" cli/src/cmd/devportal/ - AI-workspace family — paths are constants in
cli/src/utils/constants.go(AIWorkspaceLLMProvidersPathetc., under/api-proxy/api/v0.9/), wrapped by helpers incli/src/internal/aiworkspace/helpers.go(ProviderPath(orgID),ProviderResourcePath(orgID,id),ProviderListPath(orgID,q), and theProxy*/MCPProxy*equivalents). Org/project scoping is a query parameter (?organizationId=/?projectId=), not a path segment. Match on the resource segment, since the spec path is unversioned:grep -rn "llm-providers\|llm-proxies\|mcp-proxies" cli/src/utils/constants.go grep -rn "AIWorkspaceLLMProvidersPath\|ProviderPath\|ProviderResourcePath" cli/src/cmd/aiworkspace/ cli/src/internal/aiworkspace/
Outcomes:
- Found → MODIFIED/REMOVED work targets this file. Note its dir (the cobra subcommand group).
- Not found for an ADDED op → this is new-command work. Identify the right subcommand group (existing dir like
cli/src/cmd/gateway/subscription/, or a new one). - Not found for a MODIFIED op → the endpoint has no CLI coverage; treat as ADDED or report it, don't silently skip.
Step 3 — Reconcile each operation
Open references/cli-command-anatomy.md and follow the template for the operation's family. Then, per classification:
MODIFIED — update an existing command
- New/renamed parameter → add/rename the cobra flag (use or add a
Flag*constant incli/src/utils/flags.go; wire withutils.AddStringFlag/AddBoolFlag), update the path/query building, and adjust validation (MarkFlagRequired, mutual-exclusion checks). - Changed request body → update the struct/
mapmarshalled into the payload and any--fileCR parsing (gateway.ParseResourceCR). - New required field → add validation that returns a clear
fmt.Errorfbefore the request; add a flag or CR field for it. - Changed response → update any local response struct and the print path (
PrintJSONResponseneeds no change for pass-through; typed decoders do). - Changed path/method → update the path constant/helper and the client verb (
Get/PostJSON/Put/Delete/PostYAML).
ADDED — scaffold a new command
- Pick the subcommand group dir. Reuse an existing one if the resource already has a group; otherwise create a new dir +
root.go(a<Resource>Cmdcobra group) and register it in the family root (cli/src/cmd/<family>/root.go). - Add the path constant (gateway) or use
OrgScopedPath(devportal). - Create
<verb>.gofrom the matching template in the reference: consts<Verb>CmdLiteral/<Verb>CmdExample, flag vars, thecobra.Command,init()wiring flags, andrun<Verb>Commanddoing validate → client → request → status-check → print. - Register the new command in the group's
root.goviaAddCommand. - Map the operation's OAuth2 scope/auth to the family's auth model (gateway selection flags vs devportal resolve+
--insecure) — the reference explains both.
REMOVED — retire a command
- Confirm with the user first (removing a command is a breaking CLI change). If confirmed, delete the
<verb>.go, drop itsAddCommandline, remove now-unused path constants, and delete its tests.
Step 4 — Keep the surrounding artifacts in sync
A command change is not done until these match:
- Flag constants — new flags need a
Flag*entry incli/src/utils/flags.go(don't hardcode flag strings). - Path constants — new/changed gateway endpoints need entries in
cli/src/utils/constants.go. - Registration — every new command/group is wired via
AddCommandup to the family root. - Docs (manually maintained, not generated) — update the relevant
docs/cli/<area>/README.mdand, if a top-level command/flag changed,docs/cli/reference.md. Match the existing entry format (CLI Command + example blocks). - Tests — mirror the neighboring
*_test.go/commands_test.goin the same dir. Add cases for new flags/validation; update expectations for changed payloads.
Step 5 — Build, test, and report
From cli/:
make build # builds (runs tests first)
make test # tests only
# or, faster while iterating, from cli/src: go build ./... && go test ./...
Fix compile/test failures. Then report: which operations were ADDED/MODIFIED/REMOVED, the files touched (commands, constants, flags, registration, docs, tests), the build/test result, and anything that needs manual follow-up (e.g. a REMOVED op awaiting confirmation, or a platform-api change with no CLI consumer).
Guardrails
- Never invent an endpoint. Every path/verb/parameter a command uses must exist in the spec diff or the current spec. If the spec is ambiguous, read the operation's schema in the spec file rather than guessing.
- Match the family's conventions exactly — the gateway, devportal, and ai-workspace families differ in client construction, path handling (constants vs
OrgScopedPathvs query-param scoping), verb vocabulary (createvspush), auth flags, and error wrapping. Copy the closest existing sibling command; don't blend patterns across families. - Treat CLI removals/renames as breaking. Confirm before removing or renaming a command, flag, or its short form.
- Keep the license header — every Go file starts with the Apache-2.0 WSO2 header block (copy it from any sibling file).
- Prefer editing a sibling-derived copy over authoring from scratch; the templates in the reference are a fallback, the real source of truth is the existing commands in the same group.