@igniter-js/cli 0.4.93 → 0.4.95

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,14 +1,14 @@
1
- # @igniter-js/new-cli
1
+ # @igniter-js/cli
2
2
 
3
3
  The official Igniter.js command-line interface for scaffolding projects, generating features, wiring add-ons, and keeping your API docs in sync. It is designed for a fast developer experience, type-safe defaults, and seamless automation.
4
4
 
5
5
  ## Highlights
6
6
 
7
- - 🚀 **Project bootstrapper** with interactive wizards, starter templates, and add-on setup (store, jobs, auth, bots, telemetry, MCP, database, Shadcn/UI).
8
- - 🛠️ **Code generators** for features, controllers, procedures, schemas, and OpenAPI specs, all backed by Handlebars templates.
9
- - 🧠 **Schema-aware workflow** with pluggable providers (Prisma out of the box) that produce strongly typed controllers, procedures, and interfaces.
10
- - 👀 **Development dashboard** powered by Ink that automatically regenerates schema/docs and streams app logs in a split view.
11
- - 🧩 **Template engine + registries** to extend starters, add-ons, and schema providers without touching the core.
7
+ - **Project bootstrapper** with interactive wizards, starter templates, and add-on setup (store, jobs, auth, bots, telemetry, MCP, database, Shadcn/UI).
8
+ - **Code generators** for features, controllers, procedures, schemas, and OpenAPI specs, all backed by Handlebars templates.
9
+ - **Schema-aware workflow** with pluggable providers (Prisma out of the box) that produce strongly typed controllers, procedures, and interfaces.
10
+ - **Development dashboard** powered by Ink that automatically regenerates schema/docs and streams app logs in a split view.
11
+ - **Template engine + registries** to extend starters, add-ons, and schema providers without touching the core.
12
12
 
13
13
  ---
14
14
 
@@ -131,7 +131,7 @@ Generates a TypeScript client schema (const assertion + type) that mirrors your
131
131
 
132
132
  ### `igniter generate caller`
133
133
 
134
- Generate Zod schemas and a ready-to-use Igniter Caller from an OpenAPI 3 spec.
134
+ Generate an `IgniterCallerSchema` builder plus a ready-to-use Igniter Caller from an OpenAPI 3 spec.
135
135
 
136
136
  ```
137
137
  # Remote spec
@@ -141,7 +141,19 @@ igniter generate caller --name facebook --url https://api.example.com/openapi.js
141
141
  igniter generate caller --name billing --path ./openapi.yaml --output src/callers/billing
142
142
  ```
143
143
 
144
- Outputs `schema.ts` (Zod schemas with the provided prefix) and `index.ts` (preconfigured caller) under `src/callers/<hostname>` by default, ready to use with `@igniter-js/caller`.
144
+ Outputs `schema.ts` (path-first schema builder with registry, `$Infer` helpers, and derived types) and
145
+ `index.ts` (preconfigured caller) under `src/callers/<hostname>` by default, ready to use with
146
+ `@igniter-js/caller`.
147
+
148
+ Example usage:
149
+
150
+ ```
151
+ import { facebookCallerSchemas } from "./src/callers/api.example.com/schema"
152
+
153
+ type ProductsResponse = ReturnType<
154
+ typeof facebookCallerSchemas.$Infer.Response<"/products", "GET", 200>
155
+ >
156
+ ```
145
157
 
146
158
  ### `igniter dev`
147
159
 
@@ -258,5 +270,3 @@ MIT © Felipe Barcelos and the Igniter.js contributors.
258
270
 
259
271
 
260
272
 
261
-
262
-
package/dist/index.mjs CHANGED
@@ -3552,7 +3552,7 @@ import path23 from "path";
3552
3552
  import * as p13 from "@clack/prompts";
3553
3553
  import yaml from "js-yaml";
3554
3554
  import SwaggerParser from "@apidevtools/swagger-parser";
3555
- var SUPPORTED_METHODS = ["get", "post", "put", "patch", "delete"];
3555
+ var SUPPORTED_METHODS = ["get", "post", "put", "patch", "delete", "head"];
3556
3556
  async function handleGenerateCallerAction(options) {
3557
3557
  p13.intro("Generate Igniter Caller");
3558
3558
  try {
@@ -3568,26 +3568,28 @@ async function handleGenerateCallerAction(options) {
3568
3568
  );
3569
3569
  const prefix = Casing.toPascalCase(callerName);
3570
3570
  const converter = new SchemaConverter(prefix, parsedDoc.components);
3571
- const pathsCode = buildSchemasObject(parsedDoc, converter);
3572
3571
  const componentsCode = converter.renderComponentSchemas();
3573
- const schemaConstName = `${prefix}Schema`;
3574
- const schemaTypeName = `${prefix}SchemaType`;
3572
+ const schemaConstName = `${Casing.toCamelCase(callerName)}CallerSchemas`;
3573
+ const schemaTypeName = `${prefix}CallerSchemas`;
3574
+ const { builderCode, typeAliases } = buildSchemaBuilder(
3575
+ parsedDoc,
3576
+ converter,
3577
+ schemaConstName
3578
+ );
3575
3579
  const schemaFileContents = [
3576
3580
  "/* eslint-disable */",
3577
3581
  "/* prettier-ignore */",
3578
- "",
3579
3582
  "/**",
3580
3583
  " * @generated by @igniter-js/cli",
3581
3584
  " * This file was automatically created from your OpenAPI spec.",
3582
3585
  " * Do not edit manually; regenerate via `igniter generate caller`.",
3583
3586
  " */",
3584
- "",
3585
3587
  'import { z } from "zod";',
3588
+ 'import { IgniterCallerSchema } from "@igniter-js/caller";',
3586
3589
  componentsCode,
3587
- `const schemas = ${pathsCode};`,
3588
- "",
3589
- `export const ${schemaConstName} = schemas;`,
3590
+ builderCode,
3590
3591
  `export type ${schemaTypeName} = typeof ${schemaConstName};`,
3592
+ typeAliases,
3591
3593
  ""
3592
3594
  ].filter(Boolean).join("\n");
3593
3595
  const baseUrl = inferBaseUrl(source, parsedDoc);
@@ -3613,9 +3615,7 @@ async function handleGenerateCallerAction(options) {
3613
3615
  p13.log.success(
3614
3616
  `Schemas and caller created at ${path23.relative(process.cwd(), outputDir)}`
3615
3617
  );
3616
- p13.log.info(
3617
- `Exported caller: ${callerVar} (schemas: ${schemaConstName})`
3618
- );
3618
+ p13.log.info(`Exported caller: ${callerVar} (schemas: ${schemaConstName})`);
3619
3619
  } catch (error) {
3620
3620
  const message = error instanceof Error ? error.message : String(error);
3621
3621
  p13.log.error(message);
@@ -3684,13 +3684,12 @@ async function resolveSource(options) {
3684
3684
  async function loadOpenApiDocument(source) {
3685
3685
  const raw = await loadRawContent(source);
3686
3686
  const parsed = parseOpenApi(raw);
3687
- if (!parsed.openapi || !parsed.openapi.startsWith("3.")) {
3687
+ const version = parsed.openapi ?? "";
3688
+ if (typeof version !== "string" || !version.startsWith("3.")) {
3688
3689
  throw new Error("Only OpenAPI 3.x documents are supported.");
3689
3690
  }
3690
- const dereferenced = await SwaggerParser.dereference(
3691
- parsed
3692
- );
3693
- return dereferenced;
3691
+ const bundled = await SwaggerParser.bundle(parsed);
3692
+ return bundled;
3694
3693
  }
3695
3694
  async function loadRawContent(source) {
3696
3695
  if (source.type === "url") {
@@ -3756,15 +3755,34 @@ var SchemaConverter = class {
3756
3755
  }
3757
3756
  renderComponentSchemas() {
3758
3757
  const entries = [];
3759
- const names = Object.keys(this.components?.schemas ?? {}).sort();
3758
+ const names = this.listComponents();
3760
3759
  for (const name of names) {
3761
3760
  const identifier = this.componentIdentifier(name);
3762
3761
  const expression = this.convertWithCache(name);
3763
- entries.push(`const ${identifier} = ${expression};`);
3762
+ const registryKey = this.registryKey(name);
3763
+ entries.push(
3764
+ [
3765
+ "/**",
3766
+ ` * Schema: ${registryKey}`,
3767
+ ` * Source: openapi#/components/schemas/${name}`,
3768
+ " */",
3769
+ `const ${identifier} = ${expression};`,
3770
+ ""
3771
+ ].join("\n")
3772
+ );
3764
3773
  }
3765
3774
  return entries.length ? `${entries.join("\n")}
3766
3775
  ` : "";
3767
3776
  }
3777
+ listComponents() {
3778
+ return Object.keys(this.components?.schemas ?? {}).sort();
3779
+ }
3780
+ registryKey(name) {
3781
+ return Casing.toPascalCase(name);
3782
+ }
3783
+ componentName(name) {
3784
+ return this.componentIdentifier(name);
3785
+ }
3768
3786
  convert(schema) {
3769
3787
  if ("$ref" in schema) {
3770
3788
  const refName = this.refName(schema.$ref);
@@ -3772,13 +3790,15 @@ var SchemaConverter = class {
3772
3790
  this.convertWithCache(refName);
3773
3791
  return this.wrapLazyIfNeeded(refName, identifier);
3774
3792
  }
3793
+ const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
3794
+ const nullable = isNullable(schema);
3775
3795
  if (schema.oneOf?.length) {
3776
3796
  const parts = schema.oneOf.map((item) => this.convert(item));
3777
- return `z.union([${parts.join(", ")}])${schema.nullable ? ".nullable()" : ""}`;
3797
+ return wrapNullable(`z.union([${parts.join(", ")}])`, nullable);
3778
3798
  }
3779
3799
  if (schema.anyOf?.length) {
3780
3800
  const parts = schema.anyOf.map((item) => this.convert(item));
3781
- return `z.union([${parts.join(", ")}])${schema.nullable ? ".nullable()" : ""}`;
3801
+ return wrapNullable(`z.union([${parts.join(", ")}])`, nullable);
3782
3802
  }
3783
3803
  if (schema.allOf?.length) {
3784
3804
  const [first, ...rest] = schema.allOf;
@@ -3786,31 +3806,29 @@ var SchemaConverter = class {
3786
3806
  for (const part of rest) {
3787
3807
  expr = `z.intersection(${expr}, ${this.convert(part)})`;
3788
3808
  }
3789
- return schema.nullable ? `${expr}.nullable()` : expr;
3809
+ return wrapNullable(expr, nullable);
3790
3810
  }
3791
- switch (schema.type) {
3811
+ switch (schemaType) {
3792
3812
  case "string":
3793
3813
  if (schema.enum) {
3794
3814
  const values = schema.enum.map((v) => JSON.stringify(String(v)));
3795
- return `z.enum([${values.join(", ")}])${schema.nullable ? ".nullable()" : ""}`;
3815
+ return wrapNullable(`z.enum([${values.join(", ")}])`, nullable);
3796
3816
  }
3797
- return `z.string()${schema.nullable ? ".nullable()" : ""}`;
3817
+ return wrapNullable("z.string()", nullable);
3798
3818
  case "number":
3799
3819
  case "integer": {
3800
3820
  let expr = "z.number()";
3801
- if (schema.type === "integer") {
3821
+ if (schemaType === "integer") {
3802
3822
  expr = `${expr}.int()`;
3803
3823
  }
3804
- if (schema.nullable) {
3805
- expr = `${expr}.nullable()`;
3806
- }
3807
- return expr;
3824
+ return wrapNullable(expr, nullable);
3808
3825
  }
3809
3826
  case "boolean":
3810
- return `z.boolean()${schema.nullable ? ".nullable()" : ""}`;
3827
+ return wrapNullable("z.boolean()", nullable);
3811
3828
  case "array": {
3812
- const items = schema.items ? this.convert(schema.items) : "z.unknown()";
3813
- return `z.array(${items})${schema.nullable ? ".nullable()" : ""}`;
3829
+ const arraySchema = schema;
3830
+ const items = arraySchema.items ? this.convert(arraySchema.items) : "z.unknown()";
3831
+ return wrapNullable(`z.array(${items})`, nullable);
3814
3832
  }
3815
3833
  case "object": {
3816
3834
  const properties = schema.properties ?? {};
@@ -3824,10 +3842,7 @@ var SchemaConverter = class {
3824
3842
  const apValue = schema.additionalProperties === true ? "z.unknown()" : this.convert(schema.additionalProperties);
3825
3843
  base = `${base}.catchall(${apValue})`;
3826
3844
  }
3827
- if (schema.nullable) {
3828
- base = `${base}.nullable()`;
3829
- }
3830
- return base;
3845
+ return wrapNullable(base, nullable);
3831
3846
  }
3832
3847
  default:
3833
3848
  return "z.unknown()";
@@ -3848,9 +3863,8 @@ var SchemaConverter = class {
3848
3863
  this.inProgress.add(name);
3849
3864
  const expression = this.convert(schema);
3850
3865
  this.inProgress.delete(name);
3851
- const finalized = expression.startsWith("z.lazy(") ? expression : expression;
3852
- this.generated.set(name, finalized);
3853
- return finalized;
3866
+ this.generated.set(name, expression);
3867
+ return expression;
3854
3868
  }
3855
3869
  componentIdentifier(name) {
3856
3870
  return `${this.prefix}${Casing.toPascalCase(name)}Schema`;
@@ -3869,71 +3883,271 @@ var SchemaConverter = class {
3869
3883
  return identifier;
3870
3884
  }
3871
3885
  };
3872
- function buildSchemasObject(doc, converter) {
3873
- const pathEntries = [];
3886
+ function buildSchemaBuilder(doc, converter, schemaConstName) {
3887
+ const usedNames = /* @__PURE__ */ new Map();
3888
+ const operations = [];
3889
+ const pathBlocks = [];
3890
+ let needsVoid = false;
3874
3891
  const sortedPaths = Object.keys(doc.paths || {}).sort();
3875
3892
  for (const rawPath of sortedPaths) {
3876
3893
  const pathItem = doc.paths?.[rawPath];
3877
3894
  if (!pathItem) continue;
3878
- const methodEntries = [];
3895
+ const methodBlocks = [];
3879
3896
  for (const method of SUPPORTED_METHODS) {
3880
3897
  const operation = pathItem[method];
3881
3898
  if (!operation) continue;
3882
- const responses = buildResponses(operation.responses, converter);
3883
- const requestBody = buildRequestBody(operation.requestBody, converter);
3884
- const segments = [];
3885
- if (requestBody) {
3886
- segments.push(`request: ${requestBody}`);
3887
- }
3888
- segments.push(`responses: ${responses}`);
3889
- const methodBlock = [
3890
- `${method.toUpperCase()}: {`,
3891
- indentLines(segments.join(",\n"), 2),
3892
- "}"
3893
- ].join("\n");
3894
- methodEntries.push(indentLines(methodBlock, 2));
3899
+ const normalizedPath2 = normalizePath(rawPath);
3900
+ const requestBody = buildRequestBody(
3901
+ operation.requestBody,
3902
+ converter,
3903
+ doc
3904
+ );
3905
+ const responses = buildResponses(operation.responses, converter, doc);
3906
+ const typeBaseName = ensureUniqueTypeName(
3907
+ buildOperationTypeName(rawPath, method, operation.operationId),
3908
+ usedNames
3909
+ );
3910
+ operations.push({
3911
+ path: normalizedPath2,
3912
+ method: method.toUpperCase(),
3913
+ request: Boolean(requestBody),
3914
+ responses: responses.entries,
3915
+ typeName: typeBaseName
3916
+ });
3917
+ const docBlock = buildMethodDocBlock({
3918
+ method: method.toUpperCase(),
3919
+ path: normalizedPath2,
3920
+ summary: operation.summary ?? operation.description,
3921
+ tags: operation.tags,
3922
+ operationId: operation.operationId,
3923
+ requestLabel: requestBody?.label,
3924
+ responses: responses.entries,
3925
+ source: `openapi#/paths/${encodeJsonPointerPath(rawPath)}/${method}`
3926
+ });
3927
+ const methodCall = buildMethodCall(method, {
3928
+ request: requestBody?.expression,
3929
+ responses: responses.code,
3930
+ doc: operation.summary ?? operation.description,
3931
+ tags: operation.tags,
3932
+ operationId: operation.operationId
3933
+ });
3934
+ methodBlocks.push(indent(`${docBlock}
3935
+ ${methodCall}`, 3));
3936
+ needsVoid = needsVoid || responses.usesVoid;
3895
3937
  }
3896
- if (!methodEntries.length) continue;
3938
+ if (!methodBlocks.length) continue;
3897
3939
  const normalizedPath = normalizePath(rawPath);
3898
- const pathBlock = [
3899
- `${JSON.stringify(normalizedPath)}: {`,
3900
- methodEntries.join(",\n"),
3901
- "}"
3902
- ].join("\n");
3903
- pathEntries.push(indentLines(pathBlock, 1));
3904
- }
3905
- return `{
3906
- ${pathEntries.join(",\n")}
3907
- }`;
3940
+ const pathLines = [
3941
+ ` .path(${JSON.stringify(normalizedPath)}, (path) =>`,
3942
+ " path",
3943
+ methodBlocks.join("\n"),
3944
+ " )"
3945
+ ];
3946
+ pathBlocks.push(pathLines.join("\n"));
3947
+ }
3948
+ const builderLines = [];
3949
+ builderLines.push(`export const ${schemaConstName} = IgniterCallerSchema.create()`);
3950
+ const componentNames = converter.listComponents();
3951
+ for (const name of componentNames) {
3952
+ const registryKey = converter.registryKey(name);
3953
+ const identifier = converter.componentName(name);
3954
+ builderLines.push(` .schema(${JSON.stringify(registryKey)}, ${identifier})`);
3955
+ }
3956
+ if (needsVoid) {
3957
+ builderLines.push(` .schema("Void", z.void(), { internal: true })`);
3958
+ }
3959
+ if (pathBlocks.length) {
3960
+ builderLines.push(pathBlocks.join("\n"));
3961
+ }
3962
+ builderLines.push(" .build()");
3963
+ return {
3964
+ builderCode: `${builderLines.join("\n")}
3965
+ `,
3966
+ typeAliases: buildTypeAliases(operations, schemaConstName)
3967
+ };
3908
3968
  }
3909
- function buildResponses(responses, converter) {
3910
- if (!responses || !Object.keys(responses).length) {
3911
- return "{ }";
3969
+ function buildMethodDocBlock(params) {
3970
+ const lines = [];
3971
+ lines.push("/**");
3972
+ lines.push(` * ${params.method} ${params.path}`);
3973
+ if (params.summary) {
3974
+ lines.push(` * Summary: ${params.summary}`);
3975
+ }
3976
+ if (params.tags?.length) {
3977
+ lines.push(` * Tags: ${params.tags.join(", ")}`);
3978
+ }
3979
+ if (params.operationId) {
3980
+ lines.push(` * OperationId: ${params.operationId}`);
3981
+ }
3982
+ if (params.requestLabel) {
3983
+ lines.push(` * Request: ${params.requestLabel}`);
3984
+ }
3985
+ if (params.responses.length) {
3986
+ lines.push(" * Responses:");
3987
+ for (const response of params.responses) {
3988
+ lines.push(` * - ${response.status}: ${response.label}`);
3989
+ }
3912
3990
  }
3991
+ lines.push(` * Source: ${params.source}`);
3992
+ lines.push(" */");
3993
+ return lines.join("\n");
3994
+ }
3995
+ function buildMethodCall(method, params) {
3996
+ const lines = [];
3997
+ lines.push(`.${method}({`);
3998
+ if (params.request) {
3999
+ lines.push(` request: ${params.request},`);
4000
+ }
4001
+ lines.push(" responses: {");
4002
+ lines.push(params.responses);
4003
+ lines.push(" },");
4004
+ if (params.doc) {
4005
+ lines.push(` doc: ${JSON.stringify(params.doc)},`);
4006
+ }
4007
+ if (params.tags?.length) {
4008
+ lines.push(` tags: ${JSON.stringify(params.tags)},`);
4009
+ }
4010
+ if (params.operationId) {
4011
+ lines.push(` operationId: ${JSON.stringify(params.operationId)},`);
4012
+ }
4013
+ lines.push("})");
4014
+ return lines.join("\n");
4015
+ }
4016
+ function buildResponses(responses, converter, doc) {
4017
+ const builder = new CodeBuilder();
3913
4018
  const entries = [];
4019
+ let usesVoid = false;
4020
+ if (!responses || !Object.keys(responses).length) {
4021
+ const expression = `path.ref("Void").schema`;
4022
+ builder.line(indent(`200: ${expression},`, 2));
4023
+ entries.push({
4024
+ status: "200",
4025
+ statusLiteral: "200",
4026
+ label: "Void"
4027
+ });
4028
+ return { code: builder.toString(), entries, usesVoid: true };
4029
+ }
3914
4030
  const sorted = Object.keys(responses).sort();
3915
4031
  for (const status of sorted) {
3916
4032
  const responseOrRef = responses[status];
3917
4033
  if (!responseOrRef) continue;
3918
- const response = "$ref" in responseOrRef ? void 0 : responseOrRef;
3919
- const schema = resolveSchemaFromResponse(response);
3920
- const expression = schema ? converter.convert(schema) : "z.unknown()";
3921
- entries.push(`${status}: ${expression}`);
3922
- }
3923
- if (!entries.length) {
3924
- return "{ }";
4034
+ const resolved = resolveResponse(responseOrRef, doc);
4035
+ const schema = resolveSchemaFromResponse(resolved);
4036
+ const resolvedSchema = resolveSchemaExpression(schema, converter);
4037
+ builder.line(indent(`${formatStatusKey(status)}: ${resolvedSchema.expression},`, 2));
4038
+ entries.push({
4039
+ status: String(status),
4040
+ statusLiteral: formatStatusType(status),
4041
+ label: resolvedSchema.label
4042
+ });
4043
+ usesVoid = usesVoid || resolvedSchema.usesVoid;
3925
4044
  }
3926
- return `{
3927
- ${indentLines(entries.join(",\n"), 2)}
3928
- }`;
4045
+ return { code: builder.toString(), entries, usesVoid };
3929
4046
  }
3930
- function buildRequestBody(requestBody, converter) {
4047
+ function buildRequestBody(requestBody, converter, doc) {
3931
4048
  if (!requestBody) return null;
3932
- const resolved = "$ref" in requestBody ? null : requestBody;
4049
+ const resolved = "$ref" in requestBody ? resolveRequestBody(requestBody, doc) : requestBody;
3933
4050
  if (!resolved?.content) return null;
3934
4051
  const schema = selectJsonSchema(resolved.content);
3935
4052
  if (!schema) return null;
3936
- return converter.convert(schema);
4053
+ return resolveSchemaExpression(schema, converter);
4054
+ }
4055
+ function resolveSchemaExpression(schema, converter) {
4056
+ if (!schema) {
4057
+ return {
4058
+ expression: `path.ref("Void").schema`,
4059
+ label: "Void",
4060
+ usesVoid: true
4061
+ };
4062
+ }
4063
+ const refName = extractSchemaRef(schema);
4064
+ if (refName) {
4065
+ const registryKey = converter.registryKey(refName);
4066
+ const base = `path.ref(${JSON.stringify(registryKey)}).schema`;
4067
+ return {
4068
+ expression: wrapNullable(base, isNullable(schema)),
4069
+ label: registryKey,
4070
+ usesVoid: false
4071
+ };
4072
+ }
4073
+ const arrayRefName = extractArraySchemaRef(schema);
4074
+ if (arrayRefName) {
4075
+ const registryKey = converter.registryKey(arrayRefName);
4076
+ const base = `path.ref(${JSON.stringify(registryKey)}).array()`;
4077
+ return {
4078
+ expression: wrapNullable(base, isNullable(schema)),
4079
+ label: `${registryKey}[]`,
4080
+ usesVoid: false
4081
+ };
4082
+ }
4083
+ return {
4084
+ expression: converter.convert(schema),
4085
+ label: "InlineSchema",
4086
+ usesVoid: false
4087
+ };
4088
+ }
4089
+ function buildTypeAliases(operations, schemaConstName) {
4090
+ if (!operations.length) return "";
4091
+ const lines = [];
4092
+ lines.push("// Derived types");
4093
+ for (const operation of operations) {
4094
+ const pathLiteral = JSON.stringify(operation.path);
4095
+ const methodLiteral = JSON.stringify(operation.method);
4096
+ if (operation.request) {
4097
+ lines.push(
4098
+ `export type ${operation.typeName}Request = ReturnType<typeof ${schemaConstName}.$Infer.Request<${pathLiteral}, ${methodLiteral}>>;`
4099
+ );
4100
+ }
4101
+ lines.push(
4102
+ `export type ${operation.typeName}Responses = ReturnType<typeof ${schemaConstName}.$Infer.Responses<${pathLiteral}, ${methodLiteral}>>;`
4103
+ );
4104
+ for (const response of operation.responses) {
4105
+ const suffix = formatStatusSuffix(response.status);
4106
+ lines.push(
4107
+ `export type ${operation.typeName}Response${suffix} = ReturnType<typeof ${schemaConstName}.$Infer.Response<${pathLiteral}, ${methodLiteral}, ${response.statusLiteral}>>;`
4108
+ );
4109
+ }
4110
+ lines.push("");
4111
+ }
4112
+ return lines.join("\n");
4113
+ }
4114
+ function buildOperationTypeName(rawPath, method, operationId) {
4115
+ if (operationId) {
4116
+ return Casing.toPascalCase(operationId);
4117
+ }
4118
+ const normalizedPath = normalizePath(rawPath);
4119
+ const segments = normalizedPath.split("/").filter(Boolean).map((segment) => segment.startsWith(":") ? `by-${segment.slice(1)}` : segment);
4120
+ return Casing.toPascalCase([...segments, method].join("-"));
4121
+ }
4122
+ function ensureUniqueTypeName(baseName, used) {
4123
+ const count = used.get(baseName) ?? 0;
4124
+ if (count === 0) {
4125
+ used.set(baseName, 1);
4126
+ return baseName;
4127
+ }
4128
+ const next = `${baseName}${count + 1}`;
4129
+ used.set(baseName, count + 1);
4130
+ return next;
4131
+ }
4132
+ function encodeJsonPointerPath(pathname) {
4133
+ return pathname.replace(/~/g, "~0").replace(/\//g, "~1");
4134
+ }
4135
+ function extractSchemaRef(schema) {
4136
+ if (!schema || typeof schema !== "object") return null;
4137
+ if (!("$ref" in schema)) return null;
4138
+ return extractComponentSchemaName(schema.$ref);
4139
+ }
4140
+ function extractArraySchemaRef(schema) {
4141
+ if (!schema || typeof schema !== "object") return null;
4142
+ if (schema.type !== "array") return null;
4143
+ const items = schema.items;
4144
+ if (!items || typeof items !== "object") return null;
4145
+ if (!("$ref" in items)) return null;
4146
+ return extractComponentSchemaName(items.$ref);
4147
+ }
4148
+ function extractComponentSchemaName(ref) {
4149
+ const match = ref.match(/^#\/components\/schemas\/(.+)$/);
4150
+ return match ? match[1] : null;
3937
4151
  }
3938
4152
  function selectJsonSchema(content) {
3939
4153
  if (content["application/json"]?.schema) {
@@ -3951,10 +4165,61 @@ function resolveSchemaFromResponse(response) {
3951
4165
  function normalizePath(pathname) {
3952
4166
  return pathname.replace(/{(.*?)}/g, ":$1");
3953
4167
  }
3954
- function indentLines(value, depth) {
4168
+ function indent(value, depth) {
3955
4169
  const pad = " ".repeat(depth);
3956
4170
  return value.split("\n").map((line) => line ? pad + line : line).join("\n");
3957
4171
  }
4172
+ function isNullable(schema) {
4173
+ if (!schema || typeof schema !== "object") return false;
4174
+ return Boolean(schema.nullable);
4175
+ }
4176
+ function wrapNullable(expr, nullable) {
4177
+ return nullable ? `${expr}.nullable()` : expr;
4178
+ }
4179
+ function formatStatusKey(status) {
4180
+ const trimmed = String(status).trim();
4181
+ const numeric = Number(trimmed);
4182
+ if (!Number.isNaN(numeric) && `${numeric}` === trimmed) {
4183
+ return trimmed;
4184
+ }
4185
+ return JSON.stringify(trimmed);
4186
+ }
4187
+ function formatStatusType(status) {
4188
+ return formatStatusKey(status);
4189
+ }
4190
+ function formatStatusSuffix(status) {
4191
+ const safe = String(status).replace(/[^a-zA-Z0-9]+/g, "-");
4192
+ const pascal = Casing.toPascalCase(safe);
4193
+ return pascal || "Unknown";
4194
+ }
4195
+ function resolveResponse(response, doc) {
4196
+ if ("$ref" in response) {
4197
+ return getComponent(doc, response.$ref, "responses");
4198
+ }
4199
+ return response;
4200
+ }
4201
+ function resolveRequestBody(requestBody, doc) {
4202
+ return getComponent(doc, requestBody.$ref, "requestBodies");
4203
+ }
4204
+ function getComponent(doc, ref, type) {
4205
+ const match = ref.match(/^#\/components\/([^/]+)\/(.+)$/);
4206
+ if (!match) return void 0;
4207
+ const [, category, name] = match;
4208
+ if (category !== type) return void 0;
4209
+ const component = doc.components?.[type]?.[name];
4210
+ return component;
4211
+ }
4212
+ var CodeBuilder = class {
4213
+ constructor() {
4214
+ this.lines = [];
4215
+ }
4216
+ line(text4) {
4217
+ this.lines.push(text4);
4218
+ }
4219
+ toString() {
4220
+ return this.lines.join("\n");
4221
+ }
4222
+ };
3958
4223
 
3959
4224
  // src/commands/generate/caller/index.ts
3960
4225
  var callerCommand = new Command7().command("caller").description("Generate Igniter Caller schemas from an OpenAPI spec").option("--name <name>", "Name used to prefix generated schemas and caller export").option("--url <url>", "URL to the OpenAPI document").option("--path <path>", "Local path to the OpenAPI document").option(