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.
Files changed (62) hide show
  1. package/README.md +67 -0
  2. package/dist/bin/generate.d.ts +9 -0
  3. package/dist/bin/generate.js +122 -6
  4. package/dist/config.d.ts +53 -0
  5. package/dist/config.js +266 -0
  6. package/dist/define-client.js +4 -0
  7. package/dist/diagnose.d.ts +4 -0
  8. package/dist/diagnose.js +93 -0
  9. package/dist/diagnostics.d.ts +19 -0
  10. package/dist/diagnostics.js +19 -0
  11. package/dist/generate.d.ts +13 -1
  12. package/dist/generate.js +165 -12
  13. package/dist/http.d.ts +30 -25
  14. package/dist/http.js +184 -42
  15. package/dist/index.d.ts +7 -2
  16. package/dist/index.js +5 -2
  17. package/dist/runtime.d.ts +7 -0
  18. package/dist/runtime.js +30 -5
  19. package/node_modules/@poe-code/frontmatter/README.md +35 -0
  20. package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +8 -0
  21. package/node_modules/@poe-code/frontmatter/dist/fences.js +72 -0
  22. package/node_modules/@poe-code/frontmatter/dist/index.d.ts +3 -0
  23. package/node_modules/@poe-code/frontmatter/dist/index.js +3 -0
  24. package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +20 -0
  25. package/node_modules/@poe-code/frontmatter/dist/parse.js +137 -0
  26. package/node_modules/@poe-code/frontmatter/dist/stringify.d.ts +1 -0
  27. package/node_modules/@poe-code/frontmatter/dist/stringify.js +35 -0
  28. package/node_modules/@poe-code/frontmatter/package.json +25 -0
  29. package/node_modules/toolcraft-design/README.md +12 -0
  30. package/node_modules/toolcraft-design/dist/prompts/index.d.ts +39 -14
  31. package/node_modules/toolcraft-design/dist/prompts/index.js +10 -6
  32. package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.d.ts +2 -0
  33. package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.js +4 -0
  34. package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.d.ts +9 -0
  35. package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.js +47 -0
  36. package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +55 -0
  37. package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +274 -0
  38. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +20 -0
  39. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +53 -0
  40. package/node_modules/toolcraft-design/dist/prompts/interactive/index.d.ts +6 -0
  41. package/node_modules/toolcraft-design/dist/prompts/interactive/index.js +6 -0
  42. package/node_modules/toolcraft-design/dist/prompts/interactive/keys.d.ts +2 -0
  43. package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +28 -0
  44. package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.d.ts +13 -0
  45. package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +131 -0
  46. package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.d.ts +10 -0
  47. package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.js +52 -0
  48. package/node_modules/toolcraft-design/dist/prompts/interactive/password.d.ts +10 -0
  49. package/node_modules/toolcraft-design/dist/prompts/interactive/password.js +55 -0
  50. package/node_modules/toolcraft-design/dist/prompts/interactive/select.d.ts +18 -0
  51. package/node_modules/toolcraft-design/dist/prompts/interactive/select.js +89 -0
  52. package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.d.ts +21 -0
  53. package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.js +32 -0
  54. package/node_modules/toolcraft-design/dist/prompts/interactive/text.d.ts +12 -0
  55. package/node_modules/toolcraft-design/dist/prompts/interactive/text.js +60 -0
  56. package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.d.ts +4 -0
  57. package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.js +18 -0
  58. package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.d.ts +1 -1
  59. package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.js +1 -1
  60. package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.js +20 -453
  61. package/node_modules/toolcraft-design/package.json +6 -3
  62. package/package.json +7 -7
@@ -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
+ }
@@ -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, ...TRANSPORT_PARAMS].map((param) => param.paramName)));
227
- const params = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...TRANSPORT_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";', 'import { requestJson, defineApiCommand } from "toolcraft-openapi";', "", `export const ${options.exportName} = defineApiCommand({`, ` name: ${JSON.stringify(options.verb)},`);
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(" handler: async ({ params, baseUrl, tokenSource, fetch }) => {");
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
- lines.push(" return requestJson({");
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
- lines.push(...renderRequestShape(options.requestFields, options.sectionRenders, options.optionalSections));
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
- export declare class HttpError extends Error {
49
- readonly status: number;
50
- readonly statusText: string;
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>;