write-plugin
Write aptx-ft Plugin
Create custom JS plugins that extend the aptx-ft CLI with new commands and code generation capabilities.
When to Write a Plugin
| Scenario | Action |
|---|---|
| Need a custom code generator (e.g., Axios client, gRPC stub) | Write a plugin with command + custom rendering |
| Want to transform IR data into project-specific formats | Use ctx.getIr() to read OpenAPI IR |
| Need to add project-specific CLI commands to aptx-ft | Register commands via plugin |
| Built-in commands don't cover your use case | Extend with a plugin |
Command Name Mapping
The plugin defines command names with a colon separator (e.g. my:generate), but the CLI splits this into two arguments at runtime:
Plugin name field |
CLI invocation |
|---|---|
my:generate |
aptx-ft my generate |
tk:lint |
aptx-ft tk lint |
report:deps |
aptx-ft report deps |
The first part becomes a namespace subcommand, the second part becomes the actual command.
Plugin File Structure
A plugin is a CommonJS or ESM module exporting a Plugin object:
// my-plugin.js
const myPlugin = {
descriptor: {
name: 'my-plugin',
version: '1.0.0',
namespaceDescription: 'Custom code generation commands',
},
commands: [
{
name: 'my:generate',
summary: 'Generate custom output from OpenAPI',
options: [
{ flags: '-o, --output <dir>', description: 'Output directory', required: true },
{ flags: '--template <file>', description: 'Template file path' },
],
handler: async (ctx, args) => {
const inputPath = args.input; // global --input is available
const outputDir = args.output;
// Access parsed IR data
const ir = ctx.getIr(inputPath);
// Iterate endpoints
for (const ep of ir.endpoints) {
ctx.log(`Processing ${ep.method} ${ep.path} → ${ep.export_name}`);
// Your generation logic here
}
},
},
],
// Optional: runs once when plugin loads
init(ctx) {
ctx.log('my-plugin loaded');
},
};
module.exports = myPlugin;
module.exports.default = myPlugin;
TypeScript Plugin Development
Plugins can be written in TypeScript. Since the CLI loads .js files at runtime, compile your .ts plugin first:
# Compile the plugin
npx tsc my-plugin.ts --outDir ./dist --module commonjs --target ESNext
# Run the compiled plugin (colon in name becomes two CLI args)
pnpm exec aptx-ft -i ./openapi.json -p ./dist/my-plugin.js my generate -o ./output
The --plugin flag is global — place it before the subcommand. Each -p takes one path; repeat the flag for multiple plugins. The -i flag provides the OpenAPI file that ctx.getIr() reads.
TypeScript Type Definitions
All types are exported from @aptx/frontend-tk-core. Install the package for type checking:
npm install -D @aptx/frontend-tk-core
Core Plugin Types
// Main plugin interface
interface Plugin {
descriptor: PluginDescriptor;
commands: CommandDescriptor[];
renderers?: RendererDescriptor[];
init?(context: PluginContext): void | Promise<void>;
}
// Plugin metadata
interface PluginDescriptor {
name: string;
version: string;
namespaceDescription?: string;
}
// Context passed to handlers and renderers
interface PluginContext {
binding: typeof import('@aptx/frontend-tk-binding');
log: (msg: string) => void;
getIr(inputPath: string): GeneratorInput;
}
// Command handler function type
type CommandHandler = (
ctx: PluginContext,
args: Record<string, unknown>,
) => Promise<void> | void;
// Command definition
interface CommandDescriptor {
name: string;
summary: string;
description?: string;
options: OptionDescriptor[];
examples?: string[];
handler: CommandHandler;
requiresOpenApi?: boolean; // default: true
}
// CLI option definition (Commander.js style)
interface OptionDescriptor {
flags: string; // e.g. "-o, --output <dir>"
description: string;
defaultValue?: string | boolean;
required?: boolean;
}
// Code renderer definition
interface RendererDescriptor {
id: string;
render: (
ctx: PluginContext,
options: Record<string, unknown>,
) => Promise<void> | void;
}
IR (Intermediate Representation) Types
ctx.getIr(inputPath) returns GeneratorInput. See references/ir-types.md for full type definitions including GeneratorInput, EndpointItem, ProjectContext, ModelImportConfig, and ClientImportConfig.
Handling HTTP Request Parameters: Every endpoint may receive input through path parameters (path_fields), query parameters (query_fields), and request body (request_body_field). Your plugin must handle all three channels and their combinations. See references/http-params-guide.md for a complete guide covering:
- Path parameters (URL path interpolation)
- Query parameters (URL query string)
- Request body (JSON payload)
- All combinations (path+query, path+body, query+body, path+query+body)
- Detection logic and code generation patterns
Type Relationships
Plugin
├── descriptor: PluginDescriptor
├── commands: CommandDescriptor[]
│ ├── options: OptionDescriptor[]
│ └── handler: CommandHandler(ctx: PluginContext, args)
├── renderers?: RendererDescriptor[]
└── init?(ctx: PluginContext)
PluginContext
├── binding: Rust native binding
├── log: (msg) => void
└── getIr(path) -> GeneratorInput
├── project: ProjectContext
├── endpoints: EndpointItem[]
├── model_import: ModelImportConfig | null
├── client_import: ClientImportConfig | null
└── output_root: string | null
Common Patterns
Pattern 1: Custom Code Generator
Generate non-standard output from OpenAPI endpoints:
handler: async (ctx, args) => {
const ir = ctx.getIr(args.input);
const output = args.output;
const fs = await import('fs');
const path = await import('path');
// Filter endpoints by namespace
const endpoints = ir.endpoints.filter(
ep => ep.namespace.includes(args.namespace || '')
);
for (const ep of endpoints) {
const filename = `${ep.export_name}.ts`;
const content = generateCode(ep); // your logic
fs.writeFileSync(path.join(output, filename), content);
ctx.log(`Generated ${filename}`);
}
},
Pattern 2: Endpoint Analysis / Reporting
Read IR data and produce a report without generating files:
handler: async (ctx, args) => {
const ir = ctx.getIr(args.input);
ctx.log(`API: ${ir.project.package_name}`);
ctx.log(`Endpoints: ${ir.endpoints.length}`);
// Group by method
const byMethod = {};
for (const ep of ir.endpoints) {
(byMethod[ep.method] ??= []).push(ep);
}
for (const [method, eps] of Object.entries(byMethod)) {
ctx.log(` ${method.toUpperCase()}: ${eps.length}`);
}
// Find deprecated
const deprecated = ir.endpoints.filter(ep => ep.deprecated);
if (deprecated.length > 0) {
ctx.log(`\nDeprecated endpoints:`);
deprecated.forEach(ep => ctx.log(` - ${ep.method} ${ep.path}`));
}
},
Pattern 3: Multi-command Plugin
A plugin with several related commands:
const plugin = {
descriptor: {
name: 'my-toolkit',
version: '1.0.0',
namespaceDescription: 'Custom development toolkit',
},
commands: [
{
name: 'tk:lint',
summary: 'Lint generated code',
options: [
{ flags: '--fix', description: 'Auto-fix issues', defaultValue: false },
],
handler: async (ctx, args) => { /* ... */ },
},
{
name: 'tk:stats',
summary: 'Show API statistics',
options: [],
handler: async (ctx, args) => { /* ... */ },
},
{
name: 'tk:convert',
summary: 'Convert output to another format',
options: [
{ flags: '--format <type>', description: 'Target format', required: true },
],
handler: async (ctx, args) => { /* ... */ },
},
],
};
Rules
- Command names in the plugin use
namespace:commandformat (colon separator) - CLI invocation splits the colon into two args:
my:generate→aptx-ft my generate ctx.getIr()throws on invalid file path or malformed OpenAPI — handle errors in your handler- Export both
module.exportsandmodule.exports.defaultfor compatibility - Plugin files loaded at runtime must be
.jsor.mjs— compile.tsplugins first - Binary formats (
.node,.dll,.so,.dylib) are skipped - Options array can be empty
[]if the command takes no flags - Use
@aptx/frontend-tk-coretypes for TypeScript plugins — install as dev dependency requiresOpenApi: falsefor commands that don't need OpenAPI input (default istrue)argsin handler isRecord<string, unknown>— cast to specific types as neededctx.bindingprovides access to Rust native code viabinding.runCli({...})
HTTP Parameter Handling Rules:
- Always check all three parameter channels for every endpoint:
path_fields,query_fields,request_body_field - Never assume an endpoint uses only one parameter channel — real APIs commonly combine path + query, path + body, or all three
- Path params: Interpolate into
ep.pathtemplate (replace{paramName}placeholders). Path params are always required - Query params: Append to URL as
?key=value&key2=value2. Handle optional params by omitting null/undefined values - Request body: Send as JSON payload via
ep.request_body_field(a single field name, not an array). Only present when the endpoint has a body - Detection pattern: Check
ep.path_fields.length > 0,ep.query_fields.length > 0, and!!ep.request_body_fieldto determine which channels are active - Order: Build URL with path interpolation first, then append query string, then attach body to request options
- See references/http-params-guide.md for detailed examples of every combination
Boundaries
- This skill creates plugin files only — it does not modify the aptx-ft CLI itself
- Plugins are loaded at runtime via
--pluginand don't require rebuilding aptx-ft - This skill does not cover Rust/NAPI plugin development — only JS plugins
- For standard code generation (models, react-query, vue-query), use existing skills instead
Related Skills
- generate-artifacts: Standard artifact generation (models + request clients)
- generate-models: Model-only generation
- download-openapi: Fetch OpenAPI spec from URL
More from haibaraaiaptx/frontend-openapi-skills
download-openapi
Download remote OpenAPI/Swagger JSON specification from a URL to local file using aptx-ft CLI. TRIGGER when user mentions: (1) fetch/pull/download swagger or openapi from URL, (2) save API spec to openapi.json locally, (3) get API documentation from server, or (4) prepare local input for code generation. DO NOT TRIGGER when: generating code/types from local file, reading existing openapi.json, downloading non-OpenAPI files, or authentication is required.
25generate-artifacts
Generate frontend artifacts from OpenAPI via aptx-ft, including models and request clients. Use when user wants: (1) to generate API code from OpenAPI/Swagger, (2) React Query hooks from API spec, (3) Vue Query composables from API spec, (4) function-based API clients, (5) a standard flow for frontend projects without framework-specific business adaptation, (6) track generated files with manifest, (7) preview changes before generation, or (8) update barrel files automatically.
24generate-models
Generate TypeScript interfaces and enums from OpenAPI schemas using aptx-ft CLI. Use when user asks to: (1) generate types/models from OpenAPI/Swagger, (2) create TypeScript interfaces from API schema, (3) extract type definitions from openapi.json, (4) generate selective models with --name filter, (5) preserve translated enum values, (6) track generated files with manifest, (7) preview changes before generation, or (8) update barrel files automatically. Do NOT use for full artifact generation with request layer or Material UI enum adaptation.
23adapt-materal-enums
Materal-specific enum adaptation workflow: fetch enum values from provider API, let LLM fill suggested_name, then apply patch with aptx-ft. Use ONLY when: (1) user mentions Materal framework, (2) Materal naming rules are required, or (3) adapting Materal enum semantics. Do NOT use for generic OpenAPI projects.
23generate-barrels
Generate barrel index.ts files for TypeScript projects. Use when user mentions: (1) barrel files, (2) index.ts exports, (3) re-export files, (4) simplify import paths, (5) create index files for directory, or (6) generate export aggregators.
18download-swagger-file
从 URL 下载 OpenAPI 3.x JSON 规范文件。用于:(1)从远程服务器获取 API 规范,(2)将 OpenAPI JSON 保存到本地,(3)为 TypeScript 模型生成准备规范。
6