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
package/dist/http.js CHANGED
@@ -1,26 +1,11 @@
1
+ import path from "node:path";
2
+ import { randomUUID } from "node:crypto";
1
3
  import { text as designText } from "toolcraft-design";
2
- import { UserError } from "toolcraft";
4
+ import { HttpError, UserError, createHttpError } from "toolcraft";
3
5
  import { classifyNetworkError } from "./network-error.js";
4
6
  import { redactHeaders, redactHeaderValue, redactSensitiveQueryValues } from "./redaction.js";
7
+ export { HttpError };
5
8
  const TRANSCRIPT_BODY_BYTE_LIMIT = 4 * 1024;
6
- export class HttpError extends Error {
7
- status;
8
- statusText;
9
- request;
10
- response;
11
- get body() {
12
- return this.response.body;
13
- }
14
- constructor(args) {
15
- super(args.message ??
16
- `${args.request.method} ${args.request.url} → ${args.response.status} ${args.response.statusText}`);
17
- this.name = "HttpError";
18
- this.status = args.response.status;
19
- this.statusText = args.response.statusText;
20
- this.request = args.request;
21
- this.response = args.response;
22
- }
23
- }
24
9
  export async function requestJson(options) {
25
10
  const token = options.auth === "none" ? undefined : await options.tokenSource.getToken();
26
11
  const method = options.method.toUpperCase();
@@ -37,7 +22,12 @@ export async function requestJson(options) {
37
22
  : JSON.stringify(options.body)
38
23
  : undefined;
39
24
  const url = buildRequestUrl(options);
40
- const headers = createHeaders(token, hasBody, options.headers, options.accept, options.bodyMode, options.contentType);
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);
41
31
  const writeStdout = options.writeStdout ?? process.stdout.write.bind(process.stdout);
42
32
  const writeStderr = options.writeStderr ?? process.stderr.write.bind(process.stderr);
43
33
  const requestLine = `${method} ${redactSensitiveQueryValues(url)}`;
@@ -48,18 +38,12 @@ export async function requestJson(options) {
48
38
  if (options.verbose) {
49
39
  writeStderr(formatTranscriptLines(formatVerboseRequestTranscript(method, url, headers, options.body)));
50
40
  }
51
- let response;
52
- try {
53
- response = await (options.fetch ?? globalThis.fetch)(url, {
54
- method,
55
- headers,
56
- body: serializedBody,
57
- signal: options.signal
58
- });
59
- }
60
- catch (error) {
61
- throw classifyNetworkError(error, url) ?? error;
62
- }
41
+ const response = await fetchWithRetries(options, url, {
42
+ method,
43
+ headers,
44
+ body: serializedBody,
45
+ signal: options.signal
46
+ });
63
47
  const contentType = response.headers.get("content-type");
64
48
  const request = createHttpErrorRequest(method, url, headers, options.body);
65
49
  const responseHeaders = redactHeaders(serializeHeaders(response.headers));
@@ -69,7 +53,7 @@ export async function requestJson(options) {
69
53
  if (options.verbose) {
70
54
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders)));
71
55
  }
72
- return undefined;
56
+ return formatRawResponseResult(undefined, response, options.rawResponse);
73
57
  }
74
58
  const body = {
75
59
  contentType: contentType ?? options.accept ?? "application/octet-stream",
@@ -80,7 +64,7 @@ export async function requestJson(options) {
80
64
  if (options.verbose) {
81
65
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
82
66
  }
83
- return body;
67
+ return formatRawResponseResult(body, response, options.rawResponse);
84
68
  }
85
69
  const text = await response.text();
86
70
  if (response.ok) {
@@ -88,19 +72,19 @@ export async function requestJson(options) {
88
72
  if (options.verbose) {
89
73
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders)));
90
74
  }
91
- return undefined;
75
+ return formatRawResponseResult(undefined, response, options.rawResponse);
92
76
  }
93
77
  if (options.responseMode === "text") {
94
78
  if (options.verbose) {
95
79
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
96
80
  }
97
- return text;
81
+ return formatRawResponseResult(text, response, options.rawResponse);
98
82
  }
99
83
  if (!isJsonContentType(contentType)) {
100
84
  if (options.verbose) {
101
85
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
102
86
  }
103
- throw new HttpError({
87
+ throw createHttpError({
104
88
  request,
105
89
  response: {
106
90
  status: response.status,
@@ -119,7 +103,7 @@ export async function requestJson(options) {
119
103
  if (options.verbose) {
120
104
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
121
105
  }
122
- throw new HttpError({
106
+ throw createHttpError({
123
107
  request,
124
108
  response: {
125
109
  status: response.status,
@@ -133,7 +117,7 @@ export async function requestJson(options) {
133
117
  if (options.verbose) {
134
118
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
135
119
  }
136
- return body;
120
+ return formatRawResponseResult(body, response, options.rawResponse);
137
121
  }
138
122
  if (response.status === 401 && options.auth === "required") {
139
123
  await options.tokenSource.invalidate?.(token).catch(() => undefined);
@@ -142,16 +126,128 @@ export async function requestJson(options) {
142
126
  if (options.verbose) {
143
127
  writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
144
128
  }
145
- throw new HttpError({
129
+ throw createHttpError({
146
130
  request,
147
131
  response: {
148
132
  status: response.status,
149
133
  statusText: response.statusText,
150
134
  headers: responseHeaders,
151
135
  body
152
- }
136
+ },
137
+ code: extractErrorCode(body),
138
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("x-requestid") ?? undefined
153
139
  });
154
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
+ }
199
+ export async function prepareMultipartFileInputs(requestShape, options) {
200
+ if (options.bodyMode !== "multipart" ||
201
+ options.multipartBinaryFields === undefined ||
202
+ options.multipartBinaryFields.length === 0 ||
203
+ options.fs === undefined ||
204
+ options.env === undefined ||
205
+ requestShape.body === undefined) {
206
+ return requestShape;
207
+ }
208
+ if (requestShape.body === null ||
209
+ typeof requestShape.body !== "object" ||
210
+ Array.isArray(requestShape.body)) {
211
+ return requestShape;
212
+ }
213
+ const body = { ...requestShape.body };
214
+ for (const field of options.multipartBinaryFields) {
215
+ const value = body[field];
216
+ if (typeof value !== "string") {
217
+ continue;
218
+ }
219
+ const filePath = resolveUserPath(value, options.env);
220
+ if (!(await options.fs.exists(filePath))) {
221
+ continue;
222
+ }
223
+ body[field] = {
224
+ data: await options.fs.readFile(filePath, "base64"),
225
+ filename: path.basename(filePath) || field
226
+ };
227
+ }
228
+ return {
229
+ ...requestShape,
230
+ body
231
+ };
232
+ }
233
+ export async function writeBinaryResponseOutput(result, outputPath, runtime) {
234
+ if (outputPath === undefined) {
235
+ return result;
236
+ }
237
+ if (runtime.fs === undefined || runtime.env === undefined) {
238
+ throw new UserError("Cannot write binary output without a Toolcraft file runtime.");
239
+ }
240
+ if (!isBinaryHttpResponse(result)) {
241
+ throw new UserError("Cannot write an empty binary response to an output path.");
242
+ }
243
+ const resolvedOutputPath = resolveUserPath(String(outputPath), runtime.env);
244
+ await runtime.fs.writeFile(resolvedOutputPath, result.data, { encoding: "base64" });
245
+ return {
246
+ output: resolvedOutputPath,
247
+ byteLength: result.byteLength,
248
+ contentType: result.contentType
249
+ };
250
+ }
155
251
  function buildRequestUrl(options) {
156
252
  const resolvedPath = substitutePathParams(options.path, options.pathParams);
157
253
  const baseUrl = new URL(options.baseUrl);
@@ -223,6 +319,22 @@ function decodeBase64Body(body) {
223
319
  }
224
320
  return Uint8Array.from(Buffer.from(body, "base64")).buffer;
225
321
  }
322
+ function decodeMultipartBinaryValue(value, fallbackFilename) {
323
+ if (typeof value === "string") {
324
+ return {
325
+ data: value,
326
+ filename: fallbackFilename
327
+ };
328
+ }
329
+ if (value !== null &&
330
+ typeof value === "object" &&
331
+ !Array.isArray(value) &&
332
+ typeof value.data === "string" &&
333
+ typeof value.filename === "string") {
334
+ return value;
335
+ }
336
+ throw new UserError("Multipart binary request fields must be base64 strings or resolved file inputs.");
337
+ }
226
338
  function serializeMultipartBody(body, binaryFields = []) {
227
339
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
228
340
  throw new UserError("Multipart request bodies must be objects.");
@@ -233,7 +345,8 @@ function serializeMultipartBody(body, binaryFields = []) {
233
345
  if (value === undefined)
234
346
  continue;
235
347
  if (binary.has(key)) {
236
- form.append(key, new Blob([decodeBase64Body(value)]), key);
348
+ const file = decodeMultipartBinaryValue(value, key);
349
+ form.append(key, new Blob([decodeBase64Body(file.data)]), file.filename);
237
350
  }
238
351
  else if (Array.isArray(value)) {
239
352
  for (const item of value)
@@ -245,6 +358,20 @@ function serializeMultipartBody(body, binaryFields = []) {
245
358
  }
246
359
  return form;
247
360
  }
361
+ function resolveUserPath(userPath, env) {
362
+ if (path.isAbsolute(userPath)) {
363
+ return path.normalize(userPath);
364
+ }
365
+ return path.resolve(env.get("INIT_CWD") ?? process.cwd(), userPath);
366
+ }
367
+ function isBinaryHttpResponse(value) {
368
+ return (value !== null &&
369
+ typeof value === "object" &&
370
+ value.encoding === "base64" &&
371
+ typeof value.data === "string" &&
372
+ typeof value.byteLength === "number" &&
373
+ typeof value.contentType === "string");
374
+ }
248
375
  function isQueryObject(value) {
249
376
  return value !== null && typeof value === "object" && !Array.isArray(value);
250
377
  }
@@ -358,6 +485,21 @@ function parseResponseBody(text, contentType) {
358
485
  }
359
486
  return JSON.parse(text);
360
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
+ }
361
503
  function isJsonContentType(contentType) {
362
504
  if (contentType === null) {
363
505
  return false;
package/dist/index.d.ts CHANGED
@@ -2,16 +2,21 @@ export { defineApiCommand } from "./api-command.js";
2
2
  export { defineClient } from "./define-client.js";
3
3
  export { generate } from "./generate.js";
4
4
  export type { GenerateOptions, GeneratedFile, OpenApiDocument } from "./generate.js";
5
+ export { SUPPORTED_TOOLCRAFT_EDITION, mergeToolcraftConfig, readToolcraftConfig, validateToolcraftConfig } from "./config.js";
6
+ export type { ToolcraftConfig, ToolcraftMethodConfig, ToolcraftResourceConfig } from "./config.js";
7
+ export { diagnose } from "./diagnose.js";
8
+ export { DIAGNOSTIC_CODES, formatDiagnostic, formatDiagnostics } from "./diagnostics.js";
9
+ export type { Diagnostic, DiagnosticSeverity } from "./diagnostics.js";
5
10
  export { inspectOpenApiDocument } from "./inspect.js";
6
11
  export type { OpenApiInspectionOperation, OpenApiInspectionReport } from "./inspect.js";
7
12
  export { inspectOpenApiSource } from "./inspect-source.js";
8
13
  export type { InspectOpenApiSourceOptions, OpenApiInspectionSource } from "./inspect-source.js";
9
14
  export { renderOpenApiInspection } from "./render-inspection.js";
10
- export { commandsFromSpec, defineClientFromSpec } from "./runtime.js";
15
+ export { commandsFromSpec, defineClientFromSpec, resolveOpenApiBaseUrl } from "./runtime.js";
11
16
  export type { CommandsFromSpecOptions, DefineClientFromSpecOptions, OpenApiDocumentSource } from "./runtime.js";
12
17
  export type { DefineClientOptions, DefinedClient, OpenApiClientServices } from "./define-client.js";
13
18
  export type { AuthProvider, CommandContributor, TokenSource } from "./auth/types.js";
14
19
  export { bearerTokenAuth } from "./auth/bearer-token-auth.js";
15
20
  export type { BearerTokenAuthOptions } from "./auth/bearer-token-auth.js";
16
- export { HttpError, requestJson } from "./http.js";
21
+ export { HttpError, prepareMultipartFileInputs, requestJson, writeBinaryResponseOutput } from "./http.js";
17
22
  export type { BinaryHttpResponse, HttpErrorRequest, HttpErrorResponse, HttpRequestOptions, QueryValue } from "./http.js";
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  export { defineApiCommand } from "./api-command.js";
2
2
  export { defineClient } from "./define-client.js";
3
3
  export { generate } from "./generate.js";
4
+ export { SUPPORTED_TOOLCRAFT_EDITION, mergeToolcraftConfig, readToolcraftConfig, validateToolcraftConfig } from "./config.js";
5
+ export { diagnose } from "./diagnose.js";
6
+ export { DIAGNOSTIC_CODES, formatDiagnostic, formatDiagnostics } from "./diagnostics.js";
4
7
  export { inspectOpenApiDocument } from "./inspect.js";
5
8
  export { inspectOpenApiSource } from "./inspect-source.js";
6
9
  export { renderOpenApiInspection } from "./render-inspection.js";
7
- export { commandsFromSpec, defineClientFromSpec } from "./runtime.js";
10
+ export { commandsFromSpec, defineClientFromSpec, resolveOpenApiBaseUrl } from "./runtime.js";
8
11
  export { bearerTokenAuth } from "./auth/bearer-token-auth.js";
9
- export { HttpError, requestJson } from "./http.js";
12
+ export { HttpError, prepareMultipartFileInputs, requestJson, writeBinaryResponseOutput } from "./http.js";
package/dist/runtime.d.ts CHANGED
@@ -9,5 +9,12 @@ export interface CommandsFromSpecOptions {
9
9
  fs?: OpenApiSourceFileSystem;
10
10
  }
11
11
  export type DefineClientFromSpecOptions<TServices extends object = Record<string, never>> = Omit<DefineClientOptions<TServices>, "commands"> & CommandsFromSpecOptions;
12
+ export interface ResolveOpenApiBaseUrlOptions {
13
+ document: OpenApiDocument;
14
+ environments?: Record<string, string>;
15
+ environment?: string;
16
+ env?: Record<string, string | undefined>;
17
+ }
12
18
  export declare function commandsFromSpec(source: OpenApiDocumentSource, options?: CommandsFromSpecOptions): Promise<CommandNode<OpenApiClientServices>[]>;
13
19
  export declare function defineClientFromSpec<TServices extends object = Record<string, never>>(spec: OpenApiDocumentSource, options: DefineClientFromSpecOptions<TServices>): Promise<DefinedClient<TServices>>;
20
+ export declare function resolveOpenApiBaseUrl(options: ResolveOpenApiBaseUrlOptions): string | undefined;
package/dist/runtime.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
- import { defineCommand, defineGroup, S } from "toolcraft";
2
+ import { UserError, defineCommand, defineGroup, S } from "toolcraft";
3
3
  import { defineClient } from "./define-client.js";
4
4
  import { collectSchemaOptionEntries, collectGeneratedCommands } from "./generate.js";
5
5
  import { groupByNoun } from "./group-by-noun.js";
6
- import { requestJson } from "./http.js";
6
+ import { prepareMultipartFileInputs, requestJson, writeBinaryResponseOutput } from "./http.js";
7
7
  import { buildRequestShape, executePreflightBlocks } from "./interpreter.js";
8
8
  import { parseOpenApiDocument, readOpenApiSourceText } from "./spec-source.js";
9
9
  const RUNTIME_COMMAND_SCOPE = ["cli", "mcp", "sdk"];
@@ -31,6 +31,24 @@ async function resolveDocument(source, options) {
31
31
  });
32
32
  return parseOpenApiDocument(sourceText, source);
33
33
  }
34
+ export function resolveOpenApiBaseUrl(options) {
35
+ const environments = options.environments;
36
+ if (environments !== undefined && Object.keys(environments).length > 0) {
37
+ const selectedName = options.environment ?? options.env?.TOOLCRAFT_OPENAPI_ENV ?? Object.keys(environments)[0];
38
+ const selectedUrl = selectedName === undefined ? undefined : environments[selectedName];
39
+ if (selectedUrl !== undefined) {
40
+ return normalizeBaseUrl(selectedUrl);
41
+ }
42
+ if (selectedName !== undefined) {
43
+ throw new UserError(`Unknown OpenAPI environment ${JSON.stringify(selectedName)}. Available: ${Object.keys(environments).join(", ")}.`);
44
+ }
45
+ }
46
+ const server = options.document.servers?.[0]?.url;
47
+ return server === undefined ? undefined : normalizeBaseUrl(server);
48
+ }
49
+ function normalizeBaseUrl(value) {
50
+ return new URL(value).toString().replace(/\/$/, "");
51
+ }
34
52
  function createRuntimeGroups(commands) {
35
53
  return groupByNoun(commands).map(({ noun, commands: nounCommands }) => defineGroup({
36
54
  name: noun,
@@ -49,10 +67,16 @@ function createRuntimeCommand(command) {
49
67
  });
50
68
  }
51
69
  function createRuntimeHandler(command) {
52
- return async ({ params, baseUrl, tokenSource, fetch }) => {
70
+ return async ({ params, baseUrl, tokenSource, fetch, fs, env }) => {
53
71
  const resolvedValues = executePreflightBlocks(command.preflightBlocks, params);
54
72
  const requestShape = buildRequestShape(command.requestFields, command.sectionRenders, command.optionalSections, params, resolvedValues);
55
- return requestJson({
73
+ const preparedRequestShape = await prepareMultipartFileInputs(requestShape, {
74
+ bodyMode: command.bodyMode,
75
+ multipartBinaryFields: command.multipartBinaryFields,
76
+ fs,
77
+ env
78
+ });
79
+ const result = await requestJson({
56
80
  baseUrl: command.baseUrl ?? baseUrl,
57
81
  path: command.path,
58
82
  method: command.method,
@@ -66,8 +90,9 @@ function createRuntimeHandler(command) {
66
90
  fetch,
67
91
  dryRun: params.dryRun,
68
92
  verbose: params.verbose,
69
- ...requestShape
93
+ ...preparedRequestShape
70
94
  });
95
+ return writeBinaryResponseOutput(result, command.responseMode === "binary" ? params.output : undefined, { fs, env });
71
96
  };
72
97
  }
73
98
  function createRuntimeParamSchema(param) {
@@ -0,0 +1,35 @@
1
+ # @poe-code/frontmatter
2
+
3
+ Shared YAML frontmatter parsing for poe-code packages.
4
+
5
+ ## API
6
+
7
+ ```ts
8
+ import {
9
+ FrontmatterParseError,
10
+ parseFrontmatter,
11
+ parseFrontmatterDocument,
12
+ stringifyFrontmatter
13
+ } from "@poe-code/frontmatter";
14
+ ```
15
+
16
+ - `parseFrontmatter(source)` returns `{ frontmatter, body }`.
17
+ - `parseFrontmatterDocument(source)` returns `{ frontmatter, body, errors, lineCounter }` for callers that need YAML diagnostics.
18
+ - `stringifyFrontmatter(frontmatter, body)` writes `---` fences, YAML, and the body.
19
+ - `FrontmatterParseError` is thrown for malformed frontmatter, invalid YAML, non-object frontmatter, and stringify failures.
20
+
21
+ When no leading frontmatter block exists, parsing returns `{ frontmatter: {}, body: source }`.
22
+ The returned `body` is sliced from the original input and is otherwise byte-for-byte unchanged.
23
+ YAML mappings that contain `__proto__` keep it as an own property without changing the object's prototype.
24
+
25
+ Duplicate mapping keys use YAML's last-wins behavior by default, matching the
26
+ legacy frontmatter readers. Callers that need strict YAML mappings can pass
27
+ `{ uniqueKeys: true }` to `parseFrontmatter` or `parseFrontmatterDocument`.
28
+
29
+ ## Environment
30
+
31
+ This package reads no environment variables.
32
+
33
+ ## Config
34
+
35
+ This package has no configuration options.
@@ -0,0 +1,8 @@
1
+ export type FrontmatterBlock = {
2
+ raw: string;
3
+ body: string;
4
+ };
5
+ export type SplitFrontmatterResult = FrontmatterBlock | {
6
+ body: string;
7
+ };
8
+ export declare function splitFrontmatterBlock(source: string): SplitFrontmatterResult;
@@ -0,0 +1,72 @@
1
+ export function splitFrontmatterBlock(source) {
2
+ const content = source.startsWith("\uFEFF") ? source.slice(1) : source;
3
+ const opening = readOpeningFence(content);
4
+ if (opening === undefined) {
5
+ return { body: source };
6
+ }
7
+ const closing = findClosingFence(content, opening.next);
8
+ if (closing === undefined) {
9
+ throw new Error("Missing YAML frontmatter end delimiter (---).");
10
+ }
11
+ return {
12
+ raw: content.slice(opening.next, closing.index),
13
+ body: content.slice(closing.end + closing.lineBreakLength)
14
+ };
15
+ }
16
+ function readOpeningFence(source) {
17
+ if (!source.startsWith("---")) {
18
+ return undefined;
19
+ }
20
+ const lineEnd = findLineEnd(source, 0);
21
+ if (lineEnd.lineBreakLength === 0 || source.slice(0, lineEnd.index) !== "---") {
22
+ return undefined;
23
+ }
24
+ return { next: lineEnd.index + lineEnd.lineBreakLength };
25
+ }
26
+ function findClosingFence(source, start) {
27
+ let lineStart = start;
28
+ while (lineStart <= source.length) {
29
+ const lineEnd = findLineEnd(source, lineStart);
30
+ if (isClosingFenceLine(source.slice(lineStart, lineEnd.index))) {
31
+ return {
32
+ index: lineStart,
33
+ end: lineEnd.index,
34
+ lineBreakLength: lineEnd.lineBreakLength
35
+ };
36
+ }
37
+ if (lineEnd.lineBreakLength === 0) {
38
+ break;
39
+ }
40
+ lineStart = lineEnd.index + lineEnd.lineBreakLength;
41
+ }
42
+ return undefined;
43
+ }
44
+ function isClosingFenceLine(line) {
45
+ if (!line.startsWith("---")) {
46
+ return false;
47
+ }
48
+ for (let index = 3; index < line.length; index += 1) {
49
+ const character = line[index];
50
+ if (character !== " " && character !== "\t") {
51
+ return false;
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+ function findLineEnd(source, start) {
57
+ let index = start;
58
+ while (index < source.length) {
59
+ const character = source[index];
60
+ if (character === "\n") {
61
+ return { index, lineBreakLength: 1 };
62
+ }
63
+ if (character === "\r") {
64
+ return {
65
+ index,
66
+ lineBreakLength: source[index + 1] === "\n" ? 2 : 1
67
+ };
68
+ }
69
+ index += 1;
70
+ }
71
+ return { index: source.length, lineBreakLength: 0 };
72
+ }
@@ -0,0 +1,3 @@
1
+ export { splitFrontmatterBlock, type FrontmatterBlock, type SplitFrontmatterResult } from "./fences.js";
2
+ export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument, type ParsedFrontmatter, type ParsedFrontmatterDocument, type ParseFrontmatterOptions } from "./parse.js";
3
+ export { stringifyFrontmatter } from "./stringify.js";
@@ -0,0 +1,3 @@
1
+ export { splitFrontmatterBlock } from "./fences.js";
2
+ export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument } from "./parse.js";
3
+ export { stringifyFrontmatter } from "./stringify.js";
@@ -0,0 +1,20 @@
1
+ import { LineCounter } from "yaml";
2
+ export interface ParsedFrontmatter {
3
+ frontmatter: Record<string, unknown>;
4
+ body: string;
5
+ }
6
+ export interface ParsedFrontmatterDocument extends ParsedFrontmatter {
7
+ errors: readonly {
8
+ message: string;
9
+ pos?: [number, number];
10
+ }[];
11
+ lineCounter: LineCounter;
12
+ }
13
+ export interface ParseFrontmatterOptions {
14
+ uniqueKeys?: boolean;
15
+ }
16
+ export declare class FrontmatterParseError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ export declare function parseFrontmatter(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatter;
20
+ export declare function parseFrontmatterDocument(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatterDocument;