@sdk-it/cli 0.46.0 → 0.46.2
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/README.md +76 -71
- package/dist/bin.js +77 -27
- package/dist/bin.js.map +3 -3
- package/dist/lib/cli.d.ts.map +1 -1
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -1,110 +1,115 @@
|
|
|
1
1
|
# @sdk-it/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate type-safe client SDKs from OpenAPI specifications.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Run the CLI
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
# Install globally
|
|
9
|
-
npm install -g @sdk-it/cli
|
|
7
|
+
Use the latest CLI without installing it globally:
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
npx @sdk-it/cli
|
|
9
|
+
```bash
|
|
10
|
+
npx @sdk-it/cli@latest --help
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Generate SDKs from OpenAPI specifications:
|
|
18
|
-
|
|
19
|
-
### Basic Command Structure
|
|
13
|
+
To pin the CLI in a project:
|
|
20
14
|
|
|
21
15
|
```bash
|
|
22
|
-
|
|
16
|
+
npm install --save-dev @sdk-it/cli
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| Option | Alias | Description | Default |
|
|
28
|
-
| ------------- | ----- | -------------------------------------------------------- | ---------- |
|
|
29
|
-
| `--spec` | `-s` | Path to OpenAPI specification file (local or remote URL) | _Required_ |
|
|
30
|
-
| `--output` | `-o` | Output directory for the generated SDK | _Required_ |
|
|
31
|
-
| `--name` | `-n` | Name of the generated client | `Client` |
|
|
32
|
-
| `--mode` | `-m` | Generation mode: `full` or `minimal` | `minimal` |
|
|
33
|
-
| `--formatter` | | Formatter command to run on generated code | |
|
|
19
|
+
## Generate a TypeScript SDK
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
Inside an existing TypeScript project, install the generated client's runtime
|
|
22
|
+
dependencies:
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
```bash
|
|
25
|
+
npm install zod fast-content-type-parse
|
|
26
|
+
```
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
Generate the client:
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
```bash
|
|
31
|
+
npx @sdk-it/cli@latest generate typescript \
|
|
32
|
+
--spec ./openapi.json \
|
|
33
|
+
--output ./src/generated/api \
|
|
34
|
+
--name MyApi \
|
|
35
|
+
--mode minimal
|
|
36
|
+
```
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
Use it:
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
```typescript
|
|
41
|
+
import { MyApi } from './src/generated/api/index.ts';
|
|
48
42
|
|
|
49
|
-
|
|
43
|
+
const client = new MyApi({
|
|
44
|
+
baseUrl: 'https://api.example.com',
|
|
45
|
+
});
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
const users = await client.request('GET /users', {});
|
|
48
|
+
console.log(users);
|
|
49
|
+
```
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
`request` returns the unwrapped response data. Invalid inputs and non-successful
|
|
52
|
+
HTTP responses throw typed errors exported by the generated SDK.
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
## TypeScript options
|
|
57
55
|
|
|
58
|
-
```
|
|
59
|
-
npx sdk-it
|
|
56
|
+
```text
|
|
57
|
+
npx @sdk-it/cli@latest generate typescript [options]
|
|
60
58
|
```
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
| Option | Description | Default |
|
|
61
|
+
| -------------------------- | ------------------------------------------------------------ | --------- |
|
|
62
|
+
| `--spec`, `-s` | Local path or remote URL to an OpenAPI JSON or YAML document | Required |
|
|
63
|
+
| `--output`, `-o` | Output directory | Required |
|
|
64
|
+
| `--name`, `-n` | Generated client class name | `Client` |
|
|
65
|
+
| `--mode`, `-m` | `minimal` source files or a `full` standalone project | `minimal` |
|
|
66
|
+
| `--useTsExtension [value]` | Include `.ts` in generated imports | `true` |
|
|
67
|
+
| `--formatter <command>` | Command used to format the generated source directory | |
|
|
68
|
+
| `--no-default-formatter` | Skip the default Prettier formatter | |
|
|
69
|
+
| `--readme false` | Skip the generated API README | |
|
|
70
|
+
| `--pagination <config>` | Configure pagination, such as `false` or `guess=false` | `true` |
|
|
71
|
+
| `--no-install` | Skip dependency installation in `full` mode | |
|
|
72
|
+
| `--verbose`, `-v` | Show generator and installation output | `false` |
|
|
73
|
+
|
|
74
|
+
`minimal` mode writes client source files directly to `--output`. `full` mode
|
|
75
|
+
writes source files under `<output>/src`, adds `package.json` and `tsconfig.json`,
|
|
76
|
+
and installs its runtime dependencies unless `--no-install` is passed.
|
|
77
|
+
|
|
78
|
+
For a custom formatter, include the generated path explicitly:
|
|
63
79
|
|
|
64
80
|
```bash
|
|
65
|
-
npx sdk-it
|
|
81
|
+
npx @sdk-it/cli@latest generate typescript \
|
|
82
|
+
--spec ./openapi.json \
|
|
83
|
+
--output ./src/generated/api \
|
|
84
|
+
--formatter "prettier ./src/generated/api --write"
|
|
66
85
|
```
|
|
67
86
|
|
|
68
|
-
|
|
87
|
+
## Remote specification example
|
|
69
88
|
|
|
70
89
|
```bash
|
|
71
|
-
npx sdk-it
|
|
90
|
+
npx @sdk-it/cli@latest generate typescript \
|
|
91
|
+
--spec https://raw.githubusercontent.com/MaximilianKoestler/hcloud-openapi/refs/heads/main/openapi/hcloud.json \
|
|
92
|
+
--output ./src/generated/hetzner \
|
|
93
|
+
--name Hetzner \
|
|
94
|
+
--mode minimal
|
|
72
95
|
```
|
|
73
96
|
|
|
74
|
-
|
|
97
|
+
```typescript
|
|
98
|
+
import { Hetzner } from './src/generated/hetzner/index.ts';
|
|
75
99
|
|
|
76
|
-
|
|
100
|
+
const hetzner = new Hetzner({
|
|
101
|
+
token: process.env.HETZNER_API_TOKEN,
|
|
102
|
+
});
|
|
77
103
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
npx sdk-it -s https://raw.githubusercontent.com/MaximilianKoestler/hcloud-openapi/refs/heads/main/openapi/hcloud.json -o ./client --formatter "prettier $SDK_IT_OUTPUT --write"
|
|
104
|
+
const result = await hetzner.request('GET /servers', {});
|
|
105
|
+
console.log(result.servers);
|
|
81
106
|
```
|
|
82
107
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
1. Downloads the OpenAPI specification from the Hetzner Cloud documentation
|
|
86
|
-
2. Generates a type-safe TypeScript SDK in the `./client` directory
|
|
87
|
-
3. Runs Prettier on the generated code for consistent formatting
|
|
108
|
+
## Other generators
|
|
88
109
|
|
|
89
|
-
|
|
110
|
+
The same `generate` command also exposes the Dart, Python, API reference, and
|
|
111
|
+
README generators:
|
|
90
112
|
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Create a client instance with your API token
|
|
95
|
-
const client = new Client({
|
|
96
|
-
baseUrl: 'https://api.hetzner.cloud/v1',
|
|
97
|
-
headers: {
|
|
98
|
-
Authorization: 'Bearer your_api_token',
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Call API methods with type safety
|
|
103
|
-
const [servers, error] = await client.request('GET /servers', {});
|
|
104
|
-
|
|
105
|
-
if (error) {
|
|
106
|
-
console.error('Error fetching servers:', error);
|
|
107
|
-
} else {
|
|
108
|
-
console.log('Servers:', servers);
|
|
109
|
-
}
|
|
113
|
+
```bash
|
|
114
|
+
npx @sdk-it/cli@latest generate --help
|
|
110
115
|
```
|
package/dist/bin.js
CHANGED
|
@@ -816,13 +816,13 @@ Configuring ${generator} generator:`);
|
|
|
816
816
|
" npx @sdk-it/cli # Regenerate SDKs after API changes"
|
|
817
817
|
);
|
|
818
818
|
console.log(
|
|
819
|
-
" npx @sdk-it/cli typescript --help # See TypeScript-specific options"
|
|
819
|
+
" npx @sdk-it/cli generate typescript --help # See TypeScript-specific options"
|
|
820
820
|
);
|
|
821
821
|
console.log(
|
|
822
|
-
" npx @sdk-it/cli python --help # See Python-specific options"
|
|
822
|
+
" npx @sdk-it/cli generate python --help # See Python-specific options"
|
|
823
823
|
);
|
|
824
824
|
console.log(
|
|
825
|
-
" npx @sdk-it/cli dart --help # See Dart-specific options"
|
|
825
|
+
" npx @sdk-it/cli generate dart --help # See Dart-specific options"
|
|
826
826
|
);
|
|
827
827
|
console.log("\n\u{1F4A1} Tips:");
|
|
828
828
|
console.log(
|
|
@@ -959,7 +959,7 @@ import { execFile as execFile2, execSync as execSync2 } from "node:child_process
|
|
|
959
959
|
import { readdir as readdir2 } from "node:fs/promises";
|
|
960
960
|
import { join as join7 } from "node:path";
|
|
961
961
|
import { snakecase as snakecase2 } from "stringcase";
|
|
962
|
-
import { isEmpty, isRef as isRef2, pascalcase as pascalcase2 } from "@sdk-it/core";
|
|
962
|
+
import { followRef as followRef2, isEmpty as isEmpty2, isRef as isRef2, pascalcase as pascalcase2 } from "@sdk-it/core";
|
|
963
963
|
import {
|
|
964
964
|
createWriterProxy,
|
|
965
965
|
writeFiles
|
|
@@ -973,7 +973,14 @@ import {
|
|
|
973
973
|
toIR
|
|
974
974
|
} from "@sdk-it/spec";
|
|
975
975
|
import { snakecase } from "stringcase";
|
|
976
|
-
import {
|
|
976
|
+
import {
|
|
977
|
+
followRef,
|
|
978
|
+
isEmpty,
|
|
979
|
+
isRef,
|
|
980
|
+
notRef,
|
|
981
|
+
parseRef,
|
|
982
|
+
pascalcase
|
|
983
|
+
} from "@sdk-it/core";
|
|
977
984
|
import { isPrimitiveSchema } from "@sdk-it/spec";
|
|
978
985
|
var dispatcher_default = `"""HTTP dispatcher for making API requests."""
|
|
979
986
|
|
|
@@ -2272,7 +2279,18 @@ var PythonEmitter = class {
|
|
|
2272
2279
|
}
|
|
2273
2280
|
return fieldName;
|
|
2274
2281
|
}
|
|
2275
|
-
#
|
|
2282
|
+
#isBottom(schema) {
|
|
2283
|
+
if (!schema) {
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
const resolved = isRef(schema) ? followRef(this.#spec, schema.$ref) : schema;
|
|
2287
|
+
return !!resolved.not && isEmpty(resolved.not);
|
|
2288
|
+
}
|
|
2289
|
+
#ref(ref, context = {}) {
|
|
2290
|
+
const schema = followRef(this.#spec, ref.$ref);
|
|
2291
|
+
if (this.#isBottom(schema) || schema.type === "array" && this.#isBottom(schema.items)) {
|
|
2292
|
+
return this.handle(schema, context);
|
|
2293
|
+
}
|
|
2276
2294
|
const cacheKey = ref.$ref;
|
|
2277
2295
|
const cached = this.#typeCache.get(cacheKey);
|
|
2278
2296
|
if (cached) {
|
|
@@ -2332,27 +2350,25 @@ var PythonEmitter = class {
|
|
|
2332
2350
|
}
|
|
2333
2351
|
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
2334
2352
|
if (isRef(propSchema)) {
|
|
2335
|
-
this.#ref(propSchema);
|
|
2336
|
-
const
|
|
2337
|
-
const refName = refInfo.model;
|
|
2338
|
-
const pythonType = pascalcase(refName);
|
|
2353
|
+
const result = this.#ref(propSchema, context);
|
|
2354
|
+
const pythonType = result.type || "Any";
|
|
2339
2355
|
const fieldName = this.#formatFieldName(propName);
|
|
2340
2356
|
const isRequired = required.includes(propName);
|
|
2341
|
-
const fieldType = isRequired ? pythonType : `Optional[${pythonType}]`;
|
|
2342
|
-
const defaultValue = isRequired ? "" : " = None";
|
|
2357
|
+
const fieldType = isRequired || result.impossible ? pythonType : `Optional[${pythonType}]`;
|
|
2358
|
+
const defaultValue = isRequired ? "" : result.impossible ? " = Field(default=None, exclude=True)" : " = None";
|
|
2343
2359
|
fields.push(` ${fieldName}: ${fieldType}${defaultValue}`);
|
|
2344
2360
|
} else {
|
|
2345
2361
|
const result = this.handle(propSchema, { ...context, name: propName });
|
|
2346
2362
|
const fieldName = this.#formatFieldName(propName);
|
|
2347
2363
|
const isRequired = required.includes(propName);
|
|
2348
2364
|
let fieldType = result.type || "Any";
|
|
2349
|
-
if (!isRequired) {
|
|
2365
|
+
if (!isRequired && !result.impossible) {
|
|
2350
2366
|
fieldType = `Optional[${fieldType}]`;
|
|
2351
2367
|
}
|
|
2352
|
-
const defaultValue = isRequired ? "" : " = None";
|
|
2368
|
+
const defaultValue = isRequired ? "" : result.impossible ? " = Field(default=None, exclude=True)" : " = None";
|
|
2353
2369
|
let fieldDef = ` ${fieldName}: ${fieldType}${defaultValue}`;
|
|
2354
2370
|
if (fieldName !== propName) {
|
|
2355
|
-
fieldDef = ` ${fieldName}: ${fieldType} = Field(alias='${propName}'${
|
|
2371
|
+
fieldDef = ` ${fieldName}: ${fieldType} = Field(alias='${propName}'${isRequired ? "" : ", default=None"}${!isRequired && result.impossible ? ", exclude=True" : ""})`;
|
|
2356
2372
|
}
|
|
2357
2373
|
if (propSchema.description) {
|
|
2358
2374
|
fieldDef += ` # ${propSchema.description}`;
|
|
@@ -2525,7 +2541,18 @@ ${enumItems.join("\n")}
|
|
|
2525
2541
|
}
|
|
2526
2542
|
handle(schema, context = {}) {
|
|
2527
2543
|
if (isRef(schema)) {
|
|
2528
|
-
return this.#ref(schema);
|
|
2544
|
+
return this.#ref(schema, context);
|
|
2545
|
+
}
|
|
2546
|
+
if (schema.not && isEmpty(schema.not)) {
|
|
2547
|
+
const type = context.pydantic === true ? "_NeverValue" : "Never";
|
|
2548
|
+
return {
|
|
2549
|
+
type,
|
|
2550
|
+
content: "",
|
|
2551
|
+
use: type,
|
|
2552
|
+
fromJson: type,
|
|
2553
|
+
simple: true,
|
|
2554
|
+
impossible: true
|
|
2555
|
+
};
|
|
2529
2556
|
}
|
|
2530
2557
|
if ("const" in schema && schema.const !== void 0) {
|
|
2531
2558
|
return this.#const(schema);
|
|
@@ -2609,7 +2636,8 @@ ${docstring}
|
|
|
2609
2636
|
(acc, [name, { className, methods }]) => {
|
|
2610
2637
|
const fileName = `api/${snakecase2(name)}_api.py`;
|
|
2611
2638
|
const imports = [
|
|
2612
|
-
"from typing import Optional",
|
|
2639
|
+
"from typing import Any, Dict, List, Literal, Optional, Union",
|
|
2640
|
+
"from typing_extensions import Never",
|
|
2613
2641
|
"import httpx",
|
|
2614
2642
|
"",
|
|
2615
2643
|
"from ..http.dispatcher import Dispatcher, RequestConfig",
|
|
@@ -2743,7 +2771,7 @@ httpx>=0.24.0,<1.0.0
|
|
|
2743
2771
|
pydantic>=2.0.0,<3.0.0
|
|
2744
2772
|
|
|
2745
2773
|
# Enhanced type hints
|
|
2746
|
-
typing-extensions>=4.
|
|
2774
|
+
typing-extensions>=4.1.0
|
|
2747
2775
|
|
|
2748
2776
|
# Optional: For better datetime handling
|
|
2749
2777
|
python-dateutil>=2.8.0
|
|
@@ -2822,7 +2850,7 @@ ${imports}
|
|
|
2822
2850
|
}
|
|
2823
2851
|
function toInputs(spec, { entry, operation }) {
|
|
2824
2852
|
const inputName = entry.inputName || "Input";
|
|
2825
|
-
const haveInput = !
|
|
2853
|
+
const haveInput = !isEmpty2(operation.parameters) || !isEmpty2(operation.requestBody);
|
|
2826
2854
|
let contentType = "json";
|
|
2827
2855
|
if (operation.requestBody && !isRef2(operation.requestBody)) {
|
|
2828
2856
|
const content = operation.requestBody.content;
|
|
@@ -2871,26 +2899,48 @@ function toOutput(spec, operation) {
|
|
|
2871
2899
|
}
|
|
2872
2900
|
const [, mediaType] = jsonContent;
|
|
2873
2901
|
const schema = mediaType.schema;
|
|
2874
|
-
if (!schema
|
|
2902
|
+
if (!schema) {
|
|
2875
2903
|
return { returnType: "Any", successModel: null, errorModel: null };
|
|
2876
2904
|
}
|
|
2905
|
+
let outputSchema = schema;
|
|
2906
|
+
if (isRef2(schema)) {
|
|
2907
|
+
const resolvedSchema = followRef2(spec, schema.$ref);
|
|
2908
|
+
const isBottomResponse = isBottomSchema(spec, resolvedSchema) || resolvedSchema.type === "array" && isBottomSchema(spec, resolvedSchema.items);
|
|
2909
|
+
if (!isBottomResponse) {
|
|
2910
|
+
return { returnType: "Any", successModel: null, errorModel: null };
|
|
2911
|
+
}
|
|
2912
|
+
outputSchema = resolvedSchema;
|
|
2913
|
+
}
|
|
2877
2914
|
const emitter = new PythonEmitter(spec);
|
|
2878
|
-
const result = emitter.handle(
|
|
2915
|
+
const result = emitter.handle(outputSchema, {});
|
|
2879
2916
|
return {
|
|
2880
2917
|
returnType: result.type || "Any",
|
|
2881
|
-
successModel: result.type,
|
|
2918
|
+
successModel: result.simple ? null : result.type,
|
|
2882
2919
|
errorModel: null
|
|
2883
2920
|
// TODO: Handle error models
|
|
2884
2921
|
};
|
|
2885
2922
|
}
|
|
2923
|
+
function isBottomSchema(spec, schema) {
|
|
2924
|
+
if (!schema) {
|
|
2925
|
+
return false;
|
|
2926
|
+
}
|
|
2927
|
+
const resolved = isRef2(schema) ? followRef2(spec, schema.$ref) : schema;
|
|
2928
|
+
return !!resolved.not && isEmpty2(resolved.not);
|
|
2929
|
+
}
|
|
2886
2930
|
async function serializeModels(spec, emitter) {
|
|
2887
2931
|
const models = {};
|
|
2888
2932
|
const standardImports = [
|
|
2889
2933
|
"from typing import Any, Dict, List, Optional, Union, Literal",
|
|
2890
|
-
"from
|
|
2934
|
+
"from typing_extensions import Annotated, Never",
|
|
2935
|
+
"from pydantic import BaseModel, BeforeValidator, Field",
|
|
2891
2936
|
"from datetime import datetime, date",
|
|
2892
2937
|
"from uuid import UUID",
|
|
2893
|
-
"from enum import Enum"
|
|
2938
|
+
"from enum import Enum",
|
|
2939
|
+
"",
|
|
2940
|
+
"def _reject_never(value: Any) -> Never:",
|
|
2941
|
+
" raise ValueError('Value is forbidden by the schema')",
|
|
2942
|
+
"",
|
|
2943
|
+
"_NeverValue = Annotated[Any, BeforeValidator(_reject_never)]"
|
|
2894
2944
|
].join("\n");
|
|
2895
2945
|
emitter.onEmit((name, content, schema) => {
|
|
2896
2946
|
const fullContent = `${standardImports}
|
|
@@ -2909,7 +2959,7 @@ ${content}`;
|
|
|
2909
2959
|
if (spec.components?.schemas) {
|
|
2910
2960
|
for (const [name, schema] of Object.entries(spec.components.schemas)) {
|
|
2911
2961
|
if (!isRef2(schema)) {
|
|
2912
|
-
emitter.handle(schema, { name });
|
|
2962
|
+
emitter.handle(schema, { name, pydantic: true });
|
|
2913
2963
|
}
|
|
2914
2964
|
}
|
|
2915
2965
|
}
|
|
@@ -3110,7 +3160,7 @@ async function emitRemote(spec, options) {
|
|
|
3110
3160
|
}
|
|
3111
3161
|
|
|
3112
3162
|
// packages/cli/src/lib/cli.ts
|
|
3113
|
-
var generate5 = new Command7("generate").option("-c, --config <path>", "Path to an SDK-IT configuration file").action(async (options) => {
|
|
3163
|
+
var generate5 = new Command7("generate").description("Generate SDKs from configuration or OpenAPI").option("-c, --config <path>", "Path to an SDK-IT configuration file").action(async (options) => {
|
|
3114
3164
|
if (!options.config || options.config.endsWith(".ts")) {
|
|
3115
3165
|
try {
|
|
3116
3166
|
const config2 = await loadProjectConfig({ config: options.config });
|
|
@@ -3171,7 +3221,7 @@ var generate5 = new Command7("generate").option("-c, --config <path>", "Path to
|
|
|
3171
3221
|
await Promise.all(promises);
|
|
3172
3222
|
console.log("All configured generators completed successfully!");
|
|
3173
3223
|
}).addCommand(typescript_default).addCommand(python_default).addCommand(dart_default).addCommand(apiref_default).addCommand(readme_default);
|
|
3174
|
-
var cli = program.description(`CLI tool to interact with SDK-IT.`).addCommand(generate5, { isDefault: true }).addCommand(init_default).addCommand(
|
|
3224
|
+
var cli = program.name("sdk-it").description(`CLI tool to interact with SDK-IT.`).addCommand(generate5, { isDefault: true }).addCommand(init_default).addCommand(
|
|
3175
3225
|
new Command7("_internal").action(() => {
|
|
3176
3226
|
}),
|
|
3177
3227
|
{ hidden: true }
|