@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 CHANGED
@@ -1,110 +1,115 @@
1
1
  # @sdk-it/cli
2
2
 
3
- <p align="center">Command-line interface for SDK-IT that generates type-safe client SDKs from OpenAPI specifications</p>
3
+ Generate type-safe client SDKs from OpenAPI specifications.
4
4
 
5
- ## Installation
5
+ ## Run the CLI
6
6
 
7
- ```bash
8
- # Install globally
9
- npm install -g @sdk-it/cli
7
+ Use the latest CLI without installing it globally:
10
8
 
11
- # Or use with npx without installing
12
- npx @sdk-it/cli
9
+ ```bash
10
+ npx @sdk-it/cli@latest --help
13
11
  ```
14
12
 
15
- ## Usage
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
- npx @sdk-it/cli <language> --spec <path-to-spec> --output <output-directory> [options]
16
+ npm install --save-dev @sdk-it/cli
23
17
  ```
24
18
 
25
- ### Options
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
- #### Mode Options
21
+ Inside an existing TypeScript project, install the generated client's runtime
22
+ dependencies:
36
23
 
37
- - `minimal`: Generates only the client SDK files (default)
38
- - `full`: Generates a complete project including package.json and tsconfig.json (useful for monorepo/workspaces)
24
+ ```bash
25
+ npm install zod fast-content-type-parse
26
+ ```
39
27
 
40
- #### Formatter
28
+ Generate the client:
41
29
 
42
- You can specify a command to format the generated code. The special variable `$SDK_IT_OUTPUT` will be replaced with the output directory path.
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
- Examples:
38
+ Use it:
45
39
 
46
- - `--formatter "prettier $SDK_IT_OUTPUT --write"`
47
- - `--formatter "biome check $SDK_IT_OUTPUT --write"`
40
+ ```typescript
41
+ import { MyApi } from './src/generated/api/index.ts';
48
42
 
49
- ### Supported Specification Formats
43
+ const client = new MyApi({
44
+ baseUrl: 'https://api.example.com',
45
+ });
50
46
 
51
- - JSON (`.json`)
52
- - YAML (`.yaml`, `.yml`)
47
+ const users = await client.request('GET /users', {});
48
+ console.log(users);
49
+ ```
53
50
 
54
- ## Examples
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
- ### Generate SDK from a Remote OpenAPI Specification
54
+ ## TypeScript options
57
55
 
58
- ```bash
59
- npx sdk-it -s https://petstore.swagger.io/v2/swagger.json -o ./client
56
+ ```text
57
+ npx @sdk-it/cli@latest generate typescript [options]
60
58
  ```
61
59
 
62
- ### Generate SDK with Custom Client Name
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 -s ./openapi.json -o ./client -n PetStore
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
- ### Generate Full Project with Formatting
87
+ ## Remote specification example
69
88
 
70
89
  ```bash
71
- npx sdk-it -s ./openapi.yaml -o ./client -m full --formatter "prettier $SDK_IT_OUTPUT --write"
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
- ## Complete Example
97
+ ```typescript
98
+ import { Hetzner } from './src/generated/hetzner/index.ts';
75
99
 
76
- Let's generate a client SDK for the Hetzner Cloud API with automatic formatting:
100
+ const hetzner = new Hetzner({
101
+ token: process.env.HETZNER_API_TOKEN,
102
+ });
77
103
 
78
- ```bash
79
- # Generate SDK from Hetzner Cloud API spec with Prettier formatting
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
- This command:
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
- Use the generated SDK:
110
+ The same `generate` command also exposes the Dart, Python, API reference, and
111
+ README generators:
90
112
 
91
- ```typescript
92
- import { Client } from './client';
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 { isRef, notRef, parseRef, pascalcase } from "@sdk-it/core";
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
- #ref(ref) {
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 refInfo = parseRef(propSchema.$ref);
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}'${defaultValue ? ", default=None" : ""})`;
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.0.0
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 = !isEmpty(operation.parameters) || !isEmpty(operation.requestBody);
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 || isRef2(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(schema, {});
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 pydantic import BaseModel, Field",
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 }