toolcraft-openapi 0.0.17 → 0.0.19
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/dist/bin/generate.js +7 -0
- package/dist/define-client.js +2 -2
- package/dist/generate.js +2 -2
- package/dist/http.d.ts +21 -2
- package/dist/http.js +147 -22
- package/dist/index.d.ts +1 -1
- package/dist/lock.d.ts +1 -1
- package/dist/lock.js +109 -5
- package/dist/mock/fetch.js +1 -1
- package/dist/network-error.d.ts +2 -0
- package/dist/network-error.js +83 -0
- package/dist/spec-source.js +103 -3
- package/node_modules/@poe-code/design-system/dist/acp/components.js +15 -13
- package/node_modules/@poe-code/design-system/dist/components/color.d.ts +31 -0
- package/node_modules/@poe-code/design-system/dist/components/color.js +101 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/index.js +2 -0
- package/node_modules/@poe-code/design-system/dist/components/logger.js +2 -2
- package/node_modules/@poe-code/design-system/dist/components/symbols.js +3 -3
- package/node_modules/@poe-code/design-system/dist/components/table.js +191 -40
- package/node_modules/@poe-code/design-system/dist/components/template.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/components/template.js +271 -0
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +11 -3
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +20 -13
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/index.js +5 -0
- package/node_modules/@poe-code/design-system/dist/internal/color-support.d.ts +9 -0
- package/node_modules/@poe-code/design-system/dist/internal/color-support.js +12 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/cancel.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +4 -4
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +5 -5
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/outro.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/spinner.js +3 -3
- package/node_modules/@poe-code/design-system/dist/static/menu.js +5 -5
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +8 -8
- package/node_modules/@poe-code/design-system/dist/tokens/colors.js +29 -29
- package/node_modules/@poe-code/design-system/dist/tokens/typography.js +6 -6
- package/node_modules/@poe-code/design-system/package.json +6 -3
- package/package.json +2 -4
package/dist/bin/generate.js
CHANGED
|
@@ -57,6 +57,13 @@ export async function runGenerateCli(argv = process.argv, services = {
|
|
|
57
57
|
services.stderr.write(`${error.message}\n`);
|
|
58
58
|
return 1;
|
|
59
59
|
}
|
|
60
|
+
if (error instanceof Error && error.name === "ToolcraftBugError") {
|
|
61
|
+
services.stderr.write(`toolcraft hit an internal invariant: ${error.message}\n` +
|
|
62
|
+
`This is a bug in toolcraft or in the command definition; ` +
|
|
63
|
+
`it cannot be worked around by changing argv. ` +
|
|
64
|
+
`File an issue.\n`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
60
67
|
throw error;
|
|
61
68
|
}
|
|
62
69
|
}
|
package/dist/define-client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineCommand, defineGroup, UserError } from "toolcraft";
|
|
1
|
+
import { ToolcraftBugError, defineCommand, defineGroup, UserError } from "toolcraft";
|
|
2
2
|
import { toMcpPrefix } from "./naming.js";
|
|
3
3
|
const CLI_SCOPE = ["cli"];
|
|
4
4
|
export function defineClient(options) {
|
|
@@ -61,7 +61,7 @@ function registerSource(node, source, nodeSources) {
|
|
|
61
61
|
function getRegisteredSource(nodeSources, node) {
|
|
62
62
|
const source = nodeSources.get(node);
|
|
63
63
|
if (source === undefined) {
|
|
64
|
-
throw new
|
|
64
|
+
throw new ToolcraftBugError("merged command node is missing source metadata.");
|
|
65
65
|
}
|
|
66
66
|
return source;
|
|
67
67
|
}
|
package/dist/generate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UserError } from "toolcraft";
|
|
1
|
+
import { ToolcraftBugError, UserError } from "toolcraft";
|
|
2
2
|
import { METHOD_DEFAULTS, deriveNoun, deriveVerb, isIdentifierName, normalizeParamName, toCamelCase, toPascalCase } from "./naming.js";
|
|
3
3
|
import { groupByNoun } from "./group-by-noun.js";
|
|
4
4
|
import { renderPreflightBlock, renderRequestShape } from "./interpreter.js";
|
|
@@ -557,7 +557,7 @@ const FIELD_ASSEMBLERS = {
|
|
|
557
557
|
};
|
|
558
558
|
function expectQueryArraySerialization(querySerialization) {
|
|
559
559
|
if (querySerialization === undefined) {
|
|
560
|
-
throw new
|
|
560
|
+
throw new ToolcraftBugError("Missing query array serialization for generated query array field.");
|
|
561
561
|
}
|
|
562
562
|
return querySerialization;
|
|
563
563
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -17,10 +17,29 @@ export interface HttpRequestOptions {
|
|
|
17
17
|
writeStdout?: (chunk: string) => void;
|
|
18
18
|
writeStderr?: (chunk: string) => void;
|
|
19
19
|
}
|
|
20
|
+
export interface HttpErrorRequest {
|
|
21
|
+
method: string;
|
|
22
|
+
url: string;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
body?: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface HttpErrorResponse {
|
|
27
|
+
status: number;
|
|
28
|
+
statusText: string;
|
|
29
|
+
headers: Record<string, string>;
|
|
30
|
+
body: unknown;
|
|
31
|
+
}
|
|
20
32
|
export declare class HttpError extends Error {
|
|
21
33
|
readonly status: number;
|
|
22
|
-
readonly
|
|
23
|
-
|
|
34
|
+
readonly statusText: string;
|
|
35
|
+
readonly request: HttpErrorRequest;
|
|
36
|
+
readonly response: HttpErrorResponse;
|
|
37
|
+
get body(): unknown;
|
|
38
|
+
constructor(args: {
|
|
39
|
+
request: HttpErrorRequest;
|
|
40
|
+
response: HttpErrorResponse;
|
|
41
|
+
message?: string;
|
|
42
|
+
});
|
|
24
43
|
}
|
|
25
44
|
export declare function requestJson<TResult = unknown>(options: HttpRequestOptions): Promise<TResult | undefined>;
|
|
26
45
|
export {};
|
package/dist/http.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { text as designText } from "@poe-code/design-system";
|
|
2
2
|
import { UserError } from "toolcraft";
|
|
3
|
+
import { classifyNetworkError } from "./network-error.js";
|
|
4
|
+
const TRANSCRIPT_BODY_BYTE_LIMIT = 4 * 1024;
|
|
3
5
|
export class HttpError extends Error {
|
|
4
6
|
status;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
statusText;
|
|
8
|
+
request;
|
|
9
|
+
response;
|
|
10
|
+
get body() {
|
|
11
|
+
return this.response.body;
|
|
12
|
+
}
|
|
13
|
+
constructor(args) {
|
|
14
|
+
super(args.message ??
|
|
15
|
+
`${args.request.method} ${args.request.url} → ${args.response.status} ${args.response.statusText}`);
|
|
8
16
|
this.name = "HttpError";
|
|
9
|
-
this.status = status;
|
|
10
|
-
this.
|
|
17
|
+
this.status = args.response.status;
|
|
18
|
+
this.statusText = args.response.statusText;
|
|
19
|
+
this.request = args.request;
|
|
20
|
+
this.response = args.response;
|
|
11
21
|
}
|
|
12
22
|
}
|
|
13
23
|
export async function requestJson(options) {
|
|
@@ -20,34 +30,73 @@ export async function requestJson(options) {
|
|
|
20
30
|
const writeStdout = options.writeStdout ?? process.stdout.write.bind(process.stdout);
|
|
21
31
|
const writeStderr = options.writeStderr ?? process.stderr.write.bind(process.stderr);
|
|
22
32
|
const requestLine = `${method} ${url}`;
|
|
23
|
-
if (options.verbose) {
|
|
24
|
-
writeStderr(`${designText.muted(requestLine)}\n`);
|
|
25
|
-
}
|
|
26
33
|
if (options.dryRun) {
|
|
27
34
|
writeStdout(formatDryRunOutput(requestLine, headers, options.body));
|
|
28
35
|
return undefined;
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
method,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
if (options.verbose) {
|
|
38
|
+
writeStderr(formatTranscriptLines(formatVerboseRequestTranscript(method, url, headers, options.body)));
|
|
39
|
+
}
|
|
40
|
+
let response;
|
|
41
|
+
try {
|
|
42
|
+
response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: serializedBody,
|
|
46
|
+
signal: options.signal
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw classifyNetworkError(error, url) ?? error;
|
|
51
|
+
}
|
|
36
52
|
const text = await response.text();
|
|
37
53
|
const contentType = response.headers.get("content-type");
|
|
54
|
+
const request = createHttpErrorRequest(method, url, headers, options.body);
|
|
55
|
+
const responseHeaders = serializeHeaders(response.headers);
|
|
38
56
|
if (response.ok) {
|
|
39
57
|
if (text.length === 0) {
|
|
58
|
+
if (options.verbose) {
|
|
59
|
+
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders)));
|
|
60
|
+
}
|
|
40
61
|
return undefined;
|
|
41
62
|
}
|
|
42
63
|
if (!isJsonContentType(contentType)) {
|
|
43
|
-
|
|
64
|
+
if (options.verbose) {
|
|
65
|
+
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
|
|
66
|
+
}
|
|
67
|
+
throw new HttpError({
|
|
68
|
+
request,
|
|
69
|
+
response: {
|
|
70
|
+
status: response.status,
|
|
71
|
+
statusText: response.statusText,
|
|
72
|
+
headers: responseHeaders,
|
|
73
|
+
body: text
|
|
74
|
+
},
|
|
75
|
+
message: `Expected a JSON response body but received content-type ${JSON.stringify(contentType ?? "<missing>")}.`
|
|
76
|
+
});
|
|
44
77
|
}
|
|
45
|
-
|
|
78
|
+
const body = JSON.parse(text);
|
|
79
|
+
if (options.verbose) {
|
|
80
|
+
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
|
|
81
|
+
}
|
|
82
|
+
return body;
|
|
46
83
|
}
|
|
47
84
|
if (response.status === 401) {
|
|
48
85
|
await options.tokenSource.invalidate?.();
|
|
49
86
|
}
|
|
50
|
-
|
|
87
|
+
const body = parseResponseBody(text, contentType);
|
|
88
|
+
if (options.verbose) {
|
|
89
|
+
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
|
|
90
|
+
}
|
|
91
|
+
throw new HttpError({
|
|
92
|
+
request,
|
|
93
|
+
response: {
|
|
94
|
+
status: response.status,
|
|
95
|
+
statusText: response.statusText,
|
|
96
|
+
headers: responseHeaders,
|
|
97
|
+
body
|
|
98
|
+
}
|
|
99
|
+
});
|
|
51
100
|
}
|
|
52
101
|
function buildRequestUrl(options) {
|
|
53
102
|
const resolvedPath = substitutePathParams(options.path, options.pathParams);
|
|
@@ -87,25 +136,101 @@ function appendQueryValue(searchParams, key, value) {
|
|
|
87
136
|
function createHeaders(token, hasBody) {
|
|
88
137
|
return {
|
|
89
138
|
...(token === undefined ? {} : { Authorization: `Bearer ${token}` }),
|
|
90
|
-
...(hasBody ? { "Content-Type": "application/json" } : {})
|
|
139
|
+
...(hasBody ? { "Content-Type": "application/json" } : {})
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function createHttpErrorRequest(method, url, headers, body) {
|
|
143
|
+
return {
|
|
144
|
+
method,
|
|
145
|
+
url,
|
|
146
|
+
headers: redactHeaders(headers),
|
|
147
|
+
...(body === undefined ? {} : { body })
|
|
91
148
|
};
|
|
92
149
|
}
|
|
150
|
+
function serializeHeaders(headers) {
|
|
151
|
+
return Object.fromEntries(headers.entries());
|
|
152
|
+
}
|
|
153
|
+
function redactHeaders(headers) {
|
|
154
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, redactHeaderValue(key, value)]));
|
|
155
|
+
}
|
|
156
|
+
function redactHeaderValue(key, value) {
|
|
157
|
+
if (key.toLowerCase() === "authorization" && value.startsWith("Bearer ")) {
|
|
158
|
+
return "Bearer ****";
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
93
162
|
function formatDryRunOutput(requestLine, headers, body) {
|
|
94
163
|
const lines = [
|
|
95
164
|
requestLine,
|
|
96
165
|
...Object.entries(headers).map(([key, value]) => {
|
|
97
|
-
const headerValue = key
|
|
98
|
-
? "Bearer ****"
|
|
99
|
-
: value;
|
|
166
|
+
const headerValue = redactHeaderValue(key, value);
|
|
100
167
|
return `${key}: ${headerValue}`;
|
|
101
168
|
}),
|
|
102
|
-
""
|
|
169
|
+
""
|
|
103
170
|
];
|
|
104
171
|
if (body !== undefined) {
|
|
105
172
|
lines.push(JSON.stringify(body));
|
|
106
173
|
}
|
|
107
174
|
return `${lines.join("\n")}\n`;
|
|
108
175
|
}
|
|
176
|
+
function formatVerboseRequestTranscript(method, url, headers, body) {
|
|
177
|
+
const lines = [
|
|
178
|
+
`→ ${method} ${url}`,
|
|
179
|
+
...Object.entries(headers).map(([key, value]) => {
|
|
180
|
+
const headerValue = redactHeaderValue(key, value);
|
|
181
|
+
return ` ${key}: ${headerValue}`;
|
|
182
|
+
})
|
|
183
|
+
];
|
|
184
|
+
if (body !== undefined) {
|
|
185
|
+
lines.push(...indentTranscriptBlock(formatTranscriptBody(body)));
|
|
186
|
+
}
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
189
|
+
function formatVerboseResponseTranscript(response, headers, body) {
|
|
190
|
+
const lines = [
|
|
191
|
+
`← ${response.status} ${response.statusText}`,
|
|
192
|
+
...Object.entries(headers).map(([key, value]) => ` ${key}: ${value}`)
|
|
193
|
+
];
|
|
194
|
+
if (body !== undefined) {
|
|
195
|
+
lines.push(...indentTranscriptBlock(formatTranscriptBody(body)));
|
|
196
|
+
}
|
|
197
|
+
return lines;
|
|
198
|
+
}
|
|
199
|
+
function formatTranscriptLines(lines) {
|
|
200
|
+
return `${lines.map((line) => formatTranscriptLine(line)).join("\n")}\n`;
|
|
201
|
+
}
|
|
202
|
+
function formatTranscriptLine(line) {
|
|
203
|
+
if (!process.stderr.isTTY) {
|
|
204
|
+
return line;
|
|
205
|
+
}
|
|
206
|
+
return designText.muted(line);
|
|
207
|
+
}
|
|
208
|
+
function formatTranscriptBody(body) {
|
|
209
|
+
const formatted = typeof body === "string" ? body : JSON.stringify(body, null, 2);
|
|
210
|
+
return truncateTranscriptBody(formatted ?? String(body));
|
|
211
|
+
}
|
|
212
|
+
function indentTranscriptBlock(value) {
|
|
213
|
+
return value.split("\n").map((line) => ` ${line}`);
|
|
214
|
+
}
|
|
215
|
+
function truncateTranscriptBody(value) {
|
|
216
|
+
const encoder = new TextEncoder();
|
|
217
|
+
const encoded = encoder.encode(value);
|
|
218
|
+
if (encoded.byteLength <= TRANSCRIPT_BODY_BYTE_LIMIT) {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
const truncatedChars = [];
|
|
222
|
+
let truncatedByteLength = 0;
|
|
223
|
+
for (const character of value) {
|
|
224
|
+
const characterByteLength = encoder.encode(character).byteLength;
|
|
225
|
+
if (truncatedByteLength + characterByteLength > TRANSCRIPT_BODY_BYTE_LIMIT) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
truncatedChars.push(character);
|
|
229
|
+
truncatedByteLength += characterByteLength;
|
|
230
|
+
}
|
|
231
|
+
const truncatedBytes = encoded.byteLength - truncatedByteLength;
|
|
232
|
+
return `${truncatedChars.join("")}\n… (${truncatedBytes} bytes truncated)`;
|
|
233
|
+
}
|
|
109
234
|
function parseResponseBody(text, contentType) {
|
|
110
235
|
if (text.length === 0) {
|
|
111
236
|
return undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,4 +9,4 @@ export type { AuthProvider, CommandContributor, TokenSource } from "./auth/types
|
|
|
9
9
|
export { bearerTokenAuth } from "./auth/bearer-token-auth.js";
|
|
10
10
|
export type { BearerTokenAuthOptions } from "./auth/bearer-token-auth.js";
|
|
11
11
|
export { HttpError, requestJson } from "./http.js";
|
|
12
|
-
export type { HttpRequestOptions, QueryValue } from "./http.js";
|
|
12
|
+
export type { HttpErrorRequest, HttpErrorResponse, HttpRequestOptions, QueryValue } from "./http.js";
|
package/dist/lock.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface LockFileSystem {
|
|
|
8
8
|
readFile(filePath: string, encoding: BufferEncoding): Promise<string>;
|
|
9
9
|
writeFile(filePath: string, contents: string, encoding: BufferEncoding): Promise<void>;
|
|
10
10
|
}
|
|
11
|
-
export declare function parseOpenApiLock(contents: string): OpenApiLock | null;
|
|
11
|
+
export declare function parseOpenApiLock(contents: string, lockPath: string): OpenApiLock | null;
|
|
12
12
|
export declare function stringifyOpenApiLock(lock: OpenApiLock): string;
|
|
13
13
|
export declare function readOpenApiLock(fs: Pick<LockFileSystem, "readFile">, lockPath: string): Promise<OpenApiLock | null>;
|
|
14
14
|
export declare function writeOpenApiLock(fs: LockFileSystem, lockPath: string, lock: OpenApiLock): Promise<void>;
|
package/dist/lock.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
|
|
2
|
+
import { UserError } from "toolcraft";
|
|
3
|
+
import { renderSourceSnippet } from "toolcraft/source-snippet";
|
|
4
|
+
export function parseOpenApiLock(contents, lockPath) {
|
|
3
5
|
let parsed;
|
|
4
6
|
try {
|
|
5
7
|
parsed = JSON.parse(contents);
|
|
6
8
|
}
|
|
7
|
-
catch {
|
|
8
|
-
|
|
9
|
+
catch (error) {
|
|
10
|
+
throw new UserError(formatJsonParseError(lockPath, contents, error), { cause: error });
|
|
9
11
|
}
|
|
10
12
|
if (typeof parsed !== "object" ||
|
|
11
13
|
parsed === null ||
|
|
@@ -27,7 +29,7 @@ export function stringifyOpenApiLock(lock) {
|
|
|
27
29
|
}
|
|
28
30
|
export async function readOpenApiLock(fs, lockPath) {
|
|
29
31
|
try {
|
|
30
|
-
return parseOpenApiLock(await fs.readFile(lockPath, "utf8"));
|
|
32
|
+
return parseOpenApiLock(await fs.readFile(lockPath, "utf8"), lockPath);
|
|
31
33
|
}
|
|
32
34
|
catch (error) {
|
|
33
35
|
if (isNotFoundError(error)) {
|
|
@@ -38,7 +40,13 @@ export async function readOpenApiLock(fs, lockPath) {
|
|
|
38
40
|
}
|
|
39
41
|
export async function writeOpenApiLock(fs, lockPath, lock) {
|
|
40
42
|
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
41
|
-
|
|
43
|
+
try {
|
|
44
|
+
await fs.writeFile(lockPath, stringifyOpenApiLock(lock), "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const code = getErrorCode(error);
|
|
48
|
+
throw new UserError(`Failed to write lock file "${lockPath}"${code === undefined ? "" : ` (${code})`}: ${getErrorMessage(error)}`, { cause: error });
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
function isNotFoundError(error) {
|
|
44
52
|
return (typeof error === "object" &&
|
|
@@ -46,3 +54,99 @@ function isNotFoundError(error) {
|
|
|
46
54
|
"code" in error &&
|
|
47
55
|
error.code === "ENOENT");
|
|
48
56
|
}
|
|
57
|
+
function getErrorMessage(error) {
|
|
58
|
+
return error instanceof Error ? error.message : String(error);
|
|
59
|
+
}
|
|
60
|
+
function formatJsonParseError(lockPath, source, error) {
|
|
61
|
+
const location = getJsonParseErrorLocation(error, source);
|
|
62
|
+
const message = getErrorMessage(error);
|
|
63
|
+
if (location === null) {
|
|
64
|
+
return `Lock file "${lockPath}" is not valid JSON: ${message}.`;
|
|
65
|
+
}
|
|
66
|
+
return (`Lock file "${lockPath}" is not valid JSON: ${message} ` +
|
|
67
|
+
`at line ${location.line} column ${location.column}.\n` +
|
|
68
|
+
renderSourceSnippet({
|
|
69
|
+
source,
|
|
70
|
+
line: location.line,
|
|
71
|
+
column: location.column,
|
|
72
|
+
filePath: lockPath
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
function getJsonParseErrorLocation(error, source) {
|
|
76
|
+
const causeLocation = getJsonParseCauseLocation(error);
|
|
77
|
+
if (causeLocation !== null) {
|
|
78
|
+
return causeLocation;
|
|
79
|
+
}
|
|
80
|
+
const directPosition = getNumericProperty(error, "position");
|
|
81
|
+
if (directPosition !== null) {
|
|
82
|
+
return getSourceOffsetLocation(source, directPosition);
|
|
83
|
+
}
|
|
84
|
+
const messagePosition = getJsonParseMessagePosition(getErrorMessage(error));
|
|
85
|
+
if (messagePosition !== null) {
|
|
86
|
+
return getSourceOffsetLocation(source, messagePosition);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function getJsonParseCauseLocation(error) {
|
|
91
|
+
if (typeof error !== "object" || error === null || !("cause" in error)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const cause = error.cause;
|
|
95
|
+
const line = getNumericProperty(cause, "line");
|
|
96
|
+
const column = getNumericProperty(cause, "column") ?? getNumericProperty(cause, "col");
|
|
97
|
+
if (line === null || column === null) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return { line, column };
|
|
101
|
+
}
|
|
102
|
+
function getNumericProperty(value, key) {
|
|
103
|
+
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const propertyValue = value[key];
|
|
107
|
+
return typeof propertyValue === "number" && Number.isFinite(propertyValue)
|
|
108
|
+
? propertyValue
|
|
109
|
+
: null;
|
|
110
|
+
}
|
|
111
|
+
function getJsonParseMessagePosition(message) {
|
|
112
|
+
const marker = " at position ";
|
|
113
|
+
const markerIndex = message.indexOf(marker);
|
|
114
|
+
if (markerIndex === -1) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const startIndex = markerIndex + marker.length;
|
|
118
|
+
let endIndex = startIndex;
|
|
119
|
+
while (endIndex < message.length && isAsciiDigit(message[endIndex] ?? "")) {
|
|
120
|
+
endIndex += 1;
|
|
121
|
+
}
|
|
122
|
+
if (endIndex === startIndex) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return Number.parseInt(message.slice(startIndex, endIndex), 10);
|
|
126
|
+
}
|
|
127
|
+
function getSourceOffsetLocation(source, offset) {
|
|
128
|
+
let line = 1;
|
|
129
|
+
let column = 1;
|
|
130
|
+
const boundedOffset = Math.max(0, Math.floor(offset));
|
|
131
|
+
for (let index = 0; index < boundedOffset && index < source.length; index += 1) {
|
|
132
|
+
if (source[index] === "\n") {
|
|
133
|
+
line += 1;
|
|
134
|
+
column = 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
column += 1;
|
|
138
|
+
}
|
|
139
|
+
return { line, column };
|
|
140
|
+
}
|
|
141
|
+
function isAsciiDigit(value) {
|
|
142
|
+
return value >= "0" && value <= "9";
|
|
143
|
+
}
|
|
144
|
+
function getErrorCode(error) {
|
|
145
|
+
if (typeof error === "object" &&
|
|
146
|
+
error !== null &&
|
|
147
|
+
"code" in error &&
|
|
148
|
+
typeof error.code === "string") {
|
|
149
|
+
return error.code;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
package/dist/mock/fetch.js
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { UserError } from "toolcraft";
|
|
2
|
+
export function classifyNetworkError(error, url) {
|
|
3
|
+
const networkError = findNetworkError(error);
|
|
4
|
+
const urlParts = new URL(url);
|
|
5
|
+
const host = getHost(networkError, urlParts);
|
|
6
|
+
switch (networkError?.code) {
|
|
7
|
+
case "ECONNREFUSED":
|
|
8
|
+
return new UserError(`Connection refused: ${host}:${getPort(networkError, urlParts)}. Is the server running?`, { cause: error });
|
|
9
|
+
case "ETIMEDOUT":
|
|
10
|
+
return new UserError(`Request timed out after ${getTimeoutMs(networkError)}ms: ${url}.`, {
|
|
11
|
+
cause: error
|
|
12
|
+
});
|
|
13
|
+
case "ENOTFOUND":
|
|
14
|
+
return new UserError(`DNS lookup failed for ${host}. Check the URL or your network.`, {
|
|
15
|
+
cause: error
|
|
16
|
+
});
|
|
17
|
+
case "ECONNRESET":
|
|
18
|
+
return new UserError(`Connection reset by ${host}. Likely transient: try again.`, {
|
|
19
|
+
cause: error
|
|
20
|
+
});
|
|
21
|
+
case "EAI_AGAIN":
|
|
22
|
+
return new UserError(`Temporary DNS failure for ${host}. Network may be down.`, {
|
|
23
|
+
cause: error
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (findAbortError(error) !== null) {
|
|
27
|
+
return new UserError(`Request aborted: ${url}.`, { cause: error });
|
|
28
|
+
}
|
|
29
|
+
if (error instanceof TypeError && error.message === "fetch failed" && !hasCause(error)) {
|
|
30
|
+
return new UserError(`Network request failed: ${url}.`, { cause: error });
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function findNetworkError(error) {
|
|
35
|
+
let current = error;
|
|
36
|
+
while (isErrorLikeObject(current)) {
|
|
37
|
+
if (typeof current.code === "string") {
|
|
38
|
+
return current;
|
|
39
|
+
}
|
|
40
|
+
current = current.cause;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function isAbortError(error) {
|
|
45
|
+
return isErrorLikeObject(error) && error.name === "AbortError";
|
|
46
|
+
}
|
|
47
|
+
function findAbortError(error) {
|
|
48
|
+
let current = error;
|
|
49
|
+
while (isErrorLikeObject(current)) {
|
|
50
|
+
if (isAbortError(current)) {
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
current = current.cause;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function hasCause(error) {
|
|
58
|
+
return "cause" in error && error.cause !== undefined;
|
|
59
|
+
}
|
|
60
|
+
function getHost(error, url) {
|
|
61
|
+
return typeof error?.address === "string" ? error.address : url.hostname;
|
|
62
|
+
}
|
|
63
|
+
function getPort(error, url) {
|
|
64
|
+
if (typeof error?.port === "number" || typeof error?.port === "string") {
|
|
65
|
+
return String(error.port);
|
|
66
|
+
}
|
|
67
|
+
if (url.port) {
|
|
68
|
+
return url.port;
|
|
69
|
+
}
|
|
70
|
+
return url.protocol === "https:" ? "443" : "80";
|
|
71
|
+
}
|
|
72
|
+
function getTimeoutMs(error) {
|
|
73
|
+
if (typeof error.ms === "number" || typeof error.ms === "string") {
|
|
74
|
+
return String(error.ms);
|
|
75
|
+
}
|
|
76
|
+
if (typeof error.timeout === "number" || typeof error.timeout === "string") {
|
|
77
|
+
return String(error.timeout);
|
|
78
|
+
}
|
|
79
|
+
return "unknown";
|
|
80
|
+
}
|
|
81
|
+
function isErrorLikeObject(value) {
|
|
82
|
+
return typeof value === "object" && value !== null;
|
|
83
|
+
}
|
package/dist/spec-source.js
CHANGED
|
@@ -2,6 +2,8 @@ import path from "node:path";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { UserError } from "toolcraft";
|
|
5
|
+
import { renderSourceSnippet } from "toolcraft/source-snippet";
|
|
6
|
+
import { classifyNetworkError } from "./network-error.js";
|
|
5
7
|
export async function readOpenApiSourceText(input, services) {
|
|
6
8
|
const inputUrl = input instanceof URL ? input : tryParseUrl(input);
|
|
7
9
|
const sourceLabel = formatSourceLabel(input);
|
|
@@ -18,9 +20,21 @@ export async function readOpenApiSourceText(input, services) {
|
|
|
18
20
|
if (inputUrl.protocol !== "http:" && inputUrl.protocol !== "https:") {
|
|
19
21
|
throw new UserError(`Unsupported OpenAPI input URL protocol ${JSON.stringify(inputUrl.protocol)}.`);
|
|
20
22
|
}
|
|
21
|
-
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await services.fetch(inputUrl.toString());
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw classifyNetworkError(error, inputUrl.toString()) ?? error;
|
|
29
|
+
}
|
|
22
30
|
if (!response.ok) {
|
|
23
|
-
|
|
31
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
32
|
+
const text = await response.text().catch(() => "");
|
|
33
|
+
const snippet = text.length === 0 ? "" : `\n body: ${truncate(text, 500)}`;
|
|
34
|
+
throw new UserError(`Failed to fetch ${JSON.stringify(inputUrl.toString())}: ` +
|
|
35
|
+
`${response.status} ${response.statusText}` +
|
|
36
|
+
(contentType ? ` (content-type: ${contentType})` : "") +
|
|
37
|
+
snippet);
|
|
24
38
|
}
|
|
25
39
|
return await response.text();
|
|
26
40
|
}
|
|
@@ -37,7 +51,7 @@ export function parseOpenApiDocument(sourceText, input) {
|
|
|
37
51
|
parsed = parseYaml(sourceText);
|
|
38
52
|
}
|
|
39
53
|
catch (error) {
|
|
40
|
-
throw new UserError(`Failed to parse OpenAPI document ${JSON.stringify(formatSourceLabel(input))}: ${
|
|
54
|
+
throw new UserError(`Failed to parse OpenAPI document ${JSON.stringify(formatSourceLabel(input))}: ${formatParseErrorMessage(error, sourceText, formatSourceLabel(input))}`);
|
|
41
55
|
}
|
|
42
56
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
43
57
|
throw new UserError(`OpenAPI document ${JSON.stringify(formatSourceLabel(input))} must parse to an object.`);
|
|
@@ -61,3 +75,89 @@ function getErrorMessage(error) {
|
|
|
61
75
|
}
|
|
62
76
|
return String(error);
|
|
63
77
|
}
|
|
78
|
+
function formatParseErrorMessage(error, sourceText, filePath) {
|
|
79
|
+
const message = getErrorMessage(error);
|
|
80
|
+
const linePosition = getYamlLinePosition(error) ?? getYamlOffsetPosition(error, sourceText);
|
|
81
|
+
if (linePosition === null) {
|
|
82
|
+
// yaml parse errors do not always expose positional metadata for every failure mode.
|
|
83
|
+
return message;
|
|
84
|
+
}
|
|
85
|
+
const positionText = `at line ${linePosition.line} column ${linePosition.column}`;
|
|
86
|
+
const messageWithPosition = message.includes(positionText)
|
|
87
|
+
? message
|
|
88
|
+
: `${message} (${positionText})`;
|
|
89
|
+
return `${messageWithPosition}\n${renderSourceSnippet({
|
|
90
|
+
source: sourceText,
|
|
91
|
+
line: linePosition.line,
|
|
92
|
+
column: linePosition.column,
|
|
93
|
+
filePath
|
|
94
|
+
})}`;
|
|
95
|
+
}
|
|
96
|
+
function getYamlLinePosition(error) {
|
|
97
|
+
if (typeof error !== "object" || error === null || !("linePos" in error)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const linePos = error.linePos;
|
|
101
|
+
if (!Array.isArray(linePos) || linePos.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const firstPosition = linePos[0];
|
|
105
|
+
if (typeof firstPosition !== "object" || firstPosition === null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const line = "line" in firstPosition ? firstPosition.line : undefined;
|
|
109
|
+
const column = "col" in firstPosition ? firstPosition.col : undefined;
|
|
110
|
+
if (typeof line !== "number" || typeof column !== "number") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return { line, column };
|
|
114
|
+
}
|
|
115
|
+
function getYamlOffsetPosition(error, sourceText) {
|
|
116
|
+
if (typeof error !== "object" || error === null || !("pos" in error)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const pos = error.pos;
|
|
120
|
+
if (!Array.isArray(pos) || typeof pos[0] !== "number" || pos[0] < 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return getSourceTextPosition(sourceText, pos[0]);
|
|
124
|
+
}
|
|
125
|
+
function getSourceTextPosition(sourceText, offset) {
|
|
126
|
+
let line = 1;
|
|
127
|
+
let column = 1;
|
|
128
|
+
for (let index = 0; index < offset && index < sourceText.length; index += 1) {
|
|
129
|
+
if (sourceText[index] === "\n") {
|
|
130
|
+
line += 1;
|
|
131
|
+
column = 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
column += 1;
|
|
135
|
+
}
|
|
136
|
+
return { line, column };
|
|
137
|
+
}
|
|
138
|
+
function truncate(value, maxLength) {
|
|
139
|
+
const collapsed = collapseToSingleLine(value);
|
|
140
|
+
if (collapsed.length <= maxLength) {
|
|
141
|
+
return collapsed;
|
|
142
|
+
}
|
|
143
|
+
return `${collapsed.slice(0, maxLength)}…`;
|
|
144
|
+
}
|
|
145
|
+
function collapseToSingleLine(value) {
|
|
146
|
+
const characters = [];
|
|
147
|
+
let previousWasWhitespace = false;
|
|
148
|
+
for (const character of value) {
|
|
149
|
+
if (isWhitespace(character)) {
|
|
150
|
+
if (!previousWasWhitespace) {
|
|
151
|
+
characters.push(" ");
|
|
152
|
+
previousWasWhitespace = true;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
characters.push(character);
|
|
157
|
+
previousWasWhitespace = false;
|
|
158
|
+
}
|
|
159
|
+
return characters.join("").trim();
|
|
160
|
+
}
|
|
161
|
+
function isWhitespace(character) {
|
|
162
|
+
return character.trim().length === 0;
|
|
163
|
+
}
|