toolcraft-openapi 0.0.38 → 0.0.40
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 +67 -0
- package/dist/bin/generate.d.ts +9 -0
- package/dist/bin/generate.js +122 -6
- package/dist/config.d.ts +53 -0
- package/dist/config.js +266 -0
- package/dist/define-client.js +4 -0
- package/dist/diagnose.d.ts +4 -0
- package/dist/diagnose.js +93 -0
- package/dist/diagnostics.d.ts +19 -0
- package/dist/diagnostics.js +19 -0
- package/dist/generate.d.ts +13 -1
- package/dist/generate.js +165 -12
- package/dist/http.d.ts +30 -25
- package/dist/http.js +184 -42
- package/dist/index.d.ts +7 -2
- package/dist/index.js +5 -2
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +30 -5
- package/node_modules/@poe-code/frontmatter/README.md +35 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +8 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.js +72 -0
- package/node_modules/@poe-code/frontmatter/dist/index.d.ts +3 -0
- package/node_modules/@poe-code/frontmatter/dist/index.js +3 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +20 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.js +137 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.d.ts +1 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.js +35 -0
- package/node_modules/@poe-code/frontmatter/package.json +25 -0
- package/node_modules/toolcraft-design/README.md +12 -0
- package/node_modules/toolcraft-design/dist/prompts/index.d.ts +39 -14
- package/node_modules/toolcraft-design/dist/prompts/index.js +10 -6
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.js +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.js +47 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +274 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +20 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +53 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.js +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +28 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.d.ts +13 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +131 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.js +52 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.js +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.d.ts +18 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.js +89 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.d.ts +21 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.js +32 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.js +60 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.js +18 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.js +1 -1
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.js +20 -453
- package/node_modules/toolcraft-design/package.json +6 -3
- package/package.json +7 -7
package/dist/diagnose.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DIAGNOSTIC_CODES, createDiagnostic } from "./diagnostics.js";
|
|
2
|
+
export function diagnose(config, document) {
|
|
3
|
+
const configuredMethods = collectConfiguredMethods(config.resources);
|
|
4
|
+
return [
|
|
5
|
+
...diagnoseDuplicateMethods(configuredMethods),
|
|
6
|
+
...diagnoseUnknownPagination(config, configuredMethods),
|
|
7
|
+
...diagnoseUnmappedEndpoints(config, document, configuredMethods)
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
function collectConfiguredMethods(resources) {
|
|
11
|
+
const methods = [];
|
|
12
|
+
function visit(resource, path) {
|
|
13
|
+
for (const [name, method] of Object.entries(resource.methods ?? {})) {
|
|
14
|
+
methods.push({
|
|
15
|
+
configPath: [...path, "methods", name].join("."),
|
|
16
|
+
method
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
for (const [name, child] of Object.entries(resource.subresources ?? {})) {
|
|
20
|
+
visit(child, [...path, "subresources", name]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const [name, resource] of Object.entries(resources ?? {})) {
|
|
24
|
+
visit(resource, ["resources", name]);
|
|
25
|
+
}
|
|
26
|
+
return methods;
|
|
27
|
+
}
|
|
28
|
+
function diagnoseDuplicateMethods(methods) {
|
|
29
|
+
const seen = new Map();
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
for (const method of methods) {
|
|
32
|
+
const key = endpointKey(method.method.method, method.method.path);
|
|
33
|
+
const existing = seen.get(key);
|
|
34
|
+
if (existing === undefined) {
|
|
35
|
+
seen.set(key, method);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
diagnostics.push(createDiagnostic({
|
|
39
|
+
code: DIAGNOSTIC_CODES.duplicateMethodPath,
|
|
40
|
+
severity: "error",
|
|
41
|
+
location: method.configPath,
|
|
42
|
+
message: `${method.configPath} and ${existing.configPath} both bind ${key}.`
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
return diagnostics;
|
|
46
|
+
}
|
|
47
|
+
function diagnoseUnknownPagination(config, methods) {
|
|
48
|
+
const schemes = new Set(Object.keys(config.pagination ?? {}));
|
|
49
|
+
return methods
|
|
50
|
+
.filter((method) => method.method.pagination !== undefined && !schemes.has(method.method.pagination))
|
|
51
|
+
.map((method) => createDiagnostic({
|
|
52
|
+
code: DIAGNOSTIC_CODES.unknownPaginationScheme,
|
|
53
|
+
severity: "error",
|
|
54
|
+
location: method.configPath,
|
|
55
|
+
message: `${method.configPath} references undeclared pagination scheme ${JSON.stringify(method.method.pagination)}.`
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
function diagnoseUnmappedEndpoints(config, document, methods) {
|
|
59
|
+
if (config.resources === undefined) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const configured = new Set(methods.map((method) => endpointKey(method.method.method, method.method.path)));
|
|
63
|
+
const unspecified = new Set((config.unspecified_endpoints ?? []).map(normalizeEndpointText));
|
|
64
|
+
const diagnostics = [];
|
|
65
|
+
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
|
|
66
|
+
if (pathItem === undefined) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const method of ["get", "post", "put", "patch", "delete", "head", "options"]) {
|
|
70
|
+
if (!Object.hasOwn(pathItem, method)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const key = endpointKey(method, path);
|
|
74
|
+
if (configured.has(key) || unspecified.has(key)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
diagnostics.push(createDiagnostic({
|
|
78
|
+
code: DIAGNOSTIC_CODES.unmappedEndpoint,
|
|
79
|
+
severity: "error",
|
|
80
|
+
location: `paths.${path}.${method}`,
|
|
81
|
+
message: `${method.toUpperCase()} ${path} is not listed in resources or unspecified_endpoints.`
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return diagnostics;
|
|
86
|
+
}
|
|
87
|
+
function endpointKey(method, path) {
|
|
88
|
+
return `${method.toLowerCase()} ${path}`;
|
|
89
|
+
}
|
|
90
|
+
function normalizeEndpointText(value) {
|
|
91
|
+
const [method, requestPath] = value.trim().split(" ");
|
|
92
|
+
return `${(method ?? "").toLowerCase()} ${requestPath ?? ""}`;
|
|
93
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type DiagnosticSeverity = "error" | "warn";
|
|
2
|
+
export interface Diagnostic {
|
|
3
|
+
code: string;
|
|
4
|
+
severity: DiagnosticSeverity;
|
|
5
|
+
message: string;
|
|
6
|
+
location?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const DIAGNOSTIC_CODES: {
|
|
9
|
+
readonly unmappedEndpoint: "TOOLCRAFT_OPENAPI_001";
|
|
10
|
+
readonly duplicateMethodPath: "TOOLCRAFT_OPENAPI_002";
|
|
11
|
+
readonly unknownPaginationScheme: "TOOLCRAFT_OPENAPI_003";
|
|
12
|
+
readonly specDrift: "TOOLCRAFT_OPENAPI_004";
|
|
13
|
+
readonly reservedMethodName: "TOOLCRAFT_OPENAPI_005";
|
|
14
|
+
readonly invalidEdition: "TOOLCRAFT_OPENAPI_006";
|
|
15
|
+
readonly invalidConfig: "TOOLCRAFT_OPENAPI_007";
|
|
16
|
+
};
|
|
17
|
+
export declare function createDiagnostic(args: Diagnostic): Diagnostic;
|
|
18
|
+
export declare function formatDiagnostic(diagnostic: Diagnostic): string;
|
|
19
|
+
export declare function formatDiagnostics(diagnostics: readonly Diagnostic[]): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DIAGNOSTIC_CODES = {
|
|
2
|
+
unmappedEndpoint: "TOOLCRAFT_OPENAPI_001",
|
|
3
|
+
duplicateMethodPath: "TOOLCRAFT_OPENAPI_002",
|
|
4
|
+
unknownPaginationScheme: "TOOLCRAFT_OPENAPI_003",
|
|
5
|
+
specDrift: "TOOLCRAFT_OPENAPI_004",
|
|
6
|
+
reservedMethodName: "TOOLCRAFT_OPENAPI_005",
|
|
7
|
+
invalidEdition: "TOOLCRAFT_OPENAPI_006",
|
|
8
|
+
invalidConfig: "TOOLCRAFT_OPENAPI_007"
|
|
9
|
+
};
|
|
10
|
+
export function createDiagnostic(args) {
|
|
11
|
+
return args;
|
|
12
|
+
}
|
|
13
|
+
export function formatDiagnostic(diagnostic) {
|
|
14
|
+
const location = diagnostic.location === undefined ? "" : ` ${diagnostic.location}`;
|
|
15
|
+
return `${diagnostic.code} ${diagnostic.severity}${location}: ${diagnostic.message}`;
|
|
16
|
+
}
|
|
17
|
+
export function formatDiagnostics(diagnostics) {
|
|
18
|
+
return diagnostics.map((diagnostic) => `${formatDiagnostic(diagnostic)}\n`).join("");
|
|
19
|
+
}
|
package/dist/generate.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HttpMethod } from "./naming.js";
|
|
2
|
+
import type { ToolcraftConfig } from "./config.js";
|
|
2
3
|
type OpenApiOperation = OpenApiOperationObject | OpenApiReferenceObject;
|
|
3
4
|
type OpenApiOperationMap = Partial<Record<HttpMethod, OpenApiOperation>>;
|
|
4
5
|
type OpenApiParameterLocation = "path" | "query" | "header" | "cookie";
|
|
@@ -12,6 +13,7 @@ export interface OpenApiDocument {
|
|
|
12
13
|
title?: string;
|
|
13
14
|
version?: string;
|
|
14
15
|
};
|
|
16
|
+
servers?: OpenApiServerObject[];
|
|
15
17
|
security?: OpenApiSecurityRequirementObject[];
|
|
16
18
|
paths?: Record<string, OpenApiPathItemObject | undefined>;
|
|
17
19
|
components?: {
|
|
@@ -98,6 +100,7 @@ type OpenApiSecurityRequirementObject = Record<string, string[]>;
|
|
|
98
100
|
export interface GenerateOptions {
|
|
99
101
|
specSha: string;
|
|
100
102
|
brand?: string;
|
|
103
|
+
config?: ToolcraftConfig;
|
|
101
104
|
}
|
|
102
105
|
export interface GeneratedFile {
|
|
103
106
|
path: string;
|
|
@@ -110,6 +113,7 @@ export interface GeneratedCommand {
|
|
|
110
113
|
filePath: string;
|
|
111
114
|
operationId: string;
|
|
112
115
|
description?: string;
|
|
116
|
+
examples?: GeneratedCommandExample[];
|
|
113
117
|
method: string;
|
|
114
118
|
path: string;
|
|
115
119
|
auth: "required" | "none";
|
|
@@ -119,6 +123,8 @@ export interface GeneratedCommand {
|
|
|
119
123
|
bodyMode?: "json" | "form" | "raw" | "base64" | "multipart";
|
|
120
124
|
contentType?: string;
|
|
121
125
|
multipartBinaryFields?: readonly string[];
|
|
126
|
+
idempotencyHeader?: string;
|
|
127
|
+
rawResponse?: boolean;
|
|
122
128
|
confirm: boolean;
|
|
123
129
|
params: GeneratedParam[];
|
|
124
130
|
paramsSchemaOptions?: GeneratedObjectSchemaOptions;
|
|
@@ -132,12 +138,17 @@ export interface GeneratedParam {
|
|
|
132
138
|
sourceName: string;
|
|
133
139
|
location: "path" | "query" | "header" | "body" | "transport";
|
|
134
140
|
description?: string;
|
|
141
|
+
longAliases?: string[];
|
|
135
142
|
shortFlag?: string;
|
|
136
143
|
scope?: readonly [GeneratedParamScope, ...GeneratedParamScope[]];
|
|
137
144
|
global?: boolean;
|
|
138
145
|
optional: boolean;
|
|
139
146
|
definition: GeneratedParamDefinition;
|
|
140
147
|
}
|
|
148
|
+
interface GeneratedCommandExample {
|
|
149
|
+
title: string;
|
|
150
|
+
params: Record<string, unknown>;
|
|
151
|
+
}
|
|
141
152
|
interface GeneratedParamDefinitionMetadata {
|
|
142
153
|
defaultValue?: unknown;
|
|
143
154
|
format?: string;
|
|
@@ -202,6 +213,7 @@ export interface GeneratedObjectSchemaOptions {
|
|
|
202
213
|
interface RenderSchemaOptionsInput {
|
|
203
214
|
definition: GeneratedParamDefinition;
|
|
204
215
|
description?: string;
|
|
216
|
+
longAliases?: string[];
|
|
205
217
|
shortFlag?: string;
|
|
206
218
|
scope?: readonly [GeneratedParamScope, ...GeneratedParamScope[]];
|
|
207
219
|
global?: boolean;
|
|
@@ -226,7 +238,7 @@ interface SchemaOptionEntry {
|
|
|
226
238
|
}
|
|
227
239
|
type QueryArraySerialization = "repeat" | "brackets" | "comma" | "pipe";
|
|
228
240
|
export declare function generate(document: OpenApiDocument, options: GenerateOptions): GeneratedFile[];
|
|
229
|
-
export declare function collectGeneratedCommands(document: OpenApiDocument): GeneratedCommand[];
|
|
241
|
+
export declare function collectGeneratedCommands(document: OpenApiDocument, config?: ToolcraftConfig): GeneratedCommand[];
|
|
230
242
|
export declare function collectGeneratedCommand(document: OpenApiDocument, path: string, method: HttpMethod): GeneratedCommand;
|
|
231
243
|
export declare function collectSchemaOptionEntries(param: RenderSchemaOptionsInput): SchemaOptionEntry[];
|
|
232
244
|
export {};
|
package/dist/generate.js
CHANGED
|
@@ -43,6 +43,15 @@ const TRANSPORT_PARAMS = [
|
|
|
43
43
|
definition: { kind: "boolean" }
|
|
44
44
|
}
|
|
45
45
|
];
|
|
46
|
+
const BINARY_OUTPUT_PARAM = {
|
|
47
|
+
paramName: "output",
|
|
48
|
+
sourceName: "output",
|
|
49
|
+
location: "transport",
|
|
50
|
+
description: "Write a binary response body to this local file path.",
|
|
51
|
+
scope: ["cli"],
|
|
52
|
+
optional: true,
|
|
53
|
+
definition: { kind: "string" }
|
|
54
|
+
};
|
|
46
55
|
const SCHEMA_OPTION_SOURCES = [
|
|
47
56
|
{
|
|
48
57
|
key: "description",
|
|
@@ -56,6 +65,10 @@ const SCHEMA_OPTION_SOURCES = [
|
|
|
56
65
|
key: "short",
|
|
57
66
|
get: (param) => param.shortFlag
|
|
58
67
|
},
|
|
68
|
+
{
|
|
69
|
+
key: "cliAliases",
|
|
70
|
+
get: (param) => param.longAliases
|
|
71
|
+
},
|
|
59
72
|
{
|
|
60
73
|
key: "scope",
|
|
61
74
|
get: (param) => param.scope
|
|
@@ -111,7 +124,7 @@ const SCHEMA_OPTION_SOURCES = [
|
|
|
111
124
|
];
|
|
112
125
|
export function generate(document, options) {
|
|
113
126
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
114
|
-
const commands = collectGeneratedCommands(normalizedDocument);
|
|
127
|
+
const commands = collectGeneratedCommands(normalizedDocument, options.config);
|
|
115
128
|
const brand = options.brand ?? "blue";
|
|
116
129
|
const label = normalizedDocument.info?.title ?? "Toolcraft";
|
|
117
130
|
return [
|
|
@@ -126,17 +139,102 @@ export function generate(document, options) {
|
|
|
126
139
|
createCliFile({ brand, label })
|
|
127
140
|
];
|
|
128
141
|
}
|
|
129
|
-
export function collectGeneratedCommands(document) {
|
|
142
|
+
export function collectGeneratedCommands(document, config) {
|
|
130
143
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
131
144
|
const paths = normalizedDocument.paths;
|
|
132
145
|
if (paths === undefined) {
|
|
133
146
|
throw new UserError('OpenAPI document must define a top-level "paths" object.');
|
|
134
147
|
}
|
|
135
148
|
const commands = collectOperations(paths).map((entry) => createGeneratedCommand(normalizedDocument, entry));
|
|
149
|
+
applyConfiguredCommandShape(commands, config);
|
|
136
150
|
disambiguateCommandPaths(commands);
|
|
137
151
|
assertUniqueCommandPaths(commands);
|
|
138
152
|
return commands.slice().sort((left, right) => compareGeneratedCommandPaths(left, right));
|
|
139
153
|
}
|
|
154
|
+
function applyConfiguredCommandShape(commands, config) {
|
|
155
|
+
const configured = collectConfiguredCommandShapes(config?.resources);
|
|
156
|
+
if (configured.length === 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const command of commands) {
|
|
160
|
+
const match = configured.find((candidate) => candidate.method.method.toUpperCase() === command.method && candidate.method.path === command.path);
|
|
161
|
+
if (match === undefined) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
command.noun = createSafeGeneratedNoun(match.noun);
|
|
165
|
+
command.verb = createSafeGeneratedVerb(match.verb);
|
|
166
|
+
command.examples = config?.readme?.examples?.[match.exampleKey];
|
|
167
|
+
applyConfiguredRawResponse(command);
|
|
168
|
+
applyConfiguredIdempotency(command, match.method, config);
|
|
169
|
+
refreshGeneratedCommandNames(command);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function applyConfiguredRawResponse(command) {
|
|
173
|
+
command.rawResponse = true;
|
|
174
|
+
if (command.params.some((param) => param.paramName === "rawResponse")) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
command.params.push({
|
|
178
|
+
paramName: "rawResponse",
|
|
179
|
+
sourceName: "rawResponse",
|
|
180
|
+
location: "transport",
|
|
181
|
+
description: "Return parsed data with the raw HTTP Response.",
|
|
182
|
+
longAliases: ["raw"],
|
|
183
|
+
scope: ["cli", "sdk"],
|
|
184
|
+
optional: true,
|
|
185
|
+
definition: { kind: "boolean" }
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function applyConfiguredIdempotency(command, method, config) {
|
|
189
|
+
const header = config?.client_settings?.idempotency_header;
|
|
190
|
+
if (method.idempotent !== true || header === undefined || command.method === "GET") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
command.idempotencyHeader = header;
|
|
194
|
+
if (command.params.some((param) => param.paramName === "idempotencyKey")) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
command.params.push({
|
|
198
|
+
paramName: "idempotencyKey",
|
|
199
|
+
sourceName: "idempotencyKey",
|
|
200
|
+
location: "transport",
|
|
201
|
+
description: "Set an idempotency key to retry this request safely.",
|
|
202
|
+
scope: ["cli", "mcp", "sdk"],
|
|
203
|
+
optional: true,
|
|
204
|
+
definition: { kind: "string" }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function collectConfiguredCommandShapes(resources) {
|
|
208
|
+
const shapes = [];
|
|
209
|
+
function visit(resourceName, resource, resourcePath) {
|
|
210
|
+
for (const [methodName, method] of Object.entries(resource.methods ?? {})) {
|
|
211
|
+
shapes.push({
|
|
212
|
+
exampleKey: [...resourcePath, methodName].join("."),
|
|
213
|
+
noun: resourceName,
|
|
214
|
+
verb: methodName,
|
|
215
|
+
method
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
for (const [childName, childResource] of Object.entries(resource.subresources ?? {})) {
|
|
219
|
+
visit(childName, childResource, [...resourcePath, childName]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const [resourceName, resource] of Object.entries(resources ?? {})) {
|
|
223
|
+
visit(resourceName, resource, [resourceName]);
|
|
224
|
+
}
|
|
225
|
+
return shapes;
|
|
226
|
+
}
|
|
227
|
+
function createSafeGeneratedVerb(value) {
|
|
228
|
+
const verb = toCamelCase(value);
|
|
229
|
+
if (!isIdentifierName(verb)) {
|
|
230
|
+
throw new UserError(`Configured method ${JSON.stringify(value)} must map to a valid command name.`);
|
|
231
|
+
}
|
|
232
|
+
return verb;
|
|
233
|
+
}
|
|
234
|
+
function refreshGeneratedCommandNames(command) {
|
|
235
|
+
command.exportName = `${toCamelCase(command.noun)}${toPascalCase(command.verb)}Command`;
|
|
236
|
+
command.filePath = `${command.noun}/${command.verb}.ts`;
|
|
237
|
+
}
|
|
140
238
|
export function collectGeneratedCommand(document, path, method) {
|
|
141
239
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
142
240
|
const pathItem = normalizedDocument.paths?.[path];
|
|
@@ -176,7 +274,7 @@ function createGeneratedCommand(document, entry) {
|
|
|
176
274
|
const response = resolveSuccessResponse(document, operation, operationId);
|
|
177
275
|
const noun = createSafeGeneratedNoun(deriveNoun(operation, entry.path, operationId));
|
|
178
276
|
const verb = deriveVerb(entry.method, entry.path, operation, operationId, noun);
|
|
179
|
-
const collected = collectParams(document, entry, operation, operationId, auth);
|
|
277
|
+
const collected = collectParams(document, entry, operation, operationId, auth, response.mode);
|
|
180
278
|
const methodDefaults = METHOD_DEFAULTS[entry.method];
|
|
181
279
|
const exportName = `${toCamelCase(noun)}${toPascalCase(verb)}Command`;
|
|
182
280
|
const filePath = `${noun}/${verb}.ts`;
|
|
@@ -220,11 +318,12 @@ function resolveOperationBaseUrl(operation, operationId) {
|
|
|
220
318
|
throw new UserError(`Operation ${JSON.stringify(operationId)} defines invalid per-operation server URL ${JSON.stringify(server.url)}.`);
|
|
221
319
|
}
|
|
222
320
|
}
|
|
223
|
-
function collectParams(document, entry, operation, operationId, auth) {
|
|
321
|
+
function collectParams(document, entry, operation, operationId, auth, responseMode) {
|
|
322
|
+
const transportParams = responseMode === "binary" ? [...TRANSPORT_PARAMS, BINARY_OUTPUT_PARAM] : TRANSPORT_PARAMS;
|
|
224
323
|
const operationParams = collectOperationParameters(document, entry.path, entry.pathItem.parameters ?? [], operation.parameters ?? [], operationId, auth);
|
|
225
324
|
const requestBodyParams = collectRequestBodyParams(document, operation, operationId, entry.method);
|
|
226
|
-
const qualifiedRequestBodyParams = qualifyBodyParamCollisions(requestBodyParams, new Set([...operationParams.params, ...
|
|
227
|
-
const params = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...
|
|
325
|
+
const qualifiedRequestBodyParams = qualifyBodyParamCollisions(requestBodyParams, new Set([...operationParams.params, ...transportParams].map((param) => param.paramName)));
|
|
326
|
+
const params = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...transportParams];
|
|
228
327
|
const deduped = new Map();
|
|
229
328
|
for (const param of params) {
|
|
230
329
|
const existing = deduped.get(param.paramName);
|
|
@@ -1284,16 +1383,30 @@ function applyDisambiguatedVerbs(candidates, existingPaths) {
|
|
|
1284
1383
|
}
|
|
1285
1384
|
function createCommandFile(options) {
|
|
1286
1385
|
const requiresUserError = options.preflightBlocks.length > 0;
|
|
1386
|
+
const usesMultipartFileInputs = options.bodyMode === "multipart" &&
|
|
1387
|
+
options.multipartBinaryFields !== undefined &&
|
|
1388
|
+
options.multipartBinaryFields.length > 0;
|
|
1389
|
+
const usesBinaryOutput = options.responseMode === "binary";
|
|
1390
|
+
const usesRequestShapeVariable = usesMultipartFileInputs || usesBinaryOutput;
|
|
1391
|
+
const openApiImports = [
|
|
1392
|
+
"requestJson",
|
|
1393
|
+
"defineApiCommand",
|
|
1394
|
+
...(usesMultipartFileInputs ? ["prepareMultipartFileInputs"] : []),
|
|
1395
|
+
...(usesBinaryOutput ? ["writeBinaryResponseOutput"] : [])
|
|
1396
|
+
];
|
|
1287
1397
|
const lines = createGeneratedTypeScriptFileLines([
|
|
1288
1398
|
`spec-sha: ${options.specSha}`,
|
|
1289
1399
|
`operation-id: ${options.operationId}`
|
|
1290
1400
|
]);
|
|
1291
1401
|
lines.push(requiresUserError
|
|
1292
1402
|
? 'import { S, UserError } from "toolcraft";'
|
|
1293
|
-
: 'import { S } from "toolcraft";',
|
|
1403
|
+
: 'import { S } from "toolcraft";', `import { ${openApiImports.join(", ")} } from "toolcraft-openapi";`, "", `export const ${options.exportName} = defineApiCommand({`, ` name: ${JSON.stringify(options.verb)},`);
|
|
1294
1404
|
if (options.description !== undefined) {
|
|
1295
1405
|
lines.push(` description: ${JSON.stringify(options.description)},`);
|
|
1296
1406
|
}
|
|
1407
|
+
if (options.examples !== undefined && options.examples.length > 0) {
|
|
1408
|
+
lines.push(` examples: ${JSON.stringify(options.examples)},`);
|
|
1409
|
+
}
|
|
1297
1410
|
lines.push(' scope: ["cli", "mcp", "sdk"] as const,');
|
|
1298
1411
|
if (options.confirm) {
|
|
1299
1412
|
lines.push(" confirm: true,");
|
|
@@ -1303,9 +1416,30 @@ function createCommandFile(options) {
|
|
|
1303
1416
|
lines.push(options.paramsSchemaOptions?.additionalProperties === false
|
|
1304
1417
|
? " }, { additionalProperties: false }),"
|
|
1305
1418
|
: " }),");
|
|
1306
|
-
lines.push(
|
|
1419
|
+
lines.push(usesRequestShapeVariable
|
|
1420
|
+
? " handler: async ({ params, baseUrl, tokenSource, fetch, fs, env }) => {"
|
|
1421
|
+
: " handler: async ({ params, baseUrl, tokenSource, fetch }) => {");
|
|
1307
1422
|
lines.push(...options.preflightBlocks.flatMap((block) => renderPreflightBlock(block)));
|
|
1308
|
-
|
|
1423
|
+
if (usesRequestShapeVariable) {
|
|
1424
|
+
lines.push(" const requestShape = {");
|
|
1425
|
+
lines.push(...renderRequestShape(options.requestFields, options.sectionRenders, options.optionalSections));
|
|
1426
|
+
lines.push(" };");
|
|
1427
|
+
if (usesMultipartFileInputs) {
|
|
1428
|
+
lines.push(" const preparedRequestShape = await prepareMultipartFileInputs(requestShape, {");
|
|
1429
|
+
lines.push(' bodyMode: "multipart",');
|
|
1430
|
+
lines.push(` multipartBinaryFields: ${JSON.stringify(options.multipartBinaryFields)},`);
|
|
1431
|
+
lines.push(" fs,");
|
|
1432
|
+
lines.push(" env,");
|
|
1433
|
+
lines.push(" });");
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
lines.push(" const preparedRequestShape = requestShape;");
|
|
1437
|
+
}
|
|
1438
|
+
lines.push(" const result = await requestJson({");
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
lines.push(" return requestJson({");
|
|
1442
|
+
}
|
|
1309
1443
|
lines.push(options.baseUrl === undefined
|
|
1310
1444
|
? " baseUrl,"
|
|
1311
1445
|
: ` baseUrl: ${JSON.stringify(options.baseUrl)},`);
|
|
@@ -1340,8 +1474,26 @@ function createCommandFile(options) {
|
|
|
1340
1474
|
lines.push(" fetch,");
|
|
1341
1475
|
lines.push(" dryRun: params.dryRun,");
|
|
1342
1476
|
lines.push(" verbose: params.verbose,");
|
|
1343
|
-
|
|
1477
|
+
if (options.rawResponse === true) {
|
|
1478
|
+
lines.push(" rawResponse: params.rawResponse,");
|
|
1479
|
+
}
|
|
1480
|
+
if (options.idempotencyHeader !== undefined) {
|
|
1481
|
+
lines.push(" idempotency: {");
|
|
1482
|
+
lines.push(` header: ${JSON.stringify(options.idempotencyHeader)},`);
|
|
1483
|
+
lines.push(" enabled: true,");
|
|
1484
|
+
lines.push(" key: params.idempotencyKey,");
|
|
1485
|
+
lines.push(" },");
|
|
1486
|
+
}
|
|
1487
|
+
if (usesRequestShapeVariable) {
|
|
1488
|
+
lines.push(" ...preparedRequestShape,");
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
lines.push(...renderRequestShape(options.requestFields, options.sectionRenders, options.optionalSections));
|
|
1492
|
+
}
|
|
1344
1493
|
lines.push(" });");
|
|
1494
|
+
if (usesBinaryOutput) {
|
|
1495
|
+
lines.push(" return writeBinaryResponseOutput(result, params.output, { fs, env });");
|
|
1496
|
+
}
|
|
1345
1497
|
lines.push(" },");
|
|
1346
1498
|
lines.push("});");
|
|
1347
1499
|
lines.push("");
|
|
@@ -1405,14 +1557,15 @@ function renderParamLines(params) {
|
|
|
1405
1557
|
return params.map((param) => ` ${renderObjectKey(param.paramName)}: ${renderParamSchema(param)},`);
|
|
1406
1558
|
}
|
|
1407
1559
|
function renderParamSchema(param) {
|
|
1408
|
-
const schema = renderDefinition(param.definition, param.description, param.shortFlag, param.scope, param.global);
|
|
1560
|
+
const schema = renderDefinition(param.definition, param.description, param.shortFlag, param.longAliases, param.scope, param.global);
|
|
1409
1561
|
return param.optional ? `S.Optional(${schema})` : schema;
|
|
1410
1562
|
}
|
|
1411
|
-
function renderDefinition(definition, description, shortFlag, scope, global) {
|
|
1563
|
+
function renderDefinition(definition, description, shortFlag, longAliases, scope, global) {
|
|
1412
1564
|
const options = renderSchemaOptions({
|
|
1413
1565
|
definition,
|
|
1414
1566
|
description,
|
|
1415
1567
|
shortFlag,
|
|
1568
|
+
longAliases,
|
|
1416
1569
|
scope,
|
|
1417
1570
|
global
|
|
1418
1571
|
});
|
package/dist/http.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { HttpError, type HandlerEnv, type HandlerFs, type HttpErrorRequest, type HttpErrorResponse } from "toolcraft";
|
|
1
2
|
import type { TokenSource } from "./auth/types.js";
|
|
3
|
+
export { HttpError };
|
|
4
|
+
export type { HttpErrorRequest, HttpErrorResponse };
|
|
2
5
|
type QueryScalar = string | number | boolean | null | undefined;
|
|
3
6
|
type QueryObject = {
|
|
4
7
|
readonly [key: string]: QueryValue;
|
|
@@ -24,38 +27,40 @@ export interface HttpRequestOptions {
|
|
|
24
27
|
dryRun?: boolean;
|
|
25
28
|
verbose?: boolean;
|
|
26
29
|
signal?: AbortSignal;
|
|
30
|
+
rawResponse?: boolean;
|
|
31
|
+
retries?: {
|
|
32
|
+
max: number;
|
|
33
|
+
backoff: "exponential";
|
|
34
|
+
retryOn: number[];
|
|
35
|
+
sleep?: (ms: number) => Promise<void>;
|
|
36
|
+
random?: () => number;
|
|
37
|
+
};
|
|
38
|
+
idempotency?: {
|
|
39
|
+
header: string;
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
key?: string;
|
|
42
|
+
createKey?: () => string;
|
|
43
|
+
};
|
|
27
44
|
writeStdout?: (chunk: string) => void;
|
|
28
45
|
writeStderr?: (chunk: string) => void;
|
|
29
46
|
}
|
|
30
|
-
export interface HttpErrorRequest {
|
|
31
|
-
method: string;
|
|
32
|
-
url: string;
|
|
33
|
-
headers: Record<string, string>;
|
|
34
|
-
body?: unknown;
|
|
35
|
-
}
|
|
36
|
-
export interface HttpErrorResponse {
|
|
37
|
-
status: number;
|
|
38
|
-
statusText: string;
|
|
39
|
-
headers: Record<string, string>;
|
|
40
|
-
body: unknown;
|
|
41
|
-
}
|
|
42
47
|
export interface BinaryHttpResponse {
|
|
43
48
|
contentType: string;
|
|
44
49
|
encoding: "base64";
|
|
45
50
|
byteLength: number;
|
|
46
51
|
data: string;
|
|
47
52
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
readonly request: HttpErrorRequest;
|
|
52
|
-
readonly response: HttpErrorResponse;
|
|
53
|
-
get body(): unknown;
|
|
54
|
-
constructor(args: {
|
|
55
|
-
request: HttpErrorRequest;
|
|
56
|
-
response: HttpErrorResponse;
|
|
57
|
-
message?: string;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
53
|
+
type RuntimeFileSystem = Pick<HandlerFs, "exists" | "readFile" | "writeFile">;
|
|
54
|
+
type RuntimeEnvironment = Pick<HandlerEnv, "get">;
|
|
55
|
+
type RequestShape = Partial<Pick<HttpRequestOptions, "pathParams" | "query" | "headers" | "body">>;
|
|
60
56
|
export declare function requestJson<TResult = unknown>(options: HttpRequestOptions): Promise<TResult | undefined>;
|
|
61
|
-
export {
|
|
57
|
+
export declare function prepareMultipartFileInputs(requestShape: RequestShape, options: {
|
|
58
|
+
bodyMode?: HttpRequestOptions["bodyMode"];
|
|
59
|
+
multipartBinaryFields?: readonly string[];
|
|
60
|
+
fs?: RuntimeFileSystem;
|
|
61
|
+
env?: RuntimeEnvironment;
|
|
62
|
+
}): Promise<RequestShape>;
|
|
63
|
+
export declare function writeBinaryResponseOutput(result: unknown, outputPath: unknown, runtime: {
|
|
64
|
+
fs?: RuntimeFileSystem;
|
|
65
|
+
env?: RuntimeEnvironment;
|
|
66
|
+
}): Promise<unknown>;
|