Skip to content

Commit 4d6d272

Browse files
owinter86claude
andcommitted
feat: add CLI support for npx execution
Add command-line interface for generating clients without config files: npx react-query-lightbase-codegen <spec...> -o <output> Features: - Support for local and remote spec files - Multiple specs in single command - Config file support (--config) - Help and version flags Examples: npx react-query-lightbase-codegen ./api.yaml -o ./generated npx react-query-lightbase-codegen https://api.example.com/spec.json -o ./gen npx react-query-lightbase-codegen --config ./codegen.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6bdade1 commit 4d6d272

File tree

5 files changed

+226
-4
lines changed

5 files changed

+226
-4
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"license": "MIT",
55
"description": "Generate Axios API clients and React Query options from OpenAPI specifications",
66
"exports": "./dist/index.js",
7+
"bin": {
8+
"react-query-lightbase-codegen": "./dist/cli.js"
9+
},
710
"files": ["src", "dist"],
811
"author": {
912
"name": "Oliver Winter",

src/cli.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env node
2+
3+
import { readFile, stat } from "node:fs/promises";
4+
import { resolve } from "node:path";
5+
import { codegenerate } from "./index";
6+
import type { OpenAPIConfig } from "./types/config";
7+
8+
const VERSION = "2.5.11";
9+
10+
const HELP = `
11+
react-query-lightbase-codegen - Generate React Query clients from OpenAPI specs
12+
13+
USAGE:
14+
npx react-query-lightbase-codegen [options] <spec...> -o <output>
15+
npx react-query-lightbase-codegen --config <config-file>
16+
17+
ARGUMENTS:
18+
<spec...> One or more OpenAPI spec files (local paths or URLs)
19+
20+
OPTIONS:
21+
-o, --output <dir> Output directory for generated files (default: ./generated)
22+
-c, --config <file> Path to JSON config file
23+
-h, --help Show this help message
24+
-v, --version Show version number
25+
26+
EXAMPLES:
27+
# Single local spec
28+
npx react-query-lightbase-codegen ./api.yaml -o ./src/generated
29+
30+
# Remote spec
31+
npx react-query-lightbase-codegen https://api.example.com/openapi.json -o ./generated
32+
33+
# Multiple specs
34+
npx react-query-lightbase-codegen ./auth.yaml ./users.yaml -o ./generated
35+
36+
# Using config file
37+
npx react-query-lightbase-codegen --config ./codegen.json
38+
39+
CONFIG FILE FORMAT:
40+
{
41+
"specSource": "./api.yaml", // or ["./auth.yaml", "./users.yaml"]
42+
"exportDir": "./src/generated"
43+
}
44+
`;
45+
46+
interface ParsedArgs {
47+
specs: string[];
48+
output: string;
49+
config?: string;
50+
help: boolean;
51+
version: boolean;
52+
}
53+
54+
function parseArgs(args: string[]): ParsedArgs {
55+
const result: ParsedArgs = {
56+
specs: [],
57+
output: "./generated",
58+
help: false,
59+
version: false,
60+
};
61+
62+
let i = 0;
63+
while (i < args.length) {
64+
const arg = args[i];
65+
66+
switch (arg) {
67+
case "-h":
68+
case "--help":
69+
result.help = true;
70+
break;
71+
72+
case "-v":
73+
case "--version":
74+
result.version = true;
75+
break;
76+
77+
case "-o":
78+
case "--output":
79+
i++;
80+
if (i >= args.length) {
81+
throw new Error("Missing value for --output");
82+
}
83+
result.output = args[i];
84+
break;
85+
86+
case "-c":
87+
case "--config":
88+
i++;
89+
if (i >= args.length) {
90+
throw new Error("Missing value for --config");
91+
}
92+
result.config = args[i];
93+
break;
94+
95+
default:
96+
if (arg.startsWith("-")) {
97+
throw new Error(`Unknown option: ${arg}`);
98+
}
99+
result.specs.push(arg);
100+
break;
101+
}
102+
i++;
103+
}
104+
105+
return result;
106+
}
107+
108+
async function loadConfigFile(configPath: string): Promise<OpenAPIConfig> {
109+
try {
110+
const content = await readFile(configPath, "utf-8");
111+
const config = JSON.parse(content);
112+
113+
if (!config.specSource) {
114+
throw new Error("Config file must contain 'specSource'");
115+
}
116+
if (!config.exportDir) {
117+
throw new Error("Config file must contain 'exportDir'");
118+
}
119+
120+
return {
121+
specSource: config.specSource,
122+
exportDir: resolve(process.cwd(), config.exportDir),
123+
};
124+
} catch (error) {
125+
if (error instanceof Error) {
126+
throw new Error(`Failed to load config file: ${error.message}`);
127+
}
128+
throw new Error("Failed to load config file");
129+
}
130+
}
131+
132+
function resolveSpecPath(spec: string): string {
133+
// Keep URLs as-is
134+
if (spec.startsWith("http://") || spec.startsWith("https://")) {
135+
return spec;
136+
}
137+
// Resolve local paths relative to cwd
138+
return resolve(process.cwd(), spec);
139+
}
140+
141+
async function main(): Promise<void> {
142+
const args = process.argv.slice(2);
143+
144+
if (args.length === 0) {
145+
console.log(HELP);
146+
process.exit(0);
147+
}
148+
149+
let parsed: ParsedArgs;
150+
try {
151+
parsed = parseArgs(args);
152+
} catch (error) {
153+
if (error instanceof Error) {
154+
console.error(`Error: ${error.message}`);
155+
}
156+
console.error("Use --help for usage information");
157+
process.exit(1);
158+
}
159+
160+
if (parsed.help) {
161+
console.log(HELP);
162+
process.exit(0);
163+
}
164+
165+
if (parsed.version) {
166+
console.log(`react-query-lightbase-codegen v${VERSION}`);
167+
process.exit(0);
168+
}
169+
170+
let config: OpenAPIConfig;
171+
172+
if (parsed.config) {
173+
// Load from config file
174+
config = await loadConfigFile(parsed.config);
175+
} else {
176+
// Build config from CLI args
177+
if (parsed.specs.length === 0) {
178+
console.error("Error: No spec files provided");
179+
console.error("Use --help for usage information");
180+
process.exit(1);
181+
}
182+
183+
const resolvedSpecs = parsed.specs.map(resolveSpecPath);
184+
185+
config = {
186+
specSource: resolvedSpecs.length === 1 ? resolvedSpecs[0] : resolvedSpecs,
187+
exportDir: resolve(process.cwd(), parsed.output),
188+
};
189+
}
190+
191+
try {
192+
console.log("Generating API client...");
193+
console.log(
194+
` Specs: ${Array.isArray(config.specSource) ? config.specSource.join(", ") : config.specSource}`
195+
);
196+
console.log(` Output: ${config.exportDir}`);
197+
console.log("");
198+
199+
await codegenerate(config);
200+
201+
console.log("Generation complete!");
202+
} catch (error) {
203+
if (error instanceof Error) {
204+
console.error(`Error: ${error.message}`);
205+
} else {
206+
console.error("An unknown error occurred");
207+
}
208+
process.exit(1);
209+
}
210+
}
211+
212+
main();

src/generator/clientGenerator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { OpenAPIV3 } from "openapi-types";
22
import {
3+
type OperationInfo,
34
camelCase,
45
collectOperations,
56
getContentSchema,
6-
type OperationInfo,
77
pascalCase,
88
resolveSchema,
99
specTitle,
@@ -65,7 +65,8 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
6565
? resolveSchema(requestBody.content["multipart/form-data"].schema, spec)
6666
: undefined;
6767

68-
const requestBodyContent = requestBody && "content" in requestBody ? getContentSchema(requestBody.content) : undefined;
68+
const requestBodyContent =
69+
requestBody && "content" in requestBody ? getContentSchema(requestBody.content) : undefined;
6970
const requestBodySchema = requestBodyContent ? resolveSchema(requestBodyContent, spec) : undefined;
7071

7172
// Check if request body is a primitive type (string, number, boolean)

src/generator/reactQueryGenerator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { OpenAPIV3 } from "openapi-types";
22
import {
3+
type OperationInfo,
34
camelCase,
45
collectOperations,
56
getContentSchema,
6-
type OperationInfo,
77
resolveSchema,
88
specTitle,
99
} from "../utils";

src/generator/schemaGenerator.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { OpenAPIV3 } from "openapi-types";
2-
import { getContentSchema, getTypeFromSchema, pascalCase, sanitizePropertyName, sanitizeTypeName } from "../utils";
2+
import {
3+
getContentSchema,
4+
getTypeFromSchema,
5+
pascalCase,
6+
sanitizePropertyName,
7+
sanitizeTypeName,
8+
} from "../utils";
39

410
/**
511
* Formats a parameter as a TypeScript property string with optional JSDoc.

0 commit comments

Comments
 (0)