openapi-multitarget-codegen 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,394 @@
1
+ # openapi-multitarget-codegen
2
+
3
+ OpenAPI 스펙을 입력으로 받아 아래 3가지 타깃의 코드를 생성하는 **npm 배포용 CLI 패키지**입니다.
4
+
5
+ 1. **TypeScript Kubb**
6
+ 2. **Python 타입 코드**
7
+ 3. **LangGraph 도구 코드**
8
+
9
+ 이 패키지는 특정 프로젝트 구조를 전제하지 않습니다.
10
+
11
+ - 입력 스펙 경로는 `--input`으로 받습니다.
12
+ - 출력 루트 경로는 `--output`으로 받습니다.
13
+ - 생성 타깃은 `--target`으로 받습니다.
14
+
15
+ 즉, `openapi/openapi.yaml`, `generated/...` 같은 하드코딩된 경로에 묶이지 않고 사용할 수 있게 만드는 것이 목적입니다.
16
+
17
+ ---
18
+
19
+ ## 설치
20
+
21
+ ### dev dependency
22
+
23
+ ```bash
24
+ npm install -D openapi-multitarget-codegen
25
+ ```
26
+
27
+ 또는:
28
+
29
+ ```bash
30
+ pnpm add -D openapi-multitarget-codegen
31
+ ```
32
+
33
+ ### 전역 설치
34
+
35
+ ```bash
36
+ npm install -g openapi-multitarget-codegen
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 문서
42
+
43
+ - OpenAPI 작성 가이드: [`docs/openapi-authoring-guide.md`](docs/openapi-authoring-guide.md)
44
+ - 예제 스펙: [`openapi/openapi.example.yaml`](openapi/openapi.example.yaml)
45
+
46
+ ---
47
+
48
+ ## GitHub
49
+
50
+ - Repository: `https://github.com/kht2199/openapi-multitarget-codegen`
51
+ - Issues: `https://github.com/kht2199/openapi-multitarget-codegen/issues`
52
+
53
+ ---
54
+
55
+ ## 지원 타깃
56
+
57
+ ### 1) `kubb`
58
+ OpenAPI 스펙으로부터 TypeScript 타입 및 fetch client를 생성합니다.
59
+
60
+ ### 2) `python`
61
+ OpenAPI 스펙으로부터 Python 타입 코드를 생성합니다.
62
+ 기본 출력 모델 타입은 `pydantic_v2.BaseModel`입니다.
63
+
64
+ ### 3) `langgraph`
65
+ OpenAPI operation을 기준으로 LangGraph/LangChain StructuredTool 초안 코드를 생성합니다.
66
+
67
+ 중요:
68
+ - **LangGraph는 정식 지원 타깃입니다.**
69
+ - 하지만 구현이 특정 파일 경로나 특정 프로젝트 레이아웃에 하드코딩되어서는 안 됩니다.
70
+ - 이 CLI는 LangGraph 타깃도 다른 타깃과 동일하게 인자 기반으로 생성합니다.
71
+
72
+ ---
73
+
74
+ ## CLI 사용법
75
+
76
+ ```bash
77
+ openapi-multitarget-codegen generate --input <spec> --output <dir> --target <name>
78
+ ```
79
+
80
+ ### 공통 옵션
81
+
82
+ - `--input <path>`: OpenAPI 파일 경로 (`.yaml`, `.yml`, `.json`)
83
+ - `--output <dir>`: 생성 결과를 쓸 출력 루트 디렉터리
84
+ - `--target <name>`: 생성 타깃 (`kubb`, `python`, `langgraph`)
85
+ - 여러 개를 생성하려면 `--target`을 반복해서 넘깁니다.
86
+ - `--clean`: 출력 루트를 먼저 비웁니다.
87
+
88
+ ### 추가 옵션
89
+
90
+ - `--python-model-type <type>`
91
+ - 기본값: `pydantic_v2.BaseModel`
92
+ - `--kubb-output-dir <path>`
93
+ - 기본값: `<output>/kubb`
94
+ - 상대 경로는 `--output` 기준으로 해석됩니다.
95
+ - `--python-output-file <path>`
96
+ - 기본값: `<output>/python/models.py`
97
+ - 상대 경로는 `--output` 기준으로 해석됩니다.
98
+ - `--langgraph-output-dir <path>`
99
+ - 기본값: `<output>/langgraph`
100
+ - 상대 경로는 `--output` 기준으로 해석됩니다.
101
+ - `--langgraph-file <name>`
102
+ - 기본값: `langgraph_tools.py`
103
+ - `--kubb-client <client>`
104
+ - 기본값: `fetch`
105
+
106
+ ---
107
+
108
+ ## 빠른 OpenAPI 작성 템플릿
109
+
110
+ 아래 템플릿은 이 CLI로 Kubb / Python / LangGraph 생성을 시작할 때 최소한으로 복붙해 쓸 수 있는 예시입니다.
111
+ 더 자세한 기준은 [`docs/openapi-authoring-guide.md`](docs/openapi-authoring-guide.md)를 참고하세요.
112
+
113
+ ```yaml
114
+ openapi: 3.0.3
115
+ info:
116
+ title: Example API
117
+ version: 0.1.0
118
+ servers:
119
+ - url: https://api.example.com
120
+ paths:
121
+ /items:
122
+ get:
123
+ operationId: listItems
124
+ summary: List items
125
+ description: Retrieve the current item catalog.
126
+ responses:
127
+ '200':
128
+ description: Successful item list response
129
+ content:
130
+ application/json:
131
+ schema:
132
+ type: array
133
+ items:
134
+ $ref: '#/components/schemas/Item'
135
+ post:
136
+ operationId: createItem
137
+ summary: Create item
138
+ description: Create a new catalog item from the supplied payload.
139
+ requestBody:
140
+ required: true
141
+ description: Item payload to create.
142
+ content:
143
+ application/json:
144
+ schema:
145
+ $ref: '#/components/schemas/CreateItemRequest'
146
+ responses:
147
+ '200':
148
+ description: Item created successfully
149
+ content:
150
+ application/json:
151
+ schema:
152
+ $ref: '#/components/schemas/Item'
153
+ /items/{itemId}:
154
+ get:
155
+ operationId: getItem
156
+ summary: Get item by id
157
+ description: Retrieve one catalog item using its identifier.
158
+ parameters:
159
+ - in: path
160
+ name: itemId
161
+ required: true
162
+ description: Unique item identifier.
163
+ schema:
164
+ type: string
165
+ responses:
166
+ '200':
167
+ description: Matching item response
168
+ content:
169
+ application/json:
170
+ schema:
171
+ $ref: '#/components/schemas/Item'
172
+ components:
173
+ schemas:
174
+ Item:
175
+ type: object
176
+ description: Catalog item returned by the API.
177
+ required: [id, name]
178
+ properties:
179
+ id:
180
+ type: string
181
+ description: Stable item identifier.
182
+ name:
183
+ type: string
184
+ description: Display name of the item.
185
+ description:
186
+ type: string
187
+ description: Optional human-readable item details.
188
+ CreateItemRequest:
189
+ type: object
190
+ description: Payload for creating a catalog item.
191
+ required: [name]
192
+ properties:
193
+ name:
194
+ type: string
195
+ description: Name to assign to the new item.
196
+ description:
197
+ type: string
198
+ description: Optional explanatory text for the new item.
199
+ ```
200
+
201
+ 체크 포인트:
202
+ - 각 operation에 `operationId`, `summary`, `description`을 넣기
203
+ - response `description`을 비워두지 않기
204
+ - parameter/property마다 `description`을 넣기
205
+ - request/response schema는 가능하면 `components.schemas`로 분리하기
206
+
207
+ ---
208
+
209
+ ## 예시
210
+
211
+ ### Kubb만 생성
212
+
213
+ ```bash
214
+ openapi-multitarget-codegen generate \
215
+ --input ./specs/openapi.yaml \
216
+ --output ./generated \
217
+ --target kubb
218
+ ```
219
+
220
+ 예시 출력 위치 (`--output ./generated`를 준 경우):
221
+ - `./generated/kubb/...`
222
+
223
+ ### Python 타입만 생성
224
+
225
+ ```bash
226
+ openapi-multitarget-codegen generate \
227
+ --input ./specs/openapi.yaml \
228
+ --output ./generated \
229
+ --target python \
230
+ --python-model-type pydantic_v2.BaseModel
231
+ ```
232
+
233
+ 예시 출력 위치 (`--output ./generated`를 준 경우):
234
+ - `./generated/python/models.py`
235
+
236
+ ### 출력 경로까지 세분화해서 생성
237
+
238
+ ```bash
239
+ openapi-multitarget-codegen generate \
240
+ --input ./specs/openapi.yaml \
241
+ --output ./generated \
242
+ --target kubb \
243
+ --target python \
244
+ --target langgraph \
245
+ --kubb-output-dir ./sdk/ts \
246
+ --python-output-file ./sdk/python/api_models.py \
247
+ --langgraph-output-dir ./agent/tools \
248
+ --langgraph-file catalog_tools.py
249
+ ```
250
+
251
+ 위 예시는 다음처럼 생성됩니다.
252
+ - `./generated/sdk/ts/...`
253
+ - `./generated/sdk/python/api_models.py`
254
+ - `./generated/agent/tools/catalog_tools.py`
255
+
256
+ ### LangGraph 코드만 생성
257
+
258
+ ```bash
259
+ openapi-multitarget-codegen generate \
260
+ --input ./specs/openapi.yaml \
261
+ --output ./generated \
262
+ --target langgraph
263
+ ```
264
+
265
+ 예시 출력 위치 (`--output ./generated`를 준 경우):
266
+ - `./generated/langgraph/langgraph_tools.py`
267
+
268
+ ### 여러 타깃 동시 생성
269
+
270
+ ```bash
271
+ openapi-multitarget-codegen generate \
272
+ --input ./specs/openapi.yaml \
273
+ --output ./generated \
274
+ --target kubb \
275
+ --target python \
276
+ --target langgraph \
277
+ --clean
278
+ ```
279
+
280
+ 예시 출력 위치 (`--output ./generated`를 준 경우):
281
+ - `./generated/kubb/...`
282
+ - `./generated/python/models.py`
283
+ - `./generated/langgraph/langgraph_tools.py`
284
+
285
+ ---
286
+
287
+ ## 스펙 유효성 확인
288
+
289
+ ```bash
290
+ openapi-multitarget-codegen check-spec --input ./specs/openapi.yaml
291
+ ```
292
+
293
+ 이 명령은 다음을 검사합니다.
294
+ - 파일 존재 여부
295
+ - `openapi` 필드 존재 여부
296
+ - `paths` 객체 존재 여부
297
+
298
+ ---
299
+
300
+ ## 현재 출력 정책
301
+
302
+ 출력 루트는 `--output`으로 받고, 아무 추가 옵션이 없으면 기본값은 아래와 같습니다.
303
+
304
+ - `kubb` → `<output>/kubb`
305
+ - `python` → `<output>/python/models.py`
306
+ - `langgraph` → `<output>/langgraph/<langgraph-file>`
307
+
308
+ 필요하면 아래 옵션으로 타깃별 경로를 더 세분화할 수 있습니다.
309
+
310
+ - `--kubb-output-dir`
311
+ - `--python-output-file`
312
+ - `--langgraph-output-dir`
313
+
314
+ 상대 경로는 모두 `--output` 기준으로 해석되고, 절대 경로도 지원합니다.
315
+
316
+ ---
317
+
318
+ ## 현재 구현 특징
319
+
320
+ ### Kubb
321
+ - 임시 Kubb config를 생성해서 실행합니다.
322
+ - 입력 스펙 경로와 출력 경로를 동적으로 주입합니다.
323
+ - 기본 client는 `fetch`입니다.
324
+
325
+ ### Python
326
+ - `uvx --from datamodel-code-generator datamodel-codegen ...` 기반으로 실행합니다.
327
+ - 모델 타입은 `--python-model-type`으로 바꿀 수 있습니다.
328
+
329
+ ### LangGraph
330
+ - OpenAPI operation을 순회하면서 StructuredTool 초안 코드를 생성합니다.
331
+ - path/query/body를 분리해서 처리하는 기본 골격을 포함합니다.
332
+ - 출력 파일명은 `--langgraph-file`로 조절할 수 있습니다.
333
+
334
+ ---
335
+
336
+ ## 왜 이 패키지가 필요한가
337
+
338
+ 실무에서는 같은 OpenAPI 스펙을 기준으로 여러 언어/프레임워크용 코드를 동시에 만들고 싶을 때가 많습니다.
339
+
340
+ 예를 들면:
341
+ - 프론트엔드는 TypeScript client가 필요하고
342
+ - Python 서버/스크립트는 타입 모델이 필요하고
343
+ - 에이전트 계층은 LangGraph 도구 코드가 필요할 수 있습니다.
344
+
345
+ 이 패키지는 그런 상황에서 OpenAPI를 단일 source of truth로 두고, 여러 타깃을 하나의 CLI 인터페이스로 정리하는 데 목적이 있습니다.
346
+
347
+ ---
348
+
349
+ ## 예제 검증 스크립트
350
+
351
+ 저장소에는 예제 스펙 기반 검증 스크립트가 들어 있습니다.
352
+
353
+ ```bash
354
+ npm run check:example-spec
355
+ npm run generate:example:kubb
356
+ npm run generate:example:python
357
+ npm run generate:example:langgraph
358
+ npm run generate:example:all
359
+ ```
360
+
361
+ 예제 출력은 `./.tmp-output` 아래에 생성됩니다.
362
+
363
+ ---
364
+
365
+ ## 런타임 요구사항
366
+
367
+ - Node.js 20+
368
+ - npm 또는 pnpm
369
+ - Python 3.11+
370
+ - `uvx` 사용 가능 환경 (`python` 타깃용, 예: `brew install uv`)
371
+
372
+ LangGraph 생성 결과를 실제로 실행하려면 별도로 아래 Python 패키지가 필요할 수 있습니다.
373
+ - `httpx`
374
+ - `pydantic`
375
+ - `langchain-core`
376
+
377
+ ---
378
+
379
+ ## 아직 남아 있는 확장 포인트
380
+
381
+ 향후 아래 항목은 더 발전시킬 수 있습니다.
382
+
383
+ - Kubb의 React Query / Zod / MSW 확장 옵션
384
+ - Python 출력의 dataclass / TypedDict preset 단순화
385
+ - LangGraph naming / auth / filtering 전략 옵션화
386
+ - spec lint/report 전용 명령 추가
387
+ - dry-run / overwrite 정책 옵션
388
+ - npm publish용 release workflow 정리
389
+
390
+ ---
391
+
392
+ ## 한 줄 요약
393
+
394
+ **openapi-multitarget-codegen은 OpenAPI 스펙을 입력받아 Kubb, Python 타입, LangGraph 코드를 생성하는 인자 기반 npm CLI 패키지입니다.**
@@ -0,0 +1,85 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Example API
4
+ version: 0.1.0
5
+ servers:
6
+ - url: https://api.example.com
7
+ paths:
8
+ /items:
9
+ get:
10
+ operationId: listItems
11
+ summary: List items
12
+ description: Retrieve the current item catalog.
13
+ responses:
14
+ '200':
15
+ description: Successful item list response
16
+ content:
17
+ application/json:
18
+ schema:
19
+ type: array
20
+ items:
21
+ $ref: '#/components/schemas/Item'
22
+ post:
23
+ operationId: createItem
24
+ summary: Create item
25
+ description: Create a new catalog item from the supplied payload.
26
+ requestBody:
27
+ required: true
28
+ description: Item payload to create.
29
+ content:
30
+ application/json:
31
+ schema:
32
+ $ref: '#/components/schemas/CreateItemRequest'
33
+ responses:
34
+ '200':
35
+ description: Item created successfully
36
+ content:
37
+ application/json:
38
+ schema:
39
+ $ref: '#/components/schemas/Item'
40
+ /items/{itemId}:
41
+ get:
42
+ operationId: getItem
43
+ summary: Get item by id
44
+ description: Retrieve one catalog item using its identifier.
45
+ parameters:
46
+ - in: path
47
+ name: itemId
48
+ required: true
49
+ description: Unique item identifier.
50
+ schema:
51
+ type: string
52
+ responses:
53
+ '200':
54
+ description: Matching item response
55
+ content:
56
+ application/json:
57
+ schema:
58
+ $ref: '#/components/schemas/Item'
59
+ components:
60
+ schemas:
61
+ Item:
62
+ type: object
63
+ description: Catalog item returned by the API.
64
+ required: [id, name]
65
+ properties:
66
+ id:
67
+ type: string
68
+ description: Stable item identifier.
69
+ name:
70
+ type: string
71
+ description: Display name of the item.
72
+ description:
73
+ type: string
74
+ description: Optional human-readable item details.
75
+ CreateItemRequest:
76
+ type: object
77
+ description: Payload for creating a catalog item.
78
+ required: [name]
79
+ properties:
80
+ name:
81
+ type: string
82
+ description: Name to assign to the new item.
83
+ description:
84
+ type: string
85
+ description: Optional explanatory text for the new item.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "openapi-multitarget-codegen",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "OpenAPI-based multi-target code generation CLI for Kubb, Python types, and LangGraph tools",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kht2199/openapi-multitarget-codegen.git"
10
+ },
11
+ "homepage": "https://github.com/kht2199/openapi-multitarget-codegen#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/kht2199/openapi-multitarget-codegen/issues"
14
+ },
15
+ "bin": {
16
+ "openapi-multitarget-codegen": "src/cli.mjs"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "README.md",
21
+ "LICENSE",
22
+ "openapi/openapi.example.yaml"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "scripts": {
28
+ "clean": "rimraf .tmp-output",
29
+ "check:example-spec": "node ./src/cli.mjs check-spec --input ./openapi/openapi.example.yaml",
30
+ "generate:example:kubb": "node ./src/cli.mjs generate --input ./openapi/openapi.example.yaml --output ./.tmp-output --target kubb --clean",
31
+ "generate:example:python": "node ./src/cli.mjs generate --input ./openapi/openapi.example.yaml --output ./.tmp-output --target python --clean",
32
+ "generate:example:langgraph": "node ./src/cli.mjs generate --input ./openapi/openapi.example.yaml --output ./.tmp-output --target langgraph --clean",
33
+ "generate:example:all": "node ./src/cli.mjs generate --input ./openapi/openapi.example.yaml --output ./.tmp-output --target kubb --target python --target langgraph --clean"
34
+ },
35
+ "dependencies": {
36
+ "@kubb/cli": "^3.12.0",
37
+ "@kubb/core": "^3.12.0",
38
+ "@kubb/plugin-client": "^3.12.0",
39
+ "@kubb/plugin-oas": "^3.12.0",
40
+ "@kubb/plugin-ts": "^3.12.0",
41
+ "js-yaml": "^4.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "rimraf": "^6.0.1"
45
+ }
46
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,735 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import { spawnSync } from 'node:child_process'
7
+ import { createRequire } from 'node:module'
8
+ import { fileURLToPath } from 'node:url'
9
+ import yaml from 'js-yaml'
10
+
11
+ const require = createRequire(import.meta.url)
12
+ const CLI_FILE = fileURLToPath(import.meta.url)
13
+ const PACKAGE_ROOT = path.resolve(path.dirname(CLI_FILE), '..')
14
+
15
+ function main() {
16
+ const { command, options } = parseArgs(process.argv.slice(2))
17
+
18
+ try {
19
+ if (!command || command === 'help' || options.help) {
20
+ printHelp()
21
+ process.exit(0)
22
+ }
23
+
24
+ if (command === 'check-spec') {
25
+ const input = requireOption(options, 'input')
26
+ validateOpenApiFile(path.resolve(input))
27
+ console.log(`✓ valid OpenAPI spec: ${path.resolve(input)}`)
28
+ process.exit(0)
29
+ }
30
+
31
+ if (command !== 'generate') {
32
+ throw new Error(`Unsupported command: ${command}`)
33
+ }
34
+
35
+ const input = path.resolve(requireOption(options, 'input'))
36
+ const output = path.resolve(requireOption(options, 'output'))
37
+ const targets = normalizeTargets(options.target)
38
+ const pythonModelType = options['python-model-type'] || 'pydantic_v2.BaseModel'
39
+ const kubbOutputDir = options['kubb-output-dir']
40
+ const pythonOutputFile = options['python-output-file']
41
+ const langgraphOutputDir = options['langgraph-output-dir']
42
+ const langgraphFileName = options['langgraph-file'] || 'langgraph_tools.py'
43
+ const kubbClient = options['kubb-client'] || 'fetch'
44
+ const clean = Boolean(options.clean)
45
+
46
+ validateOpenApiFile(input)
47
+
48
+ if (clean) {
49
+ guardCleanPath(output)
50
+ }
51
+ if (clean && fs.existsSync(output)) {
52
+ fs.rmSync(output, { recursive: true, force: true })
53
+ }
54
+ fs.mkdirSync(output, { recursive: true })
55
+
56
+ for (const target of targets) {
57
+ if (target === 'kubb') {
58
+ runKubb({ input, outputRoot: output, outputDir: kubbOutputDir, client: kubbClient })
59
+ } else if (target === 'python') {
60
+ runPython({ input, outputRoot: output, outputFile: pythonOutputFile, modelType: pythonModelType })
61
+ } else if (target === 'langgraph') {
62
+ runLangGraph({ input, outputRoot: output, outputDir: langgraphOutputDir, fileName: langgraphFileName })
63
+ } else {
64
+ throw new Error(`Unsupported target: ${target}`)
65
+ }
66
+ }
67
+ } catch (error) {
68
+ console.error(`✗ ${error.message}`)
69
+ process.exit(1)
70
+ }
71
+ }
72
+
73
+ function parseArgs(argv) {
74
+ const args = [...argv]
75
+ const first = args[0]
76
+ const command = !first || first.startsWith('--') ? undefined : args.shift()
77
+ const options = {}
78
+
79
+ while (args.length > 0) {
80
+ const token = args.shift()
81
+ if (!token.startsWith('--')) {
82
+ continue
83
+ }
84
+
85
+ const key = token.slice(2)
86
+ if (key === 'help' || key === 'clean') {
87
+ options[key] = true
88
+ continue
89
+ }
90
+
91
+ const value = args.shift()
92
+ if (!value || value.startsWith('--')) {
93
+ throw new Error(`Missing value for --${key}`)
94
+ }
95
+
96
+ if (key === 'target') {
97
+ if (!options.target) options.target = []
98
+ options.target.push(value)
99
+ } else {
100
+ options[key] = value
101
+ }
102
+ }
103
+
104
+ return { command, options }
105
+ }
106
+
107
+ function normalizeTargets(targetValue) {
108
+ const values = Array.isArray(targetValue) ? targetValue : targetValue ? [targetValue] : []
109
+ if (values.length === 0) {
110
+ throw new Error('At least one --target is required (kubb | python | langgraph)')
111
+ }
112
+ return values
113
+ }
114
+
115
+ function requireOption(options, key) {
116
+ const value = options[key]
117
+ if (!value) {
118
+ throw new Error(`--${key} is required`)
119
+ }
120
+ return value
121
+ }
122
+
123
+ function validateOpenApiFile(specPath) {
124
+ if (!fs.existsSync(specPath)) {
125
+ throw new Error(`OpenAPI spec not found: ${specPath}`)
126
+ }
127
+
128
+ const text = fs.readFileSync(specPath, 'utf8')
129
+ let data
130
+ try {
131
+ data = yaml.load(text)
132
+ } catch {
133
+ data = JSON.parse(text)
134
+ }
135
+
136
+ if (!data || typeof data !== 'object') {
137
+ throw new Error(`Invalid OpenAPI spec: ${specPath}`)
138
+ }
139
+ if (!data.openapi) {
140
+ throw new Error(`Missing 'openapi' field: ${specPath}`)
141
+ }
142
+ if (!data.paths || typeof data.paths !== 'object') {
143
+ throw new Error(`Missing 'paths' object: ${specPath}`)
144
+ }
145
+
146
+ return data
147
+ }
148
+
149
+ function runKubb({ input, outputRoot, outputDir, client }) {
150
+ const kubbOutputDir = resolveOutputPath(outputRoot, outputDir, path.join('kubb'))
151
+ fs.mkdirSync(kubbOutputDir, { recursive: true })
152
+
153
+ const tempConfigPath = path.join(os.tmpdir(), `.openapi-multitarget-codegen-kubb-${Date.now()}.config.mjs`)
154
+ const configSource = `
155
+ import { createRequire } from 'node:module'
156
+
157
+ const require = createRequire(${JSON.stringify(path.join(PACKAGE_ROOT, 'package.json'))})
158
+ const { defineConfig } = require('@kubb/core')
159
+ const { pluginOas } = require('@kubb/plugin-oas')
160
+ const { pluginTs } = require('@kubb/plugin-ts')
161
+ const { pluginClient } = require('@kubb/plugin-client')
162
+
163
+ export default defineConfig({
164
+ root: '.',
165
+ input: {
166
+ path: ${JSON.stringify(input)},
167
+ },
168
+ output: {
169
+ path: ${JSON.stringify(kubbOutputDir)},
170
+ clean: true,
171
+ },
172
+ plugins: [
173
+ pluginOas(),
174
+ pluginTs({
175
+ output: {
176
+ path: './types',
177
+ },
178
+ }),
179
+ pluginClient({
180
+ output: {
181
+ path: './client',
182
+ },
183
+ client: ${JSON.stringify(client)},
184
+ }),
185
+ ],
186
+ })
187
+ `
188
+ fs.writeFileSync(tempConfigPath, configSource)
189
+
190
+ try {
191
+ const result = spawnSync(process.execPath, [resolveBinScript('@kubb/cli'), 'generate', '--config', tempConfigPath], {
192
+ stdio: 'inherit',
193
+ })
194
+
195
+ if (result.status !== 0) {
196
+ throw new Error('Kubb generation failed')
197
+ }
198
+ } finally {
199
+ cleanupTempFile(tempConfigPath)
200
+ }
201
+
202
+ console.log(`✓ generated kubb output -> ${kubbOutputDir}`)
203
+ }
204
+
205
+ function runPython({ input, outputRoot, outputFile, modelType }) {
206
+ ensureCommandAvailable('uvx', 'Python type generation requires uvx. Install uv first, for example: brew install uv')
207
+
208
+ const resolvedOutputFile = resolveOutputPath(outputRoot, outputFile, path.join('python', 'models.py'))
209
+ const pythonOutputDir = path.dirname(resolvedOutputFile)
210
+ fs.mkdirSync(pythonOutputDir, { recursive: true })
211
+
212
+ const result = spawnSync('uvx', [
213
+ '--from', 'datamodel-code-generator',
214
+ 'datamodel-codegen',
215
+ '--input', input,
216
+ '--output', resolvedOutputFile,
217
+ '--output-model-type', modelType,
218
+ ], {
219
+ stdio: 'inherit',
220
+ })
221
+
222
+ if (result.status !== 0) {
223
+ throw new Error('Python type generation failed')
224
+ }
225
+
226
+ console.log(`✓ generated python output -> ${resolvedOutputFile}`)
227
+ }
228
+
229
+ function runLangGraph({ input, outputRoot, outputDir, fileName }) {
230
+ const langgraphOutputDir = resolveOutputPath(outputRoot, outputDir, path.join('langgraph'))
231
+ const outputFile = path.join(langgraphOutputDir, fileName)
232
+ fs.mkdirSync(langgraphOutputDir, { recursive: true })
233
+
234
+ const doc = validateOpenApiFile(input)
235
+ const paths = doc.paths ?? {}
236
+ const components = doc.components ?? {}
237
+ const operations = []
238
+
239
+ for (const [route, methods] of Object.entries(paths)) {
240
+ for (const [method, op] of Object.entries(methods ?? {})) {
241
+ const lower = method.toLowerCase()
242
+ if (!['get', 'post', 'put', 'patch', 'delete'].includes(lower)) continue
243
+
244
+ const operationId = op?.operationId || `${lower}_${route.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '')}`
245
+ const parameters = [...(op?.parameters ?? [])].map((param) => simplifyParameter(resolveSchemaObject(param, components), components))
246
+ const requestBodySchema = resolveRequestBodySchema(op?.requestBody, components)
247
+ const requestBody = requestBodySchema ? simplifySchema(requestBodySchema, components) : null
248
+ const responseSummary = summarizePrimaryResponse(op?.responses, components)
249
+ const authRequired = hasSecurityRequirement(op?.security, doc.security)
250
+
251
+ operations.push({
252
+ method: lower,
253
+ route,
254
+ operationId,
255
+ summary: op?.summary || op?.description || `${lower.toUpperCase()} ${route}`,
256
+ description: op?.description || op?.summary || `${lower.toUpperCase()} ${route}`,
257
+ parameters,
258
+ requestBody,
259
+ responseSummary,
260
+ authRequired,
261
+ })
262
+ }
263
+ }
264
+
265
+ const operationDefs = toPythonLiteral(operations)
266
+ const sourceNote = path.basename(input)
267
+
268
+ const file = `"""
269
+ AUTO-GENERATED FILE.
270
+ Source: ${sourceNote}
271
+ Purpose: LangGraph/LangChain StructuredTool draft generated from OpenAPI.
272
+ """
273
+
274
+ from __future__ import annotations
275
+
276
+ import json
277
+ from typing import Any, Optional
278
+
279
+ import httpx
280
+ from langchain_core.tools import StructuredTool
281
+ from pydantic import BaseModel, Field, create_model
282
+
283
+ OPENAPI_TYPE_MAP: dict[str, Any] = {
284
+ "string": str,
285
+ "integer": int,
286
+ "number": float,
287
+ "boolean": bool,
288
+ "object": dict,
289
+ "array": list,
290
+ }
291
+
292
+ OPERATION_DEFS = ${operationDefs}
293
+
294
+
295
+ def _python_type(openapi_type: str | None) -> Any:
296
+ return OPENAPI_TYPE_MAP.get(openapi_type or "string", str)
297
+
298
+
299
+ def _build_args_schema(operation_id: str, parameters: list[dict[str, Any]], body_schema: dict[str, Any] | None) -> type[BaseModel] | None:
300
+ fields: dict[str, Any] = {}
301
+
302
+ for param in parameters:
303
+ name = param.get("name")
304
+ if not name:
305
+ continue
306
+ py_type = _python_type(param.get("type"))
307
+ param_location = param.get("in") or "parameter"
308
+ description = param.get("description") or f"{param_location} parameter: {name}"
309
+ required = bool(param.get("required", False))
310
+ if required:
311
+ fields[name] = (py_type, Field(description=description))
312
+ else:
313
+ fields[name] = (Optional[py_type], Field(default=None, description=description))
314
+
315
+ if body_schema:
316
+ properties = body_schema.get("properties", {}) or {}
317
+ required_props = set(body_schema.get("required", []) or [])
318
+ for prop_name, prop_schema in properties.items():
319
+ py_type = _python_type(prop_schema.get("type"))
320
+ description = prop_schema.get("description") or f"request body field: {prop_name}"
321
+ if prop_name in required_props:
322
+ fields[prop_name] = (py_type, Field(description=description))
323
+ else:
324
+ fields[prop_name] = (Optional[py_type], Field(default=None, description=description))
325
+
326
+ if not fields:
327
+ return create_model(f"{operation_id}_args")
328
+
329
+ return create_model(f"{operation_id}_args", **fields)
330
+
331
+
332
+ def _summarize_args(parameters: list[dict[str, Any]], body_schema: dict[str, Any] | None) -> list[str]:
333
+ parts: list[str] = []
334
+ path_names = [param["name"] for param in parameters if param.get("in") == "path" and param.get("name")]
335
+ query_names = [param["name"] for param in parameters if param.get("in") == "query" and param.get("name")]
336
+ body_names = list((body_schema or {}).get("properties", {}).keys())
337
+
338
+ if path_names:
339
+ parts.append("path: " + ", ".join(path_names))
340
+ if query_names:
341
+ parts.append("query: " + ", ".join(query_names))
342
+ if body_names:
343
+ parts.append("body: " + ", ".join(body_names))
344
+ return parts
345
+
346
+
347
+ def _operation_kind(method: str) -> str:
348
+ upper = method.upper()
349
+ if upper == "GET":
350
+ return "Read operation"
351
+ if upper == "POST":
352
+ return "Create operation"
353
+ if upper in {"PUT", "PATCH"}:
354
+ return "Update operation"
355
+ if upper == "DELETE":
356
+ return "Delete operation"
357
+ return "HTTP operation"
358
+
359
+
360
+
361
+ def _summarize_response(response_summary: dict[str, Any] | None) -> str:
362
+ if not response_summary:
363
+ return ""
364
+ status = response_summary.get("status")
365
+ schema_type = response_summary.get("type")
366
+ description = response_summary.get("description") or ""
367
+ parts = []
368
+ if status:
369
+ parts.append(f"Returns status {status}")
370
+ else:
371
+ parts.append("Returns a response")
372
+ if schema_type:
373
+ parts[-1] += f" with {schema_type} payload"
374
+ if description:
375
+ parts[-1] += f" ({description})"
376
+ return parts[0] + "."
377
+
378
+
379
+
380
+ def _clean_sentence(text: str) -> str:
381
+ return text.strip().rstrip(".?!")
382
+
383
+
384
+
385
+ def _build_tool_description(defn: dict[str, Any]) -> str:
386
+ operation_id = defn["operationId"]
387
+ route = defn["route"]
388
+ method = defn["method"].upper()
389
+ summary = _clean_sentence(defn.get("description") or defn.get("summary") or operation_id)
390
+ parameters = list(defn.get("parameters") or [])
391
+ body_schema = defn.get("requestBody")
392
+ response_summary = defn.get("responseSummary")
393
+ auth_required = bool(defn.get("authRequired"))
394
+ arg_summary = _summarize_args(parameters, body_schema)
395
+ parts = [f"{method} {route} - {summary}.", _operation_kind(method) + "."]
396
+ if arg_summary:
397
+ parts.append(f"Inputs: {'; '.join(arg_summary)}.")
398
+ if response_summary:
399
+ parts.append(_summarize_response(response_summary))
400
+ if auth_required:
401
+ parts.append("Authentication required.")
402
+ return " ".join(part.strip() for part in parts if part).strip()
403
+
404
+
405
+ def _make_tool(defn: dict[str, Any], base_url: str) -> StructuredTool:
406
+ operation_id = defn["operationId"]
407
+ description = _build_tool_description(defn)
408
+ route = defn["route"]
409
+ method = defn["method"]
410
+ parameters = list(defn.get("parameters") or [])
411
+ body_schema = defn.get("requestBody")
412
+
413
+ def _render_url(kwargs: dict[str, Any]) -> str:
414
+ rendered_path = route
415
+ for param in parameters:
416
+ if param.get("in") != "path":
417
+ continue
418
+ name = param.get("name")
419
+ value = kwargs.get(name)
420
+ if name and value is not None:
421
+ rendered_path = rendered_path.replace("{" + name + "}", str(value))
422
+ return base_url.rstrip("/") + rendered_path
423
+
424
+ def _query_params(kwargs: dict[str, Any]) -> dict[str, Any]:
425
+ return {
426
+ param["name"]: kwargs.get(param["name"])
427
+ for param in parameters
428
+ if param.get("in") == "query" and kwargs.get(param.get("name")) is not None
429
+ }
430
+
431
+ def _request_body(kwargs: dict[str, Any]) -> dict[str, Any]:
432
+ path_names = {param["name"] for param in parameters if param.get("in") == "path" and param.get("name")}
433
+ query_names = {param["name"] for param in parameters if param.get("in") == "query" and param.get("name")}
434
+ return {
435
+ key: value
436
+ for key, value in kwargs.items()
437
+ if value is not None and key not in path_names and key not in query_names
438
+ }
439
+
440
+ def _call_sync(**kwargs: Any) -> str:
441
+ url = _render_url(kwargs)
442
+ try:
443
+ with httpx.Client(timeout=30.0) as client:
444
+ if method == "get":
445
+ response = client.get(url, params=_query_params(kwargs))
446
+ elif method == "post":
447
+ response = client.post(url, params=_query_params(kwargs), json=_request_body(kwargs))
448
+ elif method == "put":
449
+ response = client.put(url, params=_query_params(kwargs), json=_request_body(kwargs))
450
+ elif method == "patch":
451
+ response = client.patch(url, params=_query_params(kwargs), json=_request_body(kwargs))
452
+ elif method == "delete":
453
+ response = client.delete(url, params=_query_params(kwargs))
454
+ else:
455
+ return f"Unsupported HTTP method: {method}"
456
+
457
+ if response.is_success:
458
+ return _format_response(response)
459
+ return f"Request failed with status {response.status_code}: {response.text}"
460
+ except Exception as exc: # noqa: BLE001
461
+ return f"Tool call error: {exc}"
462
+
463
+ async def _call_async(**kwargs: Any) -> str:
464
+ url = _render_url(kwargs)
465
+ try:
466
+ async with httpx.AsyncClient(timeout=30.0) as client:
467
+ if method == "get":
468
+ response = await client.get(url, params=_query_params(kwargs))
469
+ elif method == "post":
470
+ response = await client.post(url, params=_query_params(kwargs), json=_request_body(kwargs))
471
+ elif method == "put":
472
+ response = await client.put(url, params=_query_params(kwargs), json=_request_body(kwargs))
473
+ elif method == "patch":
474
+ response = await client.patch(url, params=_query_params(kwargs), json=_request_body(kwargs))
475
+ elif method == "delete":
476
+ response = await client.delete(url, params=_query_params(kwargs))
477
+ else:
478
+ return f"Unsupported HTTP method: {method}"
479
+
480
+ if response.is_success:
481
+ return _format_response(response)
482
+ return f"Request failed with status {response.status_code}: {response.text}"
483
+ except Exception as exc: # noqa: BLE001
484
+ return f"Tool call error: {exc}"
485
+
486
+ _call_sync.__doc__ = description
487
+ _call_async.__doc__ = description
488
+
489
+ args_schema = _build_args_schema(operation_id, parameters, body_schema)
490
+ return StructuredTool.from_function(
491
+ func=_call_sync,
492
+ coroutine=_call_async,
493
+ name=operation_id,
494
+ description=description,
495
+ return_direct=False,
496
+ args_schema=args_schema,
497
+ )
498
+
499
+
500
+ def _format_response(response: httpx.Response) -> str:
501
+ content_type = response.headers.get("content-type", "")
502
+ if "application/json" in content_type:
503
+ try:
504
+ return json.dumps(response.json(), ensure_ascii=False)
505
+ except Exception: # noqa: BLE001
506
+ return response.text
507
+ return response.text
508
+
509
+
510
+ def get_generated_tools(base_url: str) -> list[StructuredTool]:
511
+ return [_make_tool(defn, base_url=base_url) for defn in OPERATION_DEFS]
512
+
513
+
514
+ def list_generated_tool_defs() -> list[dict[str, Any]]:
515
+ return [
516
+ {
517
+ "name": defn["operationId"],
518
+ "method": defn["method"].upper(),
519
+ "path": defn["route"],
520
+ "description": _build_tool_description(defn),
521
+ }
522
+ for defn in OPERATION_DEFS
523
+ ]
524
+ `
525
+
526
+ fs.writeFileSync(outputFile, file)
527
+ console.log(`✓ generated langgraph output -> ${outputFile}`)
528
+ }
529
+
530
+ function resolveBinScript(moduleName) {
531
+ const packageJsonPath = require.resolve(`${moduleName}/package.json`)
532
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
533
+ const binField = packageJson.bin
534
+ const relativeBinPath = typeof binField === 'string'
535
+ ? binField
536
+ : typeof binField === 'object' && binField !== null
537
+ ? Object.values(binField)[0]
538
+ : null
539
+
540
+ if (!relativeBinPath) {
541
+ throw new Error(`Unable to resolve bin entry for ${moduleName}`)
542
+ }
543
+
544
+ return path.join(path.dirname(packageJsonPath), relativeBinPath)
545
+ }
546
+
547
+ function cleanupTempFile(filePath) {
548
+ try {
549
+ fs.unlinkSync(filePath)
550
+ } catch {
551
+ // ignore cleanup errors
552
+ }
553
+ }
554
+
555
+ function ensureCommandAvailable(command, installHint) {
556
+ const result = spawnSync(command, ['--version'], { stdio: 'ignore' })
557
+ if (result.error?.code === 'ENOENT') {
558
+ throw new Error(installHint)
559
+ }
560
+ }
561
+
562
+ function resolveOutputPath(outputRoot, candidatePath, defaultRelativePath) {
563
+ if (!candidatePath) {
564
+ return path.join(outputRoot, defaultRelativePath)
565
+ }
566
+ return path.isAbsolute(candidatePath)
567
+ ? path.resolve(candidatePath)
568
+ : path.resolve(outputRoot, candidatePath)
569
+ }
570
+
571
+ function toPythonLiteral(value, indent = 0) {
572
+ const spaces = ' '.repeat(indent)
573
+ const childSpaces = ' '.repeat(indent + 1)
574
+
575
+ if (value === null) return 'None'
576
+ if (value === true) return 'True'
577
+ if (value === false) return 'False'
578
+ if (typeof value === 'string') return JSON.stringify(value)
579
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'None'
580
+ if (Array.isArray(value)) {
581
+ if (value.length === 0) return '[]'
582
+ return `[\n${value.map((item) => `${childSpaces}${toPythonLiteral(item, indent + 1)}`).join(',\n')}\n${spaces}]`
583
+ }
584
+ if (value && typeof value === 'object') {
585
+ const entries = Object.entries(value)
586
+ if (entries.length === 0) return '{}'
587
+ return `{\n${entries.map(([key, item]) => `${childSpaces}${JSON.stringify(key)}: ${toPythonLiteral(item, indent + 1)}`).join(',\n')}\n${spaces}}`
588
+ }
589
+ return 'None'
590
+ }
591
+
592
+ function guardCleanPath(outputPath) {
593
+ const resolved = path.resolve(outputPath)
594
+ const currentWorkingDirectory = process.cwd()
595
+ const homeDirectory = process.env.HOME ? path.resolve(process.env.HOME) : null
596
+
597
+ if (resolved === path.parse(resolved).root) {
598
+ throw new Error('--clean cannot target the filesystem root')
599
+ }
600
+ if (resolved === currentWorkingDirectory) {
601
+ throw new Error('--clean cannot target the current working directory directly')
602
+ }
603
+ if (homeDirectory && resolved === homeDirectory) {
604
+ throw new Error('--clean cannot target the home directory')
605
+ }
606
+ }
607
+
608
+ function summarizePrimaryResponse(responses, components) {
609
+ if (!responses || typeof responses !== 'object') return null
610
+
611
+ const preferredStatuses = ['200', '201', '202', '204', 'default']
612
+ const responseEntries = Object.entries(responses)
613
+ const chosenEntry = preferredStatuses
614
+ .map((status) => responseEntries.find(([code]) => code === status))
615
+ .find(Boolean) ?? responseEntries[0]
616
+
617
+ if (!chosenEntry) return null
618
+
619
+ const [status, response] = chosenEntry
620
+ const responseObj = resolveSchemaObject(response, components) ?? response
621
+ const content = responseObj?.content ?? {}
622
+ const schema = content['application/json']?.schema
623
+ ?? content['application/*+json']?.schema
624
+ ?? content['*/*']?.schema
625
+ const schemaObj = schema ? resolveSchemaObject(schema, components) : null
626
+ const schemaType = schemaObj ? (schemaObj.type ?? inferSchemaType(schemaObj)) : null
627
+
628
+ return {
629
+ status,
630
+ description: responseObj?.description || '',
631
+ type: schemaType,
632
+ }
633
+ }
634
+
635
+ function hasSecurityRequirement(operationSecurity, globalSecurity) {
636
+ if (Array.isArray(operationSecurity)) {
637
+ return operationSecurity.length > 0
638
+ }
639
+ if (Array.isArray(globalSecurity)) {
640
+ return globalSecurity.length > 0
641
+ }
642
+ return false
643
+ }
644
+
645
+ function resolveRequestBodySchema(requestBody, components) {
646
+ if (!requestBody) return null
647
+ const bodyObj = resolveSchemaObject(requestBody, components)
648
+ const schema = bodyObj?.content?.['application/json']?.schema
649
+ ?? bodyObj?.content?.['application/*+json']?.schema
650
+ ?? bodyObj?.content?.['*/*']?.schema
651
+ if (!schema) return null
652
+ return resolveSchemaObject(schema, components)
653
+ }
654
+
655
+ function resolveSchemaObject(obj, components) {
656
+ if (!obj || typeof obj !== 'object') return obj
657
+ if (obj.$ref) {
658
+ const ref = obj.$ref
659
+ if (ref.startsWith('#/components/schemas/')) {
660
+ const key = ref.split('/').pop()
661
+ return resolveSchemaObject(components?.schemas?.[key], components)
662
+ }
663
+ if (ref.startsWith('#/components/requestBodies/')) {
664
+ const key = ref.split('/').pop()
665
+ return resolveSchemaObject(components?.requestBodies?.[key], components)
666
+ }
667
+ if (ref.startsWith('#/components/parameters/')) {
668
+ const key = ref.split('/').pop()
669
+ return resolveSchemaObject(components?.parameters?.[key], components)
670
+ }
671
+ }
672
+ return obj
673
+ }
674
+
675
+ function simplifyParameter(param, components) {
676
+ const p = resolveSchemaObject(param, components) ?? {}
677
+ const schema = resolveSchemaObject(p.schema ?? {}, components) ?? {}
678
+ return {
679
+ name: p.name,
680
+ in: p.in,
681
+ required: Boolean(p.required),
682
+ description: p.description ?? schema.description ?? '',
683
+ type: schema.type ?? inferSchemaType(schema),
684
+ }
685
+ }
686
+
687
+ function simplifySchema(schema, components) {
688
+ const resolved = resolveSchemaObject(schema, components) ?? {}
689
+ const properties = {}
690
+ for (const [name, propSchema] of Object.entries(resolved.properties ?? {})) {
691
+ const prop = resolveSchemaObject(propSchema, components) ?? {}
692
+ properties[name] = {
693
+ type: prop.type ?? inferSchemaType(prop),
694
+ description: prop.description ?? '',
695
+ }
696
+ }
697
+ return {
698
+ type: resolved.type ?? 'object',
699
+ required: resolved.required ?? [],
700
+ properties,
701
+ }
702
+ }
703
+
704
+ function inferSchemaType(schema) {
705
+ if (!schema || typeof schema !== 'object') return 'string'
706
+ if (schema.type) return schema.type
707
+ if (schema.properties) return 'object'
708
+ if (schema.items) return 'array'
709
+ return 'string'
710
+ }
711
+
712
+ function printHelp() {
713
+ console.log(`openapi-multitarget-codegen
714
+
715
+ Usage:
716
+ openapi-multitarget-codegen generate --input <spec> --output <dir> --target <kubb|python|langgraph> [--target ...]
717
+ openapi-multitarget-codegen check-spec --input <spec>
718
+
719
+ Options:
720
+ --input <path> OpenAPI file path (.yaml or .json)
721
+ --output <dir> Output root directory
722
+ --target <name> Generation target (repeatable)
723
+ --python-model-type <type> Python output model type (default: pydantic_v2.BaseModel)
724
+ --kubb-output-dir <path> Kubb output directory (default: <output>/kubb)
725
+ --python-output-file <path> Python output file (default: <output>/python/models.py)
726
+ --langgraph-output-dir <path> LangGraph output directory (default: <output>/langgraph)
727
+ --langgraph-file <name> LangGraph output filename (default: langgraph_tools.py)
728
+ --kubb-client <client> Kubb client type (default: fetch)
729
+ --clean Remove output root before generation
730
+ --help Show help
731
+ `)
732
+ }
733
+
734
+ main()
735
+