@wacht/bench 0.1.6 → 0.1.7
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 -2
- package/dist/docs-search.js +68 -3
- package/dist/http.js +73 -0
- package/dist/init.js +2 -1
- package/dist/machine-api.js +2 -1
- package/dist/oauth.js +3 -2
- package/dist/openapi.js +2 -1
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -16,7 +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
|
+
import { docsGet, docsSearch } from './docs-search.js';
|
|
20
20
|
import { envPull } from './env-pull.js';
|
|
21
21
|
import { apiCommand, listProjects } from './machine-api.js';
|
|
22
22
|
import { openApiCall, openApiDescribe, openApiList, openApiRefresh } from './openapi.js';
|
|
@@ -220,7 +220,9 @@ export async function runCli(args) {
|
|
|
220
220
|
.action((options) => {
|
|
221
221
|
printMcpConfig(options.client);
|
|
222
222
|
});
|
|
223
|
-
const docs = program
|
|
223
|
+
const docs = program
|
|
224
|
+
.command('docs')
|
|
225
|
+
.description('search and read Wacht docs from the terminal — no MCP required');
|
|
224
226
|
docs
|
|
225
227
|
.command('search <query...>')
|
|
226
228
|
.description('full-text search Wacht docs and print matching pages')
|
|
@@ -235,6 +237,18 @@ export async function runCli(args) {
|
|
|
235
237
|
json: options.json,
|
|
236
238
|
});
|
|
237
239
|
});
|
|
240
|
+
docs
|
|
241
|
+
.command('get <path>')
|
|
242
|
+
.description('print the full Markdown of a Wacht docs page (e.g. /sdks/nextjs/middleware)')
|
|
243
|
+
.option('--base-url <url>', 'docs base URL (default: https://wacht.dev/docs, or $WACHT_DOCS_URL)')
|
|
244
|
+
.option('--json', 'emit JSON ({ path, url, markdown }) instead of raw Markdown')
|
|
245
|
+
.action(async (docPath, options) => {
|
|
246
|
+
await docsGet(context(program), {
|
|
247
|
+
path: docPath,
|
|
248
|
+
baseUrl: options.baseUrl,
|
|
249
|
+
json: options.json,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
238
252
|
const env = program.command('env').description('manage deployment credentials and environment files');
|
|
239
253
|
env
|
|
240
254
|
.command('pull')
|
package/dist/docs-search.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { httpFetch } from './http.js';
|
|
2
|
+
import { field, log, muted, printBannerFor, printError, printJson, section } from './ui.js';
|
|
2
3
|
const DEFAULT_DOCS_URL = 'https://wacht.dev/docs';
|
|
3
4
|
export async function docsSearch(ctx, options) {
|
|
4
5
|
const baseUrl = (options.baseUrl ?? process.env.WACHT_DOCS_URL ?? DEFAULT_DOCS_URL).replace(/\/+$/, '');
|
|
5
6
|
const endpoint = `${baseUrl}/api/search?query=${encodeURIComponent(options.query)}`;
|
|
6
7
|
let response;
|
|
7
8
|
try {
|
|
8
|
-
response = await
|
|
9
|
+
response = await httpFetch(endpoint, { headers: { Accept: 'application/json' } });
|
|
9
10
|
}
|
|
10
11
|
catch (error) {
|
|
11
12
|
printError(error);
|
|
@@ -38,7 +39,7 @@ export async function docsSearch(ctx, options) {
|
|
|
38
39
|
}
|
|
39
40
|
const orderedPages = Array.from(pages.values());
|
|
40
41
|
const sliced = options.limit ? orderedPages.slice(0, options.limit) : orderedPages;
|
|
41
|
-
if (options.json) {
|
|
42
|
+
if (ctx.json || options.json) {
|
|
42
43
|
printJson({
|
|
43
44
|
ok: true,
|
|
44
45
|
query: options.query,
|
|
@@ -78,4 +79,68 @@ export async function docsSearch(ctx, options) {
|
|
|
78
79
|
}
|
|
79
80
|
log(ctx, '');
|
|
80
81
|
}
|
|
82
|
+
if (!ctx.quiet) {
|
|
83
|
+
log(ctx, muted('Read a full page with `wacht docs get <path>` (e.g. `wacht docs get /sdks/nextjs/middleware`).'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Normalize whatever the caller passes (a docs path, a `/docs/...` path, a full
|
|
88
|
+
* URL, or one that already ends in `content.md`) down to the page's slug segments.
|
|
89
|
+
*/
|
|
90
|
+
function normalizeDocPath(input) {
|
|
91
|
+
let value = input.trim();
|
|
92
|
+
if (/^https?:\/\//i.test(value)) {
|
|
93
|
+
try {
|
|
94
|
+
value = new URL(value).pathname;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// fall through and treat it as a plain path
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
value = value.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
101
|
+
value = value.replace(/^docs\//, '');
|
|
102
|
+
// tolerate a pasted markdown URL: .../sdks/node/content.md → sdks/node
|
|
103
|
+
value = value.replace(/\/content\.md$/i, '').replace(/\.mdx?$/i, '');
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
export async function docsGet(ctx, options) {
|
|
107
|
+
const baseUrl = (options.baseUrl ?? process.env.WACHT_DOCS_URL ?? DEFAULT_DOCS_URL).replace(/\/+$/, '');
|
|
108
|
+
const slug = normalizeDocPath(options.path);
|
|
109
|
+
if (!slug) {
|
|
110
|
+
printError(new Error('Pass a docs path, for example `wacht docs get /sdks/nextjs/middleware`.'));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const docPath = `/${slug}`;
|
|
115
|
+
const url = `${baseUrl}/llms.mdx/docs/${slug}/content.md`;
|
|
116
|
+
let response;
|
|
117
|
+
try {
|
|
118
|
+
response = await httpFetch(url, { headers: { Accept: 'text/markdown' } });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
printError(error);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (response.status === 404) {
|
|
126
|
+
printError(new Error(`No docs page at ${docPath}. Run \`wacht docs search <terms>\` to find the right path.`));
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
printError(new Error(`docs get failed: ${response.status} ${response.statusText} (${url})`));
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const markdown = (await response.text()).trim();
|
|
136
|
+
if (ctx.json || options.json) {
|
|
137
|
+
printJson({ ok: true, path: docPath, url, markdown });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!ctx.quiet) {
|
|
141
|
+
log(ctx, muted(`# source: ${baseUrl}${docPath}`));
|
|
142
|
+
log(ctx, '');
|
|
143
|
+
}
|
|
144
|
+
// The page body is the deliverable here — print it raw so it's pipeable.
|
|
145
|
+
console.log(markdown);
|
|
81
146
|
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Shared HTTP layer: every network call in the CLI goes through `httpFetch` so
|
|
2
|
+
// no request can hang an agent loop forever, and transient blips on idempotent
|
|
3
|
+
// reads retry instead of failing the whole command.
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = (() => {
|
|
5
|
+
const raw = Number(process.env.WACHT_HTTP_TIMEOUT_MS);
|
|
6
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 20_000;
|
|
7
|
+
})();
|
|
8
|
+
const DEFAULT_RETRIES = 2;
|
|
9
|
+
const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD']);
|
|
10
|
+
export class HttpError extends Error {
|
|
11
|
+
url;
|
|
12
|
+
status;
|
|
13
|
+
constructor(message, url, status) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.url = url;
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.name = 'HttpError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function methodOf(init) {
|
|
21
|
+
return (init.method ?? 'GET').toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
function isRetryableStatus(status) {
|
|
24
|
+
return status === 429 || (status >= 500 && status <= 599);
|
|
25
|
+
}
|
|
26
|
+
function delay(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `fetch` with a hard timeout and bounded retries. Mutations (POST/PUT/PATCH/DELETE)
|
|
31
|
+
* are never retried automatically — only idempotent reads — so a flaky network can't
|
|
32
|
+
* double-apply a write. Non-retryable responses (including 4xx) are returned as-is so
|
|
33
|
+
* callers keep their own status handling.
|
|
34
|
+
*/
|
|
35
|
+
export async function httpFetch(url, init = {}, opts = {}) {
|
|
36
|
+
const target = String(url);
|
|
37
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
38
|
+
const idempotent = IDEMPOTENT_METHODS.has(methodOf(init));
|
|
39
|
+
const maxRetries = Math.max(0, opts.retries ?? (idempotent ? DEFAULT_RETRIES : 0));
|
|
40
|
+
for (let attempt = 0;; attempt += 1) {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
timedOut = true;
|
|
45
|
+
controller.abort();
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
49
|
+
if (isRetryableStatus(response.status) && attempt < maxRetries) {
|
|
50
|
+
await delay(200 * (attempt + 1));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// A timeout means we already waited the full budget — retrying just
|
|
57
|
+
// multiplies the wait, so fail fast and let the per-request timeout cap it.
|
|
58
|
+
// Connection errors (refused/reset/DNS) are cheap to retry and often transient.
|
|
59
|
+
if (!timedOut && attempt < maxRetries) {
|
|
60
|
+
await delay(200 * (attempt + 1));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (timedOut) {
|
|
64
|
+
throw new HttpError(`request timed out after ${timeoutMs}ms: ${target}`, target);
|
|
65
|
+
}
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
throw new HttpError(`request failed: ${message} (${target})`, target);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/init.js
CHANGED
|
@@ -50,7 +50,7 @@ This project is configured for AI-assisted Wacht development.
|
|
|
50
50
|
| Need | Source |
|
|
51
51
|
| --- | --- |
|
|
52
52
|
| Framework wiring patterns (provider, middleware, loaders) | Skills: \`${skills}\`. Always load before editing SDK code — never freelance the wiring. |
|
|
53
|
-
| Endpoint contracts, request/response shapes, errors | Wacht Docs MCP at \`${MCP_URL}
|
|
53
|
+
| Endpoint contracts, request/response shapes, errors | Wacht Docs MCP at \`${MCP_URL}\` — or, with no MCP, \`wacht docs search "<terms>"\` then \`wacht docs get <path>\`. Ground here before calling any Machine API operation by hand. |
|
|
54
54
|
| Live deployment context (project id, deployment id, hosts) | \`wacht deployments current\` — re-run every time, never cache. |
|
|
55
55
|
| Available CLI surface | \`wacht --help\` and \`wacht <command> --help\`. |
|
|
56
56
|
| Any Machine API operation by name | \`wacht api ls --search <text>\` → \`wacht api describe <op>\` → \`wacht api call <op>\`. |
|
|
@@ -64,6 +64,7 @@ This project is configured for AI-assisted Wacht development.
|
|
|
64
64
|
| Manage users | \`wacht users list\` · \`wacht users get <id>\` · \`wacht users create --field …\` |
|
|
65
65
|
| Manage orgs / workspaces | \`wacht orgs list\` · \`wacht workspaces list --org <id>\` |
|
|
66
66
|
| Pull / diff / apply config | \`wacht config pull\` · \`wacht config diff\` · \`wacht config apply --yes\` |
|
|
67
|
+
| Read Wacht docs (no MCP needed) | \`wacht docs search "<terms>"\` · \`wacht docs get <path>\` |
|
|
67
68
|
| Configure Docs MCP across clients | \`wacht mcp install\` (interactive picker) · \`wacht mcp list\` |
|
|
68
69
|
|
|
69
70
|
### Rules for agent loops
|
package/dist/machine-api.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { MACHINE_API_URL } from './config.js';
|
|
4
4
|
import { readBenchContext } from './context-store.js';
|
|
5
|
+
import { httpFetch } from './http.js';
|
|
5
6
|
import { getValidAuth } from './oauth.js';
|
|
6
7
|
import { promptChoice, promptOptionalList, promptText } from './prompts.js';
|
|
7
8
|
import { field, log, printBannerFor, printJson, section } from './ui.js';
|
|
@@ -38,7 +39,7 @@ export async function machineRequest(pathname, options = {}) {
|
|
|
38
39
|
const headers = new Headers(options.headers);
|
|
39
40
|
headers.set('authorization', `Bearer ${auth.access_token}`);
|
|
40
41
|
headers.set('accept', 'application/json');
|
|
41
|
-
const response = await
|
|
42
|
+
const response = await httpFetch(url, {
|
|
42
43
|
...options,
|
|
43
44
|
headers,
|
|
44
45
|
});
|
package/dist/oauth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { openBrowser } from './browser.js';
|
|
2
2
|
import { MACHINE_API_URL, OAUTH_AUTHORIZE_URL, OAUTH_CLIENT_ID, OAUTH_ISSUER, OAUTH_REVOCATION_URL, OAUTH_SCOPES, OAUTH_TOKEN_URL, REDIRECT_URI, } from './config.js';
|
|
3
3
|
import { isOAuthTokenResponse } from './guards.js';
|
|
4
|
+
import { httpFetch } from './http.js';
|
|
4
5
|
import { startOAuthCallbackServer } from './oauth-callback.js';
|
|
5
6
|
import { codeChallengeFor, randomToken } from './pkce.js';
|
|
6
7
|
import { clearAuth, readAuth, writeAuth } from './auth-store.js';
|
|
@@ -174,7 +175,7 @@ export async function logout(ctx) {
|
|
|
174
175
|
const auth = await readAuth();
|
|
175
176
|
if (auth?.refresh_token) {
|
|
176
177
|
try {
|
|
177
|
-
await
|
|
178
|
+
await httpFetch(OAUTH_REVOCATION_URL, {
|
|
178
179
|
method: 'POST',
|
|
179
180
|
headers: {
|
|
180
181
|
'content-type': 'application/x-www-form-urlencoded',
|
|
@@ -184,7 +185,7 @@ export async function logout(ctx) {
|
|
|
184
185
|
token_type_hint: 'refresh_token',
|
|
185
186
|
client_id: OAUTH_CLIENT_ID,
|
|
186
187
|
}),
|
|
187
|
-
});
|
|
188
|
+
}, { timeoutMs: 5_000 });
|
|
188
189
|
}
|
|
189
190
|
catch {
|
|
190
191
|
// Local logout should still succeed if the network is unavailable.
|
package/dist/openapi.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { AUTH_DIR, OPENAPI_CACHE_FILE, PLATFORM_OPENAPI_URL } from './config.js';
|
|
4
4
|
import { readBenchContext } from './context-store.js';
|
|
5
|
+
import { httpFetch } from './http.js';
|
|
5
6
|
import { entries, requestBody, machineRequest } from './machine-api.js';
|
|
6
7
|
import { validateBody } from './openapi-validate.js';
|
|
7
8
|
import { field, log, printBannerFor, printJson, section, warning } from './ui.js';
|
|
@@ -34,7 +35,7 @@ async function readCache() {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
async function fetchSpec() {
|
|
37
|
-
const response = await
|
|
38
|
+
const response = await httpFetch(PLATFORM_OPENAPI_URL);
|
|
38
39
|
if (!response.ok) {
|
|
39
40
|
throw new Error(`OpenAPI fetch failed: HTTP ${response.status}`);
|
|
40
41
|
}
|