toolcraft-openapi 0.0.39 → 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 +107 -4
- package/dist/http.d.ts +17 -26
- package/dist/http.js +99 -41
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -1
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +19 -1
- 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
|
@@ -65,6 +65,10 @@ const SCHEMA_OPTION_SOURCES = [
|
|
|
65
65
|
key: "short",
|
|
66
66
|
get: (param) => param.shortFlag
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
key: "cliAliases",
|
|
70
|
+
get: (param) => param.longAliases
|
|
71
|
+
},
|
|
68
72
|
{
|
|
69
73
|
key: "scope",
|
|
70
74
|
get: (param) => param.scope
|
|
@@ -120,7 +124,7 @@ const SCHEMA_OPTION_SOURCES = [
|
|
|
120
124
|
];
|
|
121
125
|
export function generate(document, options) {
|
|
122
126
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
123
|
-
const commands = collectGeneratedCommands(normalizedDocument);
|
|
127
|
+
const commands = collectGeneratedCommands(normalizedDocument, options.config);
|
|
124
128
|
const brand = options.brand ?? "blue";
|
|
125
129
|
const label = normalizedDocument.info?.title ?? "Toolcraft";
|
|
126
130
|
return [
|
|
@@ -135,17 +139,102 @@ export function generate(document, options) {
|
|
|
135
139
|
createCliFile({ brand, label })
|
|
136
140
|
];
|
|
137
141
|
}
|
|
138
|
-
export function collectGeneratedCommands(document) {
|
|
142
|
+
export function collectGeneratedCommands(document, config) {
|
|
139
143
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
140
144
|
const paths = normalizedDocument.paths;
|
|
141
145
|
if (paths === undefined) {
|
|
142
146
|
throw new UserError('OpenAPI document must define a top-level "paths" object.');
|
|
143
147
|
}
|
|
144
148
|
const commands = collectOperations(paths).map((entry) => createGeneratedCommand(normalizedDocument, entry));
|
|
149
|
+
applyConfiguredCommandShape(commands, config);
|
|
145
150
|
disambiguateCommandPaths(commands);
|
|
146
151
|
assertUniqueCommandPaths(commands);
|
|
147
152
|
return commands.slice().sort((left, right) => compareGeneratedCommandPaths(left, right));
|
|
148
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
|
+
}
|
|
149
238
|
export function collectGeneratedCommand(document, path, method) {
|
|
150
239
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
151
240
|
const pathItem = normalizedDocument.paths?.[path];
|
|
@@ -1315,6 +1404,9 @@ function createCommandFile(options) {
|
|
|
1315
1404
|
if (options.description !== undefined) {
|
|
1316
1405
|
lines.push(` description: ${JSON.stringify(options.description)},`);
|
|
1317
1406
|
}
|
|
1407
|
+
if (options.examples !== undefined && options.examples.length > 0) {
|
|
1408
|
+
lines.push(` examples: ${JSON.stringify(options.examples)},`);
|
|
1409
|
+
}
|
|
1318
1410
|
lines.push(' scope: ["cli", "mcp", "sdk"] as const,');
|
|
1319
1411
|
if (options.confirm) {
|
|
1320
1412
|
lines.push(" confirm: true,");
|
|
@@ -1382,6 +1474,16 @@ function createCommandFile(options) {
|
|
|
1382
1474
|
lines.push(" fetch,");
|
|
1383
1475
|
lines.push(" dryRun: params.dryRun,");
|
|
1384
1476
|
lines.push(" verbose: params.verbose,");
|
|
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
|
+
}
|
|
1385
1487
|
if (usesRequestShapeVariable) {
|
|
1386
1488
|
lines.push(" ...preparedRequestShape,");
|
|
1387
1489
|
}
|
|
@@ -1455,14 +1557,15 @@ function renderParamLines(params) {
|
|
|
1455
1557
|
return params.map((param) => ` ${renderObjectKey(param.paramName)}: ${renderParamSchema(param)},`);
|
|
1456
1558
|
}
|
|
1457
1559
|
function renderParamSchema(param) {
|
|
1458
|
-
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);
|
|
1459
1561
|
return param.optional ? `S.Optional(${schema})` : schema;
|
|
1460
1562
|
}
|
|
1461
|
-
function renderDefinition(definition, description, shortFlag, scope, global) {
|
|
1563
|
+
function renderDefinition(definition, description, shortFlag, longAliases, scope, global) {
|
|
1462
1564
|
const options = renderSchemaOptions({
|
|
1463
1565
|
definition,
|
|
1464
1566
|
description,
|
|
1465
1567
|
shortFlag,
|
|
1568
|
+
longAliases,
|
|
1466
1569
|
scope,
|
|
1467
1570
|
global
|
|
1468
1571
|
});
|
package/dist/http.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type HandlerEnv, type HandlerFs } from "toolcraft";
|
|
1
|
+
import { HttpError, type HandlerEnv, type HandlerFs, type HttpErrorRequest, type HttpErrorResponse } from "toolcraft";
|
|
2
2
|
import type { TokenSource } from "./auth/types.js";
|
|
3
|
+
export { HttpError };
|
|
4
|
+
export type { HttpErrorRequest, HttpErrorResponse };
|
|
3
5
|
type QueryScalar = string | number | boolean | null | undefined;
|
|
4
6
|
type QueryObject = {
|
|
5
7
|
readonly [key: string]: QueryValue;
|
|
@@ -25,21 +27,23 @@ export interface HttpRequestOptions {
|
|
|
25
27
|
dryRun?: boolean;
|
|
26
28
|
verbose?: boolean;
|
|
27
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
|
+
};
|
|
28
44
|
writeStdout?: (chunk: string) => void;
|
|
29
45
|
writeStderr?: (chunk: string) => void;
|
|
30
46
|
}
|
|
31
|
-
export interface HttpErrorRequest {
|
|
32
|
-
method: string;
|
|
33
|
-
url: string;
|
|
34
|
-
headers: Record<string, string>;
|
|
35
|
-
body?: unknown;
|
|
36
|
-
}
|
|
37
|
-
export interface HttpErrorResponse {
|
|
38
|
-
status: number;
|
|
39
|
-
statusText: string;
|
|
40
|
-
headers: Record<string, string>;
|
|
41
|
-
body: unknown;
|
|
42
|
-
}
|
|
43
47
|
export interface BinaryHttpResponse {
|
|
44
48
|
contentType: string;
|
|
45
49
|
encoding: "base64";
|
|
@@ -49,18 +53,6 @@ export interface BinaryHttpResponse {
|
|
|
49
53
|
type RuntimeFileSystem = Pick<HandlerFs, "exists" | "readFile" | "writeFile">;
|
|
50
54
|
type RuntimeEnvironment = Pick<HandlerEnv, "get">;
|
|
51
55
|
type RequestShape = Partial<Pick<HttpRequestOptions, "pathParams" | "query" | "headers" | "body">>;
|
|
52
|
-
export declare class HttpError extends Error {
|
|
53
|
-
readonly status: number;
|
|
54
|
-
readonly statusText: string;
|
|
55
|
-
readonly request: HttpErrorRequest;
|
|
56
|
-
readonly response: HttpErrorResponse;
|
|
57
|
-
get body(): unknown;
|
|
58
|
-
constructor(args: {
|
|
59
|
-
request: HttpErrorRequest;
|
|
60
|
-
response: HttpErrorResponse;
|
|
61
|
-
message?: string;
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
56
|
export declare function requestJson<TResult = unknown>(options: HttpRequestOptions): Promise<TResult | undefined>;
|
|
65
57
|
export declare function prepareMultipartFileInputs(requestShape: RequestShape, options: {
|
|
66
58
|
bodyMode?: HttpRequestOptions["bodyMode"];
|
|
@@ -72,4 +64,3 @@ export declare function writeBinaryResponseOutput(result: unknown, outputPath: u
|
|
|
72
64
|
fs?: RuntimeFileSystem;
|
|
73
65
|
env?: RuntimeEnvironment;
|
|
74
66
|
}): Promise<unknown>;
|
|
75
|
-
export {};
|
package/dist/http.js
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { text as designText } from "toolcraft-design";
|
|
3
|
-
import { UserError } from "toolcraft";
|
|
4
|
+
import { HttpError, UserError, createHttpError } from "toolcraft";
|
|
4
5
|
import { classifyNetworkError } from "./network-error.js";
|
|
5
6
|
import { redactHeaders, redactHeaderValue, redactSensitiveQueryValues } from "./redaction.js";
|
|
7
|
+
export { HttpError };
|
|
6
8
|
const TRANSCRIPT_BODY_BYTE_LIMIT = 4 * 1024;
|
|
7
|
-
export class HttpError extends Error {
|
|
8
|
-
status;
|
|
9
|
-
statusText;
|
|
10
|
-
request;
|
|
11
|
-
response;
|
|
12
|
-
get body() {
|
|
13
|
-
return this.response.body;
|
|
14
|
-
}
|
|
15
|
-
constructor(args) {
|
|
16
|
-
super(args.message ??
|
|
17
|
-
`${args.request.method} ${args.request.url} → ${args.response.status} ${args.response.statusText}`);
|
|
18
|
-
this.name = "HttpError";
|
|
19
|
-
this.status = args.response.status;
|
|
20
|
-
this.statusText = args.response.statusText;
|
|
21
|
-
this.request = args.request;
|
|
22
|
-
this.response = args.response;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
9
|
export async function requestJson(options) {
|
|
26
10
|
const token = options.auth === "none" ? undefined : await options.tokenSource.getToken();
|
|
27
11
|
const method = options.method.toUpperCase();
|
|
@@ -38,7 +22,12 @@ export async function requestJson(options) {
|
|
|
38
22
|
: JSON.stringify(options.body)
|
|
39
23
|
: undefined;
|
|
40
24
|
const url = buildRequestUrl(options);
|
|
41
|
-
const
|
|
25
|
+
const idempotencyHeader = options.idempotency?.enabled === true && method !== "GET"
|
|
26
|
+
? {
|
|
27
|
+
[options.idempotency.header]: options.idempotency.key ?? options.idempotency.createKey?.() ?? randomUUID()
|
|
28
|
+
}
|
|
29
|
+
: {};
|
|
30
|
+
const headers = createHeaders(token, hasBody, { ...options.headers, ...idempotencyHeader }, options.accept, options.bodyMode, options.contentType);
|
|
42
31
|
const writeStdout = options.writeStdout ?? process.stdout.write.bind(process.stdout);
|
|
43
32
|
const writeStderr = options.writeStderr ?? process.stderr.write.bind(process.stderr);
|
|
44
33
|
const requestLine = `${method} ${redactSensitiveQueryValues(url)}`;
|
|
@@ -49,18 +38,12 @@ export async function requestJson(options) {
|
|
|
49
38
|
if (options.verbose) {
|
|
50
39
|
writeStderr(formatTranscriptLines(formatVerboseRequestTranscript(method, url, headers, options.body)));
|
|
51
40
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
signal: options.signal
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
throw classifyNetworkError(error, url) ?? error;
|
|
63
|
-
}
|
|
41
|
+
const response = await fetchWithRetries(options, url, {
|
|
42
|
+
method,
|
|
43
|
+
headers,
|
|
44
|
+
body: serializedBody,
|
|
45
|
+
signal: options.signal
|
|
46
|
+
});
|
|
64
47
|
const contentType = response.headers.get("content-type");
|
|
65
48
|
const request = createHttpErrorRequest(method, url, headers, options.body);
|
|
66
49
|
const responseHeaders = redactHeaders(serializeHeaders(response.headers));
|
|
@@ -70,7 +53,7 @@ export async function requestJson(options) {
|
|
|
70
53
|
if (options.verbose) {
|
|
71
54
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders)));
|
|
72
55
|
}
|
|
73
|
-
return undefined;
|
|
56
|
+
return formatRawResponseResult(undefined, response, options.rawResponse);
|
|
74
57
|
}
|
|
75
58
|
const body = {
|
|
76
59
|
contentType: contentType ?? options.accept ?? "application/octet-stream",
|
|
@@ -81,7 +64,7 @@ export async function requestJson(options) {
|
|
|
81
64
|
if (options.verbose) {
|
|
82
65
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
|
|
83
66
|
}
|
|
84
|
-
return body;
|
|
67
|
+
return formatRawResponseResult(body, response, options.rawResponse);
|
|
85
68
|
}
|
|
86
69
|
const text = await response.text();
|
|
87
70
|
if (response.ok) {
|
|
@@ -89,19 +72,19 @@ export async function requestJson(options) {
|
|
|
89
72
|
if (options.verbose) {
|
|
90
73
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders)));
|
|
91
74
|
}
|
|
92
|
-
return undefined;
|
|
75
|
+
return formatRawResponseResult(undefined, response, options.rawResponse);
|
|
93
76
|
}
|
|
94
77
|
if (options.responseMode === "text") {
|
|
95
78
|
if (options.verbose) {
|
|
96
79
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
|
|
97
80
|
}
|
|
98
|
-
return text;
|
|
81
|
+
return formatRawResponseResult(text, response, options.rawResponse);
|
|
99
82
|
}
|
|
100
83
|
if (!isJsonContentType(contentType)) {
|
|
101
84
|
if (options.verbose) {
|
|
102
85
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
|
|
103
86
|
}
|
|
104
|
-
throw
|
|
87
|
+
throw createHttpError({
|
|
105
88
|
request,
|
|
106
89
|
response: {
|
|
107
90
|
status: response.status,
|
|
@@ -120,7 +103,7 @@ export async function requestJson(options) {
|
|
|
120
103
|
if (options.verbose) {
|
|
121
104
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
|
|
122
105
|
}
|
|
123
|
-
throw
|
|
106
|
+
throw createHttpError({
|
|
124
107
|
request,
|
|
125
108
|
response: {
|
|
126
109
|
status: response.status,
|
|
@@ -134,7 +117,7 @@ export async function requestJson(options) {
|
|
|
134
117
|
if (options.verbose) {
|
|
135
118
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
|
|
136
119
|
}
|
|
137
|
-
return body;
|
|
120
|
+
return formatRawResponseResult(body, response, options.rawResponse);
|
|
138
121
|
}
|
|
139
122
|
if (response.status === 401 && options.auth === "required") {
|
|
140
123
|
await options.tokenSource.invalidate?.(token).catch(() => undefined);
|
|
@@ -143,16 +126,76 @@ export async function requestJson(options) {
|
|
|
143
126
|
if (options.verbose) {
|
|
144
127
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
|
|
145
128
|
}
|
|
146
|
-
throw
|
|
129
|
+
throw createHttpError({
|
|
147
130
|
request,
|
|
148
131
|
response: {
|
|
149
132
|
status: response.status,
|
|
150
133
|
statusText: response.statusText,
|
|
151
134
|
headers: responseHeaders,
|
|
152
135
|
body
|
|
153
|
-
}
|
|
136
|
+
},
|
|
137
|
+
code: extractErrorCode(body),
|
|
138
|
+
requestId: response.headers.get("x-request-id") ?? response.headers.get("x-requestid") ?? undefined
|
|
154
139
|
});
|
|
155
140
|
}
|
|
141
|
+
async function fetchWithRetries(options, url, init) {
|
|
142
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
143
|
+
const retries = options.retries;
|
|
144
|
+
const maxRetries = retries?.max ?? 0;
|
|
145
|
+
let attempt = 0;
|
|
146
|
+
while (true) {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetchImpl(url, init);
|
|
149
|
+
if (retries === undefined ||
|
|
150
|
+
attempt >= maxRetries ||
|
|
151
|
+
!shouldRetryStatus(response.status, retries.retryOn)) {
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
await sleepBeforeRetry(response, retries, attempt);
|
|
155
|
+
attempt += 1;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const classified = classifyNetworkError(error, url) ?? error;
|
|
159
|
+
if (attempt >= maxRetries || retries === undefined) {
|
|
160
|
+
throw classified;
|
|
161
|
+
}
|
|
162
|
+
await sleepBeforeRetry(undefined, retries, attempt);
|
|
163
|
+
attempt += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function shouldRetryStatus(status, retryOn) {
|
|
168
|
+
return retryOn?.includes(status) === true;
|
|
169
|
+
}
|
|
170
|
+
async function sleepBeforeRetry(response, retries, attempt) {
|
|
171
|
+
const retryAfter = response?.headers.get("retry-after");
|
|
172
|
+
const delay = parseRetryAfter(retryAfter) ?? calculateBackoffDelay(attempt, retries.random);
|
|
173
|
+
await (retries.sleep ?? defaultSleep)(delay);
|
|
174
|
+
}
|
|
175
|
+
function parseRetryAfter(value) {
|
|
176
|
+
if (value === null || value === undefined) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const seconds = Number(value);
|
|
180
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
181
|
+
return seconds * 1000;
|
|
182
|
+
}
|
|
183
|
+
const timestamp = Date.parse(value);
|
|
184
|
+
if (!Number.isNaN(timestamp)) {
|
|
185
|
+
return Math.max(0, timestamp - Date.now());
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
function calculateBackoffDelay(attempt, random) {
|
|
190
|
+
const base = 100 * 2 ** attempt;
|
|
191
|
+
return Math.floor(base * (random ?? Math.random)());
|
|
192
|
+
}
|
|
193
|
+
function defaultSleep(ms) {
|
|
194
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
function formatRawResponseResult(data, response, rawResponse) {
|
|
197
|
+
return rawResponse === true ? { data, response } : data;
|
|
198
|
+
}
|
|
156
199
|
export async function prepareMultipartFileInputs(requestShape, options) {
|
|
157
200
|
if (options.bodyMode !== "multipart" ||
|
|
158
201
|
options.multipartBinaryFields === undefined ||
|
|
@@ -442,6 +485,21 @@ function parseResponseBody(text, contentType) {
|
|
|
442
485
|
}
|
|
443
486
|
return JSON.parse(text);
|
|
444
487
|
}
|
|
488
|
+
function extractErrorCode(body) {
|
|
489
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
const directCode = body.code;
|
|
493
|
+
if (typeof directCode === "string") {
|
|
494
|
+
return directCode;
|
|
495
|
+
}
|
|
496
|
+
const error = body.error;
|
|
497
|
+
if (error !== null && typeof error === "object" && !Array.isArray(error)) {
|
|
498
|
+
const nestedCode = error.code;
|
|
499
|
+
return typeof nestedCode === "string" ? nestedCode : undefined;
|
|
500
|
+
}
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
445
503
|
function isJsonContentType(contentType) {
|
|
446
504
|
if (contentType === null) {
|
|
447
505
|
return false;
|