feat: improve openapi-to-ts cli and docs

pull/700/head
Travis Fischer 2025-03-24 22:39:15 +08:00
rodzic 125088dcd5
commit f04739e098
10 zmienionych plików z 177 dodań i 57 usunięć

Wyświetl plik

@ -13,7 +13,7 @@
"@agentic/weather": "workspace:*",
"@ai-sdk/openai": "catalog:",
"ai": "catalog:",
"exit-hook": "^4.0.0",
"exit-hook": "catalog:",
"openai": "catalog:",
"zod": "catalog:"
},

Wyświetl plik

@ -1,21 +0,0 @@
/* eslint-disable no-template-curly-in-string */
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { assert } from '@agentic/core'
import { generateTSFromOpenAPI } from '../src'
const dirname = path.dirname(fileURLToPath(import.meta.url))
// TODO: Add proper CLI handling
async function main() {
const pathToOpenApiSpec =
process.argv[2] ??
path.join(dirname, '..', 'fixtures', 'openapi', '3.0', 'notion.json')
assert(pathToOpenApiSpec, 'Missing path to OpenAPI spec')
await generateTSFromOpenAPI(pathToOpenApiSpec)
}
await main()

Wyświetl plik

@ -0,0 +1,72 @@
import { cli } from 'cleye'
import { gracefulExit } from 'exit-hook'
import { generateTSFromOpenAPI } from '../src'
async function main() {
const args = cli(
{
name: 'openapi-to-ts',
parameters: ['<openapi file path>'],
flags: {
debug: {
type: Boolean,
description: 'Enables verbose debug logging',
alias: 'v',
default: false
},
outputDir: {
type: String,
description: 'Path to the output directory (defaults to cwd)',
alias: 'o'
},
dryRun: {
type: Boolean,
description: 'Disables all side effects',
default: false
},
noPrettier: {
type: Boolean,
description: 'Disables prettier formatting',
default: false
},
noEslint: {
type: Boolean,
description: 'Disables eslint formatting',
default: false
}
}
},
() => {},
process.argv
)
const openapiFilePath = args._[2]!
if (!openapiFilePath) {
console.error('Missing required argument: <openapi file path>\n')
args.showHelp()
gracefulExit(1)
return
}
const output = await generateTSFromOpenAPI({
openapiFilePath,
outputDir: args.flags.outputDir || process.cwd(),
dryRun: args.flags.dryRun,
prettier: !args.flags.noPrettier,
eslint: !args.flags.noEslint
})
if (args.flags.dryRun) {
console.log(output)
}
}
try {
await main()
gracefulExit(0)
} catch (err) {
console.error(err)
gracefulExit(1)
}

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "@agentic/openapi-to-ts",
"version": "0.1.0",
"description": "TODO",
"description": "Generate an Agentic TypeScript client from an OpenAPI spec.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"license": "MIT",
"repository": {
@ -13,7 +13,7 @@
"types": "./dist/index.d.ts",
"sideEffects": false,
"bin": {
"openapi-to-ts": "./dist/generate-from-openapi.js"
"openapi-to-ts": "./dist/openapi-to-ts.js"
},
"files": [
"dist"
@ -30,15 +30,16 @@
"@agentic/core": "workspace:*",
"@apidevtools/swagger-parser": "^10.1.1",
"camelcase": "^8.0.0",
"cleye": "catalog:",
"decamelize": "^6.0.0",
"execa": "^9.5.2",
"exit-hook": "catalog:",
"json-schema-to-zod": "^2.6.0",
"openapi-types": "^12.1.3",
"zod": "catalog:"
},
"devDependencies": {
"@agentic/tsconfig": "workspace:*",
"ky": "catalog:"
"@agentic/tsconfig": "workspace:*"
},
"publishConfig": {
"access": "public"

Wyświetl plik

@ -5,7 +5,7 @@
</p>
<p align="center">
<em>TODO.</em>
<em>Generate an Agentic TypeScript client from an OpenAPI spec.</em>
</p>
<p align="center">
@ -19,15 +19,36 @@
**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.**
## Example Usage
```sh
# local development
tsx bin/openapi-to-ts.ts fixtures/openapi/3.0/notion.json -o fixtures/generated
# published version
npx @agentic/openapi-to-ts fixtures/openapi/3.0/notion.json -o fixtures/generated
# npm install -g version
npm install -g @agentic/openapi-to-ts
openapi-to-ts fixtures/openapi/3.0/notion.json -o fixtures/generated
```
Most OpenAPI specs should be supported, but I've made no attempt to maximize robustness. This tool is meant to generate Agentic TS clients which are 99% of the way there, but some tweaking may be necessary post-generation.
Some things you may want to tweak:
- simplifying the zod parameters accepted by `@aiFunction` input schemas since LLMs tend to do better with a few, key parameters
- removing unused API endpoints & associated types that you don't want to expose to LLMs
## TODO
- convert to https://github.com/readmeio/oas
- convert openapi parsing & utils to https://github.com/readmeio/oas
- support filters
- // match only the schema named `foo` and `GET` operation for the `/api/v1/foo` path
- include: '^(#/components/schemas/foo|#/paths/api/v1/foo/get)$',
- [ ] Convert HTML in descriptions to markdown.
- [ ] Properly format multiline function comments.
- [ ] Debug stripe schema issue.
- [ ] Convert HTML in descriptions to markdown
- [ ] Properly format multiline function comments
- [ ] Debug stripe schema issue
## License

Wyświetl plik

@ -1,7 +1,6 @@
/* eslint-disable no-template-curly-in-string */
import * as fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { IJsonSchema, OpenAPIV3 } from 'openapi-types'
import { assert } from '@agentic/core'
@ -16,7 +15,6 @@ import {
dereferenceFull,
getAndResolve,
getComponentDisplayName,
getComponentName,
getDescription,
getOperationParamsName,
getOperationResponseName,
@ -26,7 +24,6 @@ import {
prettify
} from './utils'
const dirname = path.dirname(fileURLToPath(import.meta.url))
const jsonContentType = 'application/json'
const multipartFormData = 'multipart/form-data'
@ -41,7 +38,21 @@ const httpMethods = [
'trace'
] as const
export async function generateTSFromOpenAPI(openapiFilePath: string) {
export type GenerateTSFromOpenAPIOptions = {
openapiFilePath: string
outputDir: string
dryRun?: boolean
prettier?: boolean
eslint?: boolean
}
export async function generateTSFromOpenAPI({
openapiFilePath,
outputDir,
dryRun = false,
prettier = true,
eslint = true
}: GenerateTSFromOpenAPIOptions): Promise<string> {
const parser = new SwaggerParser()
const spec = (await parser.bundle(openapiFilePath)) as OpenAPIV3.Document
// | OpenAPIV3_1.Document
@ -68,12 +79,8 @@ export async function generateTSFromOpenAPI(openapiFilePath: string) {
const nameUpperCase = nameSnakeCase.toUpperCase()
const clientName = `${name}Client`
const namespaceName = nameLowerCase
// const destFolder = path.join('packages', nameKebabCase)
// const destFolderSrc = path.join(destFolder, 'src')
const destFolder = path.join(dirname, '..', 'fixtures', 'generated')
const destFileClient = path.join(destFolder, `${nameKebabCase}-client.ts`)
const destFileClient = path.join(outputDir, `${nameKebabCase}-client.ts`)
const apiBaseUrl = spec.servers?.[0]?.url
const securitySchemes = spec.components?.securitySchemes
@ -543,14 +550,14 @@ export async function generateTSFromOpenAPI(openapiFilePath: string) {
componentSchemas[type] = schema
}
console.log(
'\ncomponents',
JSON.stringify(
sortedComponents.map((ref) => getComponentName(ref)),
null,
2
)
)
// console.log(
// '\ncomponents',
// JSON.stringify(
// sortedComponents.map((ref) => getComponentName(ref)),
// null,
// 2
// )
// )
// console.log(
// '\nmodels',
@ -603,9 +610,10 @@ import { z } from 'zod'`.trim()
.join('\n\n')
const description = getDescription(spec.info?.description)
const prettifyImpl = prettier ? prettify : (code: string) => code
const output = (
await prettify(
await prettifyImpl(
[
outputTypes,
`
@ -659,8 +667,16 @@ export class ${clientName} extends AIFunctionsProvider {
.replaceAll(/z\s*\.object\({}\)\s*\.merge\(([^)]*)\)/gm, '$1')
.replaceAll(/\/\*\*(\S.*\S)\*\//g, '/** $1 */')
console.log(output)
await fs.mkdir(destFolder, { recursive: true })
if (dryRun) {
return output
}
await fs.mkdir(outputDir, { recursive: true })
await fs.writeFile(destFileClient, output)
await execa('npx', ['eslint', '--fix', '--no-ignore', destFileClient])
if (eslint) {
await execa('npx', ['eslint', '--fix', '--no-ignore', destFileClient])
}
return output
}

Wyświetl plik

@ -1,6 +1,8 @@
/**
* This file is forked from: https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-jsonschema-parameters
*
* Several fixes have been applied.
*
* The original code is licensed under the MIT license.
*/

Wyświetl plik

@ -14,7 +14,7 @@ export default defineConfig([
dts: true
},
{
entry: ['bin/generate-from-openapi.ts'],
entry: ['bin/openapi-to-ts.ts'],
outDir: 'dist',
target: 'node18',
platform: 'node',

Wyświetl plik

@ -39,12 +39,18 @@ catalogs:
ai:
specifier: ^4.1.61
version: 4.1.61
cleye:
specifier: ^1.3.4
version: 1.3.4
dedent:
specifier: ^1.5.3
version: 1.5.3
delay:
specifier: ^6.0.0
version: 6.0.0
exit-hook:
specifier: ^4.0.0
version: 4.0.0
fast-xml-parser:
specifier: ^5.0.9
version: 5.0.9
@ -189,7 +195,7 @@ importers:
specifier: 'catalog:'
version: 4.1.61(react@18.3.1)(zod@3.24.2)
exit-hook:
specifier: ^4.0.0
specifier: 'catalog:'
version: 4.0.0
openai:
specifier: 'catalog:'
@ -891,12 +897,18 @@ importers:
camelcase:
specifier: ^8.0.0
version: 8.0.0
cleye:
specifier: 'catalog:'
version: 1.3.4
decamelize:
specifier: ^6.0.0
version: 6.0.0
execa:
specifier: ^9.5.2
version: 9.5.2
exit-hook:
specifier: 'catalog:'
version: 4.0.0
json-schema-to-zod:
specifier: ^2.6.0
version: 2.6.0
@ -910,9 +922,6 @@ importers:
'@agentic/tsconfig':
specifier: workspace:*
version: link:../tsconfig
ky:
specifier: 'catalog:'
version: 1.7.5
packages/people-data-labs:
dependencies:
@ -3873,6 +3882,9 @@ packages:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
cleye@1.3.4:
resolution: {integrity: sha512-Rd6M8ecBDtdYdPR22h6gG37lPqqJ3hSOaplaGwuGYey9xKmEElOvTgupqfyLSlISshroRpVhYjDtW3vwNUNBaQ==}
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@ -6494,6 +6506,9 @@ packages:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
terminal-columns@1.4.1:
resolution: {integrity: sha512-IKVL/itiMy947XWVv4IHV7a0KQXvKjj4ptbi7Ew9MPMcOLzkiQeyx3Gyvh62hKrfJ0RZc4M1nbhzjNM39Kyujw==}
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -6671,6 +6686,9 @@ packages:
resolution: {integrity: sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==}
engines: {node: '>=16'}
type-flag@3.0.0:
resolution: {integrity: sha512-3YaYwMseXCAhBB14RXW5cRQfJQlEknS6i4C8fCfeUdS3ihG9EdccdR9kt3vP73ZdeTGmPb4bZtkDn5XMIn1DLA==}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -10214,6 +10232,11 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
cleye@1.3.4:
dependencies:
terminal-columns: 1.4.1
type-flag: 3.0.0
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@ -13109,6 +13132,8 @@ snapshots:
mkdirp: 1.0.4
yallist: 4.0.0
terminal-columns@1.4.1: {}
text-table@0.2.0: {}
thenify-all@1.6.0:
@ -13266,6 +13291,8 @@ snapshots:
type-fest@4.37.0: {}
type-flag@3.0.0: {}
type-is@1.6.18:
dependencies:
media-typer: 0.3.0

Wyświetl plik

@ -7,8 +7,10 @@ catalog:
zod: ^3.24.2
zod-validation-error: ^3.4.0
openai-zod-to-json-schema: ^1.0.3
cleye: ^1.3.4
dedent: ^1.5.3
delay: ^6.0.0
exit-hook: ^4.0.0
jsonrepair: ^3.9.0
jsrsasign: ^10.9.0
mathjs: ^13.0.3