@wacht/bench 0.1.3 → 0.1.4
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/commands.js +16 -0
- package/dist/docs-search.js +81 -0
- package/dist/openapi-validate.js +2 -23
- package/dist/openapi.js +2 -0
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -16,6 +16,7 @@ import { completionScript } from './completion.js';
|
|
|
16
16
|
import { configApply, configDiff, configPull, configSchemaCommand, printConfigTemplate, } from './config-workflow.js';
|
|
17
17
|
import { clearDeployment, createDeploymentCommand, createProjectCommand, currentDeployment, selectDeployment, } from './deployment-context.js';
|
|
18
18
|
import { initProject, initStarter } from './init.js';
|
|
19
|
+
import { docsSearch } from './docs-search.js';
|
|
19
20
|
import { envPull } from './env-pull.js';
|
|
20
21
|
import { apiCommand, listProjects } from './machine-api.js';
|
|
21
22
|
import { openApiCall, openApiDescribe, openApiList, openApiRefresh } from './openapi.js';
|
|
@@ -219,6 +220,21 @@ export async function runCli(args) {
|
|
|
219
220
|
.action((options) => {
|
|
220
221
|
printMcpConfig(options.client);
|
|
221
222
|
});
|
|
223
|
+
const docs = program.command('docs').description('search and explore Wacht docs');
|
|
224
|
+
docs
|
|
225
|
+
.command('search <query...>')
|
|
226
|
+
.description('full-text search Wacht docs and print matching pages')
|
|
227
|
+
.option('--limit <n>', 'max number of pages to print', (v) => Number.parseInt(v, 10))
|
|
228
|
+
.option('--base-url <url>', 'docs base URL (default: https://wacht.dev/docs, or $WACHT_DOCS_URL)')
|
|
229
|
+
.option('--json', 'emit JSON instead of human-readable output')
|
|
230
|
+
.action(async (queryParts, options) => {
|
|
231
|
+
await docsSearch(context(program), {
|
|
232
|
+
query: queryParts.join(' '),
|
|
233
|
+
limit: options.limit,
|
|
234
|
+
baseUrl: options.baseUrl,
|
|
235
|
+
json: options.json,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
222
238
|
const env = program.command('env').description('manage deployment credentials and environment files');
|
|
223
239
|
env
|
|
224
240
|
.command('pull')
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { field, log, printBannerFor, printError, printJson, section } from './ui.js';
|
|
2
|
+
const DEFAULT_DOCS_URL = 'https://wacht.dev/docs';
|
|
3
|
+
export async function docsSearch(ctx, options) {
|
|
4
|
+
const baseUrl = (options.baseUrl ?? process.env.WACHT_DOCS_URL ?? DEFAULT_DOCS_URL).replace(/\/+$/, '');
|
|
5
|
+
const endpoint = `${baseUrl}/api/search?query=${encodeURIComponent(options.query)}`;
|
|
6
|
+
let response;
|
|
7
|
+
try {
|
|
8
|
+
response = await fetch(endpoint, { headers: { Accept: 'application/json' } });
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
printError(error);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
printError(new Error(`docs search failed: ${response.status} ${response.statusText} (${endpoint})`));
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const raw = (await response.json());
|
|
21
|
+
if (!Array.isArray(raw)) {
|
|
22
|
+
printError(new Error('unexpected docs search response shape'));
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const pages = new Map();
|
|
27
|
+
const snippets = new Map();
|
|
28
|
+
for (const hit of raw) {
|
|
29
|
+
const pageUrl = hit.url.split('#')[0];
|
|
30
|
+
if (hit.type === 'page') {
|
|
31
|
+
pages.set(pageUrl, hit);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!snippets.has(pageUrl))
|
|
35
|
+
snippets.set(pageUrl, []);
|
|
36
|
+
snippets.get(pageUrl).push(hit);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const orderedPages = Array.from(pages.values());
|
|
40
|
+
const sliced = options.limit ? orderedPages.slice(0, options.limit) : orderedPages;
|
|
41
|
+
if (options.json) {
|
|
42
|
+
printJson({
|
|
43
|
+
ok: true,
|
|
44
|
+
query: options.query,
|
|
45
|
+
baseUrl,
|
|
46
|
+
results: sliced.map((page) => ({
|
|
47
|
+
title: page.content,
|
|
48
|
+
url: `${baseUrl}${page.url}`,
|
|
49
|
+
breadcrumbs: page.breadcrumbs ?? [],
|
|
50
|
+
snippets: (snippets.get(page.url) ?? []).map((s) => ({
|
|
51
|
+
text: s.content,
|
|
52
|
+
url: `${baseUrl}${s.url}`,
|
|
53
|
+
})),
|
|
54
|
+
})),
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
printBannerFor(ctx);
|
|
59
|
+
log(ctx, section('Docs Search'));
|
|
60
|
+
log(ctx, field('Query', options.query));
|
|
61
|
+
log(ctx, field('Source', baseUrl));
|
|
62
|
+
log(ctx, field('Results', String(sliced.length)));
|
|
63
|
+
log(ctx, '');
|
|
64
|
+
if (sliced.length === 0) {
|
|
65
|
+
log(ctx, 'No matches.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const page of sliced) {
|
|
69
|
+
const breadcrumbs = page.breadcrumbs?.length ? page.breadcrumbs.join(' › ') : '';
|
|
70
|
+
log(ctx, `• ${page.content}`);
|
|
71
|
+
if (breadcrumbs)
|
|
72
|
+
log(ctx, ` ${breadcrumbs}`);
|
|
73
|
+
log(ctx, ` ${baseUrl}${page.url}`);
|
|
74
|
+
const pageSnippets = snippets.get(page.url) ?? [];
|
|
75
|
+
for (const s of pageSnippets.slice(0, 3)) {
|
|
76
|
+
const trimmed = s.content.length > 140 ? `${s.content.slice(0, 137).trimEnd()}…` : s.content;
|
|
77
|
+
log(ctx, ` – ${trimmed}`);
|
|
78
|
+
}
|
|
79
|
+
log(ctx, '');
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/openapi-validate.js
CHANGED
|
@@ -20,22 +20,8 @@ export function validateBody(spec, schema, body) {
|
|
|
20
20
|
return null;
|
|
21
21
|
return summariseErrors(compiled.errors ?? [], body);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* - `anyOf` / `oneOf` umbrellas restate "doesn't match" without specifics.
|
|
26
|
-
* - The `type: null` branches our `nullable` rewrite introduces are never what
|
|
27
|
-
* the user is trying to send.
|
|
28
|
-
* - oneOf-with-discriminator emits per-variant required errors from every
|
|
29
|
-
* non-matching branch, drowning out the actual problem.
|
|
30
|
-
*
|
|
31
|
-
* Strategy:
|
|
32
|
-
* 1. Drop nullable + anyOf/oneOf umbrellas.
|
|
33
|
-
* 2. When an `oneOf` umbrella was present at a path, collapse all required/const
|
|
34
|
-
* errors under that path into a single "doesn't match any variant" line with
|
|
35
|
-
* the discriminator value found in the body (if any). Tell the user to run
|
|
36
|
-
* `wacht api describe` for the variant payload — avoids guessing wrong.
|
|
37
|
-
* 3. Pass everything else through unchanged.
|
|
38
|
-
*/
|
|
23
|
+
// Collapse `oneOf` failures into one line per union and drop the noisy umbrella
|
|
24
|
+
// + `type: null` errors our `nullable` rewrite introduces.
|
|
39
25
|
function summariseErrors(rawErrors, body) {
|
|
40
26
|
// Discover oneOf failure points by looking for the `oneOf` umbrella error.
|
|
41
27
|
const oneOfPaths = new Set();
|
|
@@ -72,13 +58,6 @@ function summariseErrors(rawErrors, body) {
|
|
|
72
58
|
// Dedupe identical lines (e.g. two variants both miss the same field).
|
|
73
59
|
return [...new Set(out)];
|
|
74
60
|
}
|
|
75
|
-
/**
|
|
76
|
-
* When a `oneOf` discriminated union mismatches, ajv emits a "must be equal to constant"
|
|
77
|
-
* error for every variant whose const failed, plus the required-field errors from each
|
|
78
|
-
* variant's payload schema. That's overwhelming. If the input has a discriminator value
|
|
79
|
-
* that matches *no* variant, surface a single "X must be one of [...]"; if it matches
|
|
80
|
-
* exactly one, drop the per-variant required errors from the other variants.
|
|
81
|
-
*/
|
|
82
61
|
function readJsonPointer(value, pointer) {
|
|
83
62
|
if (!pointer || pointer === '/')
|
|
84
63
|
return value;
|
package/dist/openapi.js
CHANGED
|
@@ -119,6 +119,8 @@ function schemaType(schema) {
|
|
|
119
119
|
return schema.anyOf.map((item) => isRecord(item) ? schemaType(item) : 'unknown').join(' | ');
|
|
120
120
|
if (Array.isArray(schema.oneOf))
|
|
121
121
|
return schema.oneOf.map((item) => isRecord(item) ? schemaType(item) : 'unknown').join(' | ');
|
|
122
|
+
if (schema.const !== undefined)
|
|
123
|
+
return `const ${JSON.stringify(schema.const)}`;
|
|
122
124
|
return 'object';
|
|
123
125
|
}
|
|
124
126
|
const REF_PREFIX = '#/components/schemas/';
|