@wacht/bench 0.1.1 → 0.1.3
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 +31 -5
- package/dist/env-pull.js +105 -0
- package/dist/init.js +34 -9
- package/dist/machine-api.js +37 -2
- package/dist/openapi-validate.js +193 -0
- package/dist/openapi.js +231 -7
- package/dist/skills.js +16 -4
- package/package.json +2 -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 { envPull } from './env-pull.js';
|
|
19
20
|
import { apiCommand, listProjects } from './machine-api.js';
|
|
20
21
|
import { openApiCall, openApiDescribe, openApiList, openApiRefresh } from './openapi.js';
|
|
21
22
|
import { installMcp, listMcp, printMcpConfig, uninstallMcp } from './mcp.js';
|
|
@@ -168,10 +169,22 @@ export async function runCli(args) {
|
|
|
168
169
|
const skills = program.command('skills').description('install Wacht agent skills');
|
|
169
170
|
skills
|
|
170
171
|
.command('install')
|
|
171
|
-
.description('install the Wacht skills pack')
|
|
172
|
+
.description('install the Wacht skills pack into one or more AI agents')
|
|
172
173
|
.option('--skill <name>', 'install one skill from the pack')
|
|
174
|
+
.option('--agent <ids>', 'comma-separated agent ids (e.g. claude-code,cursor,codex); skips the agent picker', (value) => value.split(',').map((s) => s.trim()).filter(Boolean))
|
|
175
|
+
.option('--all-agents', "install into every supported agent (passes -a '*')")
|
|
176
|
+
.option('--global', 'install at user scope instead of project scope')
|
|
177
|
+
.option('--yes', 'do not prompt for confirmation')
|
|
178
|
+
.option('--copy', 'copy skill files instead of symlinking')
|
|
173
179
|
.action(async (options) => {
|
|
174
|
-
await installSkills(
|
|
180
|
+
await installSkills({
|
|
181
|
+
skill: options.skill,
|
|
182
|
+
agents: options.agent,
|
|
183
|
+
allAgents: options.allAgents,
|
|
184
|
+
global: options.global,
|
|
185
|
+
yes: options.yes,
|
|
186
|
+
copy: options.copy,
|
|
187
|
+
});
|
|
175
188
|
});
|
|
176
189
|
const mcp = program.command('mcp').description('configure Wacht Docs MCP across AI clients');
|
|
177
190
|
mcp
|
|
@@ -206,6 +219,15 @@ export async function runCli(args) {
|
|
|
206
219
|
.action((options) => {
|
|
207
220
|
printMcpConfig(options.client);
|
|
208
221
|
});
|
|
222
|
+
const env = program.command('env').description('manage deployment credentials and environment files');
|
|
223
|
+
env
|
|
224
|
+
.command('pull')
|
|
225
|
+
.description('mint a fresh backend API key for the active deployment and write keys to .env.local')
|
|
226
|
+
.option('--file <path>', 'env file path; defaults to .env.local in the current directory')
|
|
227
|
+
.option('--print', 'print credentials to stdout instead of writing the env file')
|
|
228
|
+
.action(async (options) => {
|
|
229
|
+
await envPull(context(program), options);
|
|
230
|
+
});
|
|
209
231
|
const config = program.command('config').description('manage Wacht deployment settings as code');
|
|
210
232
|
config
|
|
211
233
|
.command('pull')
|
|
@@ -285,14 +307,18 @@ export async function runCli(args) {
|
|
|
285
307
|
.argument('<operation>', 'OpenAPI operation id')
|
|
286
308
|
.option('--deployment <id>', 'deployment id override; defaults to active deployment')
|
|
287
309
|
.option('--param <key=value>', 'path or query parameter; repeatable', collect, [])
|
|
288
|
-
.option('--body <json>', 'JSON request body')
|
|
310
|
+
.option('--body <json>', 'JSON request body; pass @path/to/file.json to read from disk')
|
|
289
311
|
.option('--field <key=value>', 'URL-encoded form field; repeatable', collect, [])
|
|
290
312
|
.option('--form <key=value>', 'multipart form field; value @path is treated as a file; repeatable', collect, [])
|
|
291
313
|
.option('--file <key=path>', 'multipart file field; key=path or key=@path; repeatable', collect, [])
|
|
292
314
|
.option('--header <key=value>', 'request header; repeatable', collect, [])
|
|
293
315
|
.option('--refresh', 'refresh the cached OpenAPI schema first')
|
|
294
|
-
.
|
|
295
|
-
|
|
316
|
+
.option('--no-validate', 'skip local JSON Schema validation of the request body')
|
|
317
|
+
.action(async (operation, _options, cmd) => {
|
|
318
|
+
// The parent `api` command also declares --body / --field / --form / --file / --header
|
|
319
|
+
// so `optsWithGlobals()` is what actually surfaces them through this subcommand.
|
|
320
|
+
// Commander does not merge clashing options automatically.
|
|
321
|
+
await openApiCall(context(program), operation, cmd.optsWithGlobals());
|
|
296
322
|
});
|
|
297
323
|
const schema = api.command('schema').description('manage cached OpenAPI schema');
|
|
298
324
|
schema
|
package/dist/env-pull.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { machineRequest } from './machine-api.js';
|
|
4
|
+
import { detectProject } from './project-detect.js';
|
|
5
|
+
import { field, log, printBannerFor, printJson, section } from './ui.js';
|
|
6
|
+
function isCredentialsResponse(value) {
|
|
7
|
+
if (typeof value !== 'object' || value === null)
|
|
8
|
+
return false;
|
|
9
|
+
const v = value;
|
|
10
|
+
if (typeof v.publishable_key !== 'string')
|
|
11
|
+
return false;
|
|
12
|
+
if (typeof v.frontend_host !== 'string')
|
|
13
|
+
return false;
|
|
14
|
+
if (typeof v.backend_host !== 'string')
|
|
15
|
+
return false;
|
|
16
|
+
if (typeof v.api_key !== 'object' || v.api_key === null)
|
|
17
|
+
return false;
|
|
18
|
+
const k = v.api_key;
|
|
19
|
+
return typeof k.secret === 'string';
|
|
20
|
+
}
|
|
21
|
+
function publishableKeyVar(frameworks) {
|
|
22
|
+
if (frameworks.has('Next.js'))
|
|
23
|
+
return 'NEXT_PUBLIC_WACHT_PUBLISHABLE_KEY';
|
|
24
|
+
if (frameworks.has('React Router') || frameworks.has('TanStack Router')) {
|
|
25
|
+
return 'VITE_WACHT_PUBLISHABLE_KEY';
|
|
26
|
+
}
|
|
27
|
+
return 'NEXT_PUBLIC_WACHT_PUBLISHABLE_KEY';
|
|
28
|
+
}
|
|
29
|
+
function upsertEnvLines(existing, updates) {
|
|
30
|
+
const lines = existing.length === 0 ? [] : existing.split(/\r?\n/);
|
|
31
|
+
const matched = new Set();
|
|
32
|
+
const next = lines.map((line) => {
|
|
33
|
+
const eqIdx = line.indexOf('=');
|
|
34
|
+
if (eqIdx <= 0)
|
|
35
|
+
return line;
|
|
36
|
+
const key = line.slice(0, eqIdx).trim();
|
|
37
|
+
if (!(key in updates))
|
|
38
|
+
return line;
|
|
39
|
+
matched.add(key);
|
|
40
|
+
return `${key}=${updates[key]}`;
|
|
41
|
+
});
|
|
42
|
+
const missing = Object.entries(updates).filter(([key]) => !matched.has(key));
|
|
43
|
+
if (missing.length) {
|
|
44
|
+
if (next.length && next[next.length - 1].trim() !== '')
|
|
45
|
+
next.push('');
|
|
46
|
+
for (const [key, value] of missing)
|
|
47
|
+
next.push(`${key}=${value}`);
|
|
48
|
+
}
|
|
49
|
+
let out = next.join('\n');
|
|
50
|
+
if (!out.endsWith('\n'))
|
|
51
|
+
out += '\n';
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export async function envPull(ctx, options = {}) {
|
|
55
|
+
const data = await machineRequest('/credentials', { method: 'POST' });
|
|
56
|
+
if (!isCredentialsResponse(data)) {
|
|
57
|
+
throw new Error('Unexpected response shape from /credentials.');
|
|
58
|
+
}
|
|
59
|
+
if (options.print || ctx.json) {
|
|
60
|
+
if (ctx.json) {
|
|
61
|
+
printJson({ ok: true, data });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
printBannerFor(ctx);
|
|
65
|
+
log(ctx, section('Deployment Credentials'));
|
|
66
|
+
log(ctx, field('Publishable key', data.publishable_key));
|
|
67
|
+
log(ctx, field('API key', data.api_key.secret));
|
|
68
|
+
log(ctx, field('API key suffix', `${data.api_key.prefix}…${data.api_key.suffix}`));
|
|
69
|
+
log(ctx, field('Frontend host', data.frontend_host));
|
|
70
|
+
log(ctx, field('Backend host', data.backend_host));
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const root = process.cwd();
|
|
75
|
+
const profile = await detectProject(root);
|
|
76
|
+
const frameworks = new Set(profile.frameworks);
|
|
77
|
+
const pubVar = publishableKeyVar(frameworks);
|
|
78
|
+
// Vite-based frameworks read `.env`; Next.js reads `.env.local`.
|
|
79
|
+
const defaultFile = frameworks.has('React Router') || frameworks.has('TanStack Router')
|
|
80
|
+
? '.env'
|
|
81
|
+
: '.env.local';
|
|
82
|
+
const filePath = options.file
|
|
83
|
+
? path.resolve(root, options.file)
|
|
84
|
+
: path.join(root, defaultFile);
|
|
85
|
+
const existing = await readFile(filePath, 'utf8').catch((err) => {
|
|
86
|
+
if (err.code === 'ENOENT')
|
|
87
|
+
return '';
|
|
88
|
+
throw err;
|
|
89
|
+
});
|
|
90
|
+
const updated = upsertEnvLines(existing, {
|
|
91
|
+
[pubVar]: data.publishable_key,
|
|
92
|
+
WACHT_API_KEY: data.api_key.secret,
|
|
93
|
+
});
|
|
94
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
95
|
+
await writeFile(filePath, updated, 'utf8');
|
|
96
|
+
printBannerFor(ctx);
|
|
97
|
+
log(ctx, section('Wrote credentials'));
|
|
98
|
+
log(ctx, field('File', path.relative(root, filePath) || filePath));
|
|
99
|
+
log(ctx, field(pubVar, `${data.publishable_key.slice(0, 12)}…`));
|
|
100
|
+
log(ctx, field('WACHT_API_KEY', `${data.api_key.prefix}…${data.api_key.suffix}`));
|
|
101
|
+
log(ctx, field('Frontend host', data.frontend_host));
|
|
102
|
+
log(ctx, field('Backend host', data.backend_host));
|
|
103
|
+
log(ctx, '');
|
|
104
|
+
log(ctx, 'Note: the API key secret is only shown once. Subsequent calls mint a new key.');
|
|
105
|
+
}
|
package/dist/init.js
CHANGED
|
@@ -43,7 +43,7 @@ This project is configured for AI-assisted Wacht development.
|
|
|
43
43
|
- Detected project shape: \`${frameworks}\`.
|
|
44
44
|
- Suggested Wacht skills for this project: \`${skills}\`.
|
|
45
45
|
- Active skill router: \`wacht\` (always start there). For CLI work specifically, use the \`wacht-bench-cli\` skill.
|
|
46
|
-
- Install or update skills with \`wacht skills
|
|
46
|
+
- Install or update skills with \`wacht skills install\`.
|
|
47
47
|
|
|
48
48
|
### Where to look (single source of truth)
|
|
49
49
|
|
|
@@ -96,6 +96,13 @@ async function upsertAgentsBlock(root, profile) {
|
|
|
96
96
|
return agentsPath;
|
|
97
97
|
}
|
|
98
98
|
async function writeEnvTemplate(root, profile) {
|
|
99
|
+
// If the project already ships a `.env.local.example` / `.env.example` (typical for
|
|
100
|
+
// scaffolded starters), don't drop a second redundant file alongside it.
|
|
101
|
+
for (const existing of ['.env.local.example', '.env.example']) {
|
|
102
|
+
if (await pathExists(path.join(root, existing))) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
99
106
|
const envPath = path.join(root, '.env.wacht.example');
|
|
100
107
|
const frameworks = new Set(profile.frameworks);
|
|
101
108
|
const isVite = frameworks.has('React Router') || frameworks.has('TanStack Router');
|
|
@@ -105,7 +112,8 @@ async function writeEnvTemplate(root, profile) {
|
|
|
105
112
|
? 'VITE_WACHT_PUBLISHABLE_KEY'
|
|
106
113
|
: 'NEXT_PUBLIC_WACHT_PUBLISHABLE_KEY';
|
|
107
114
|
const lines = [
|
|
108
|
-
'# Wacht SDK environment.
|
|
115
|
+
'# Wacht SDK environment. Run `wacht env pull` to populate these automatically,',
|
|
116
|
+
'# or copy values from https://console.wacht.dev.',
|
|
109
117
|
'',
|
|
110
118
|
'# Client-safe publishable key. Encodes deployment + frontend host.',
|
|
111
119
|
`${publishableKeyVar}=`,
|
|
@@ -129,7 +137,9 @@ export async function initProject(args, ctx) {
|
|
|
129
137
|
log(ctx, field('Suggested skills', profile.suggestedSkills.join(', ')));
|
|
130
138
|
log(ctx, '');
|
|
131
139
|
if (!options.skipEnv) {
|
|
132
|
-
|
|
140
|
+
const envPath = await writeEnvTemplate(root, profile);
|
|
141
|
+
if (envPath)
|
|
142
|
+
written.push(envPath);
|
|
133
143
|
}
|
|
134
144
|
if (!options.skipAgents) {
|
|
135
145
|
written.push(await upsertAgentsBlock(root, profile));
|
|
@@ -140,11 +150,7 @@ export async function initProject(args, ctx) {
|
|
|
140
150
|
if (options.installSkills) {
|
|
141
151
|
log(ctx, '');
|
|
142
152
|
log(ctx, section('Install Skills'));
|
|
143
|
-
await installSkills();
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
log(ctx, '');
|
|
147
|
-
log(ctx, field('Skills', `run ${command('wacht skills add')} when you want to install/update the pack`));
|
|
153
|
+
await installSkills({ yes: true });
|
|
148
154
|
}
|
|
149
155
|
if (ctx.json) {
|
|
150
156
|
printJson({
|
|
@@ -162,6 +168,22 @@ export async function initProject(args, ctx) {
|
|
|
162
168
|
log(ctx, '');
|
|
163
169
|
log(ctx, success('Wacht Bench project bootstrap complete.'));
|
|
164
170
|
}
|
|
171
|
+
function printAgentBootstrapSteps(ctx, opts) {
|
|
172
|
+
log(ctx, '');
|
|
173
|
+
log(ctx, section('Next'));
|
|
174
|
+
const cd = opts.starterDir ? `cd ${opts.starterDir} && ` : '';
|
|
175
|
+
if (!opts.installedSkills) {
|
|
176
|
+
log(ctx, ` ${command('wacht skills install --agent claude-code --global --yes')}`);
|
|
177
|
+
}
|
|
178
|
+
log(ctx, ` ${command('wacht mcp install --client claude-code-user --yes')}`);
|
|
179
|
+
log(ctx, ` ${command('wacht login && wacht deployments select')}`);
|
|
180
|
+
log(ctx, ` ${command(`${cd}wacht env pull`)}`);
|
|
181
|
+
if (opts.starterDir) {
|
|
182
|
+
log(ctx, ` ${command(`${cd}pnpm install && pnpm dev`)}`);
|
|
183
|
+
}
|
|
184
|
+
log(ctx, '');
|
|
185
|
+
log(ctx, '(Restart your AI client after installing skills/MCP so they load.)');
|
|
186
|
+
}
|
|
165
187
|
// ─── Starter mode ───────────────────────────────────────────────────
|
|
166
188
|
const STARTERS = {
|
|
167
189
|
nextjs: {
|
|
@@ -232,7 +254,10 @@ export async function initStarter(options, ctx) {
|
|
|
232
254
|
}
|
|
233
255
|
log(ctx, '');
|
|
234
256
|
log(ctx, success(`Starter ready at ${path.relative(process.cwd(), absoluteTarget) || '.'}`));
|
|
235
|
-
|
|
257
|
+
printAgentBootstrapSteps(ctx, {
|
|
258
|
+
starterDir: path.relative(process.cwd(), absoluteTarget) || '.',
|
|
259
|
+
installedSkills: options.install,
|
|
260
|
+
});
|
|
236
261
|
}
|
|
237
262
|
export function listStarters() {
|
|
238
263
|
return Object.entries(STARTERS).map(([framework, info]) => ({ framework, description: info.description, repo: info.repo }));
|
package/dist/machine-api.js
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { MACHINE_API_URL } from './config.js';
|
|
4
|
+
import { readBenchContext } from './context-store.js';
|
|
4
5
|
import { getValidAuth } from './oauth.js';
|
|
5
6
|
import { promptChoice, promptOptionalList, promptText } from './prompts.js';
|
|
6
7
|
import { field, log, printBannerFor, printJson, section } from './ui.js';
|
|
8
|
+
function isProjectScopedPath(p) {
|
|
9
|
+
if (p === '/projects' || p === '/project')
|
|
10
|
+
return true;
|
|
11
|
+
if (p.startsWith('/projects/') || p.startsWith('/project/'))
|
|
12
|
+
return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function isAlreadyDeploymentScoped(p) {
|
|
16
|
+
return p === '/deployments' || p.startsWith('/deployments/');
|
|
17
|
+
}
|
|
18
|
+
async function applyDeploymentPrefix(pathname) {
|
|
19
|
+
const normalized = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
20
|
+
if (isProjectScopedPath(normalized) || isAlreadyDeploymentScoped(normalized)) {
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
const context = await readBenchContext();
|
|
24
|
+
if (!context?.deployment_id) {
|
|
25
|
+
throw new Error('No active deployment selected. Run `wacht deployments select`.');
|
|
26
|
+
}
|
|
27
|
+
const splitIdx = normalized.search(/[?#]/);
|
|
28
|
+
const prefix = `/deployments/${context.deployment_id}`;
|
|
29
|
+
const pathPart = splitIdx === -1 ? normalized : normalized.slice(0, splitIdx);
|
|
30
|
+
const suffix = splitIdx === -1 ? '' : normalized.slice(splitIdx);
|
|
31
|
+
const joinedPath = pathPart === '/' ? prefix : `${prefix}${pathPart}`;
|
|
32
|
+
return `${joinedPath}${suffix}`;
|
|
33
|
+
}
|
|
7
34
|
export async function machineRequest(pathname, options = {}) {
|
|
8
35
|
const auth = await getValidAuth();
|
|
9
|
-
const
|
|
36
|
+
const resolvedPath = await applyDeploymentPrefix(pathname);
|
|
37
|
+
const url = new URL(resolvedPath, auth.machine_api_url || MACHINE_API_URL);
|
|
10
38
|
const headers = new Headers(options.headers);
|
|
11
39
|
headers.set('authorization', `Bearer ${auth.access_token}`);
|
|
12
40
|
headers.set('accept', 'application/json');
|
|
@@ -211,9 +239,16 @@ export async function requestBody(options) {
|
|
|
211
239
|
headers.set(key, value);
|
|
212
240
|
}
|
|
213
241
|
if (options.body) {
|
|
242
|
+
// `--body @path/to/file.json` reads the JSON from disk so prompt files and
|
|
243
|
+
// larger configuration blobs don't have to be inlined in the shell.
|
|
244
|
+
let source = options.body;
|
|
245
|
+
if (source.startsWith('@')) {
|
|
246
|
+
const filePath = path.resolve(source.slice(1));
|
|
247
|
+
source = await readFile(filePath, 'utf8');
|
|
248
|
+
}
|
|
214
249
|
headers.set('content-type', 'application/json');
|
|
215
250
|
return {
|
|
216
|
-
body: JSON.stringify(JSON.parse(
|
|
251
|
+
body: JSON.stringify(JSON.parse(source)),
|
|
217
252
|
headers,
|
|
218
253
|
};
|
|
219
254
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
/**
|
|
3
|
+
* Validate a JSON request body against an operation's `requestBody` schema using
|
|
4
|
+
* the cached OpenAPI spec. Returns `null` on success; an array of human-readable
|
|
5
|
+
* error strings on failure.
|
|
6
|
+
*
|
|
7
|
+
* The validator handles OpenAPI 3.0 `nullable: true` (rewritten to JSON-Schema
|
|
8
|
+
* `type: [..., "null"]`), `$ref` resolution against `components.schemas`, and
|
|
9
|
+
* discriminated unions via `oneOf` + `discriminator`.
|
|
10
|
+
*/
|
|
11
|
+
export function validateBody(spec, schema, body) {
|
|
12
|
+
if (!isRecord(schema))
|
|
13
|
+
return null;
|
|
14
|
+
const componentSchemas = readComponentSchemas(spec);
|
|
15
|
+
const ajv = buildAjv(componentSchemas);
|
|
16
|
+
const compiled = compileSafely(ajv, schema);
|
|
17
|
+
if (!compiled)
|
|
18
|
+
return null;
|
|
19
|
+
if (compiled(body))
|
|
20
|
+
return null;
|
|
21
|
+
return summariseErrors(compiled.errors ?? [], body);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The raw ajv error stream is noisy:
|
|
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
|
+
*/
|
|
39
|
+
function summariseErrors(rawErrors, body) {
|
|
40
|
+
// Discover oneOf failure points by looking for the `oneOf` umbrella error.
|
|
41
|
+
const oneOfPaths = new Set();
|
|
42
|
+
for (const e of rawErrors) {
|
|
43
|
+
if (e.keyword === 'oneOf')
|
|
44
|
+
oneOfPaths.add(e.instancePath);
|
|
45
|
+
}
|
|
46
|
+
const collapsed = [];
|
|
47
|
+
const skip = new Set();
|
|
48
|
+
for (const path of oneOfPaths) {
|
|
49
|
+
const value = readJsonPointer(body, path);
|
|
50
|
+
// The variant tag is conventionally `type`; if absent, just describe the path.
|
|
51
|
+
const tag = isRecord(value) && typeof value.type === 'string'
|
|
52
|
+
? `, got type "${value.type}"`
|
|
53
|
+
: '';
|
|
54
|
+
collapsed.push(`${path || '(body root)'}: doesn't match any variant of the discriminated union${tag}. ` +
|
|
55
|
+
`Run \`wacht api describe <operation>\` to see each variant's required fields.`);
|
|
56
|
+
// Drop every error at the union path; the user fixes the variant first.
|
|
57
|
+
for (const e of rawErrors) {
|
|
58
|
+
if (e.instancePath === path || e.instancePath.startsWith(path + '/'))
|
|
59
|
+
skip.add(e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const out = [...collapsed];
|
|
63
|
+
for (const e of rawErrors) {
|
|
64
|
+
if (skip.has(e))
|
|
65
|
+
continue;
|
|
66
|
+
if (e.keyword === 'anyOf' || e.keyword === 'oneOf')
|
|
67
|
+
continue;
|
|
68
|
+
if (e.keyword === 'type' && e.params?.type === 'null')
|
|
69
|
+
continue;
|
|
70
|
+
out.push(formatError(e));
|
|
71
|
+
}
|
|
72
|
+
// Dedupe identical lines (e.g. two variants both miss the same field).
|
|
73
|
+
return [...new Set(out)];
|
|
74
|
+
}
|
|
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
|
+
function readJsonPointer(value, pointer) {
|
|
83
|
+
if (!pointer || pointer === '/')
|
|
84
|
+
return value;
|
|
85
|
+
const segments = pointer.split('/').slice(1).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
86
|
+
let cur = value;
|
|
87
|
+
for (const seg of segments) {
|
|
88
|
+
if (Array.isArray(cur)) {
|
|
89
|
+
const idx = Number.parseInt(seg, 10);
|
|
90
|
+
if (Number.isNaN(idx))
|
|
91
|
+
return undefined;
|
|
92
|
+
cur = cur[idx];
|
|
93
|
+
}
|
|
94
|
+
else if (isRecord(cur)) {
|
|
95
|
+
cur = cur[seg];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return cur;
|
|
102
|
+
}
|
|
103
|
+
function readComponentSchemas(spec) {
|
|
104
|
+
if (!isRecord(spec))
|
|
105
|
+
return {};
|
|
106
|
+
const components = isRecord(spec.components) ? spec.components : undefined;
|
|
107
|
+
const schemas = components && isRecord(components.schemas) ? components.schemas : undefined;
|
|
108
|
+
return schemas ?? {};
|
|
109
|
+
}
|
|
110
|
+
function buildAjv(componentSchemas) {
|
|
111
|
+
// OpenAPI 3.1 schemas are JSON Schema draft 2020-12, but the spec we generate
|
|
112
|
+
// also leans on draft-7 `nullable: true` shorthand. Rewrite to draft-7 unions
|
|
113
|
+
// and run Ajv permissively — we're validating user payloads, not enforcing
|
|
114
|
+
// the spec itself.
|
|
115
|
+
const ajv = new Ajv({
|
|
116
|
+
strict: false,
|
|
117
|
+
allErrors: true,
|
|
118
|
+
coerceTypes: false,
|
|
119
|
+
allowUnionTypes: true,
|
|
120
|
+
validateFormats: false,
|
|
121
|
+
});
|
|
122
|
+
// Make every component schema addressable by its $ref so nested references resolve.
|
|
123
|
+
for (const [name, raw] of Object.entries(componentSchemas)) {
|
|
124
|
+
const rewritten = rewriteNullable(raw);
|
|
125
|
+
try {
|
|
126
|
+
ajv.addSchema(rewritten, `#/components/schemas/${name}`);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Bad schemas shouldn't break the CLI — skip and keep going.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return ajv;
|
|
133
|
+
}
|
|
134
|
+
function compileSafely(ajv, schema) {
|
|
135
|
+
try {
|
|
136
|
+
return ajv.compile(rewriteNullable(schema));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Walk a schema and rewrite `{type: X, nullable: true}` → `{type: [X, "null"]}`. */
|
|
143
|
+
function rewriteNullable(value) {
|
|
144
|
+
if (Array.isArray(value))
|
|
145
|
+
return value.map(rewriteNullable);
|
|
146
|
+
if (!isRecord(value))
|
|
147
|
+
return value;
|
|
148
|
+
const out = {};
|
|
149
|
+
let nullable = false;
|
|
150
|
+
for (const [key, v] of Object.entries(value)) {
|
|
151
|
+
if (key === 'nullable' && v === true) {
|
|
152
|
+
nullable = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
out[key] = rewriteNullable(v);
|
|
156
|
+
}
|
|
157
|
+
if (nullable && typeof out.type === 'string') {
|
|
158
|
+
out.type = [out.type, 'null'];
|
|
159
|
+
}
|
|
160
|
+
else if (nullable && Array.isArray(out.type)) {
|
|
161
|
+
if (!out.type.includes('null'))
|
|
162
|
+
out.type = [...out.type, 'null'];
|
|
163
|
+
}
|
|
164
|
+
else if (nullable) {
|
|
165
|
+
// No `type`; allow null alongside whatever shape the schema describes.
|
|
166
|
+
out.anyOf = [{ ...out }, { type: 'null' }];
|
|
167
|
+
for (const k of Object.keys(out))
|
|
168
|
+
if (k !== 'anyOf')
|
|
169
|
+
delete out[k];
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
function formatError(err) {
|
|
174
|
+
const path = err.instancePath || '(body root)';
|
|
175
|
+
const reason = err.message ?? 'invalid';
|
|
176
|
+
const extras = err.params ? formatParams(err.params) : '';
|
|
177
|
+
return `${path}: ${reason}${extras}`;
|
|
178
|
+
}
|
|
179
|
+
function formatParams(params) {
|
|
180
|
+
// Skim the most useful bits — missingProperty, allowedValues — without
|
|
181
|
+
// dumping every Ajv internal.
|
|
182
|
+
const bits = [];
|
|
183
|
+
if (typeof params.missingProperty === 'string')
|
|
184
|
+
bits.push(`missing "${params.missingProperty}"`);
|
|
185
|
+
if (Array.isArray(params.allowedValues))
|
|
186
|
+
bits.push(`allowed: ${params.allowedValues.map((v) => JSON.stringify(v)).join(', ')}`);
|
|
187
|
+
if (typeof params.additionalProperty === 'string')
|
|
188
|
+
bits.push(`unknown property "${params.additionalProperty}"`);
|
|
189
|
+
return bits.length ? ` (${bits.join('; ')})` : '';
|
|
190
|
+
}
|
|
191
|
+
function isRecord(value) {
|
|
192
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
193
|
+
}
|
package/dist/openapi.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { AUTH_DIR, OPENAPI_CACHE_FILE, PLATFORM_OPENAPI_URL } from './config.js';
|
|
3
4
|
import { readBenchContext } from './context-store.js';
|
|
4
5
|
import { entries, requestBody, machineRequest } from './machine-api.js';
|
|
6
|
+
import { validateBody } from './openapi-validate.js';
|
|
5
7
|
import { field, log, printBannerFor, printJson, section, warning } from './ui.js';
|
|
6
8
|
const OPENAPI_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
9
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
@@ -74,6 +76,7 @@ function operations(spec) {
|
|
|
74
76
|
tags: typed.tags ?? [],
|
|
75
77
|
parameters: typed.parameters ?? [],
|
|
76
78
|
requestBody: typed.requestBody,
|
|
79
|
+
responses: typed.responses,
|
|
77
80
|
});
|
|
78
81
|
}
|
|
79
82
|
}
|
|
@@ -95,14 +98,170 @@ function findOperation(spec, target, maybePath) {
|
|
|
95
98
|
function schemaType(schema) {
|
|
96
99
|
if (!schema)
|
|
97
100
|
return 'unknown';
|
|
98
|
-
if (typeof schema.type === 'string')
|
|
101
|
+
if (typeof schema.type === 'string') {
|
|
102
|
+
if (schema.type === 'array' && isRecord(schema.items)) {
|
|
103
|
+
return `array<${schemaType(schema.items)}>`;
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(schema.enum) && schema.enum.length) {
|
|
106
|
+
return `${schema.type}<${schema.enum.map((v) => JSON.stringify(v)).join(' | ')}>`;
|
|
107
|
+
}
|
|
108
|
+
if (typeof schema.const === 'string')
|
|
109
|
+
return `const "${schema.const}"`;
|
|
110
|
+
// Surface common formats like `binary` (file uploads) so multipart fields
|
|
111
|
+
// are recognisable at a glance vs plain strings.
|
|
112
|
+
if (typeof schema.format === 'string')
|
|
113
|
+
return `${schema.type} (${schema.format})`;
|
|
99
114
|
return schema.type;
|
|
115
|
+
}
|
|
100
116
|
if (typeof schema.$ref === 'string')
|
|
101
117
|
return schema.$ref.split('/').pop() ?? schema.$ref;
|
|
102
118
|
if (Array.isArray(schema.anyOf))
|
|
103
119
|
return schema.anyOf.map((item) => isRecord(item) ? schemaType(item) : 'unknown').join(' | ');
|
|
120
|
+
if (Array.isArray(schema.oneOf))
|
|
121
|
+
return schema.oneOf.map((item) => isRecord(item) ? schemaType(item) : 'unknown').join(' | ');
|
|
104
122
|
return 'object';
|
|
105
123
|
}
|
|
124
|
+
const REF_PREFIX = '#/components/schemas/';
|
|
125
|
+
function resolveSchemaRef(spec, schema, seen = new Set()) {
|
|
126
|
+
if (!schema)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (typeof schema.$ref !== 'string')
|
|
129
|
+
return schema;
|
|
130
|
+
const name = schema.$ref.startsWith(REF_PREFIX) ? schema.$ref.slice(REF_PREFIX.length) : '';
|
|
131
|
+
if (!name || seen.has(name))
|
|
132
|
+
return schema;
|
|
133
|
+
const schemas = isRecord(spec.components) && isRecord(spec.components.schemas) ? spec.components.schemas : undefined;
|
|
134
|
+
const target = schemas?.[name];
|
|
135
|
+
if (!isRecord(target))
|
|
136
|
+
return schema;
|
|
137
|
+
seen.add(name);
|
|
138
|
+
return resolveSchemaRef(spec, target, seen);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Merge an `allOf` chain into a single object schema. Used for our generator's
|
|
142
|
+
* internally-tagged enum encoding: `allOf: [{$ref: Payload}, {properties: {<tag>: const}}]`.
|
|
143
|
+
*/
|
|
144
|
+
function flattenAllOf(spec, schema) {
|
|
145
|
+
if (!Array.isArray(schema.allOf))
|
|
146
|
+
return schema;
|
|
147
|
+
const props = {};
|
|
148
|
+
const required = new Set();
|
|
149
|
+
for (const part of schema.allOf) {
|
|
150
|
+
if (!isRecord(part))
|
|
151
|
+
continue;
|
|
152
|
+
const resolved = resolveSchemaRef(spec, part) ?? part;
|
|
153
|
+
if (isRecord(resolved.properties)) {
|
|
154
|
+
for (const [key, value] of Object.entries(resolved.properties))
|
|
155
|
+
props[key] = value;
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(resolved.required)) {
|
|
158
|
+
for (const key of resolved.required) {
|
|
159
|
+
if (typeof key === 'string')
|
|
160
|
+
required.add(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { type: 'object', properties: props, required: Array.from(required) };
|
|
165
|
+
}
|
|
166
|
+
/** How many levels of nested object schemas to inline before falling back to the type name. */
|
|
167
|
+
const MAX_BODY_DEPTH = 3;
|
|
168
|
+
/** Pull the schema-component name out of a `$ref`, if any. */
|
|
169
|
+
function refName(schema) {
|
|
170
|
+
if (!schema || typeof schema.$ref !== 'string')
|
|
171
|
+
return undefined;
|
|
172
|
+
return schema.$ref.startsWith(REF_PREFIX) ? schema.$ref.slice(REF_PREFIX.length) : undefined;
|
|
173
|
+
}
|
|
174
|
+
/** True when a resolved schema carries structure worth expanding inline. */
|
|
175
|
+
function isStructural(schema) {
|
|
176
|
+
if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf) || Array.isArray(schema.allOf))
|
|
177
|
+
return true;
|
|
178
|
+
if (isRecord(schema.properties) && Object.keys(schema.properties).length > 0)
|
|
179
|
+
return true;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
function printSchema(pctx, raw, indent, depth) {
|
|
183
|
+
// Stop recursing if we're already too deep — caller has shown the type name.
|
|
184
|
+
if (depth > MAX_BODY_DEPTH)
|
|
185
|
+
return;
|
|
186
|
+
const name = refName(raw);
|
|
187
|
+
if (name && pctx.seen.has(name)) {
|
|
188
|
+
log(pctx.ctx, `${indent}(circular reference to ${name})`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const resolved = resolveSchemaRef(pctx.spec, raw) ?? raw;
|
|
192
|
+
const flat = Array.isArray(resolved.allOf) ? flattenAllOf(pctx.spec, resolved) : resolved;
|
|
193
|
+
if (Array.isArray(flat.oneOf)) {
|
|
194
|
+
if (depth >= MAX_BODY_DEPTH) {
|
|
195
|
+
log(pctx.ctx, `${indent}(oneOf — drill further with \`wacht api describe ${name ?? '<schema>'}\`)`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (depth === 0)
|
|
199
|
+
log(pctx.ctx, `${indent}(oneOf — pick exactly one variant)`);
|
|
200
|
+
if (name)
|
|
201
|
+
pctx.seen.add(name);
|
|
202
|
+
for (const variant of flat.oneOf) {
|
|
203
|
+
if (!isRecord(variant))
|
|
204
|
+
continue;
|
|
205
|
+
const variantInner = resolveSchemaRef(pctx.spec, variant) ?? variant;
|
|
206
|
+
const variantFlat = Array.isArray(variantInner.allOf) ? flattenAllOf(pctx.spec, variantInner) : variantInner;
|
|
207
|
+
const variantProps = isRecord(variantFlat.properties) ? variantFlat.properties : {};
|
|
208
|
+
const tagEntry = Object.entries(variantProps).find(([, val]) => isRecord(val) && typeof val.const === 'string');
|
|
209
|
+
const label = tagEntry
|
|
210
|
+
? `variant "${tagEntry[1].const}"`
|
|
211
|
+
: 'variant';
|
|
212
|
+
log(pctx.ctx, `${indent} ${label}:`);
|
|
213
|
+
printObjectFields(pctx, variantFlat, `${indent} `, depth + 1);
|
|
214
|
+
}
|
|
215
|
+
if (name)
|
|
216
|
+
pctx.seen.delete(name);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
printObjectFields(pctx, flat, indent, depth);
|
|
220
|
+
}
|
|
221
|
+
function printObjectFields(pctx, schema, indent, depth) {
|
|
222
|
+
const expanded = Array.isArray(schema.allOf) ? flattenAllOf(pctx.spec, schema) : schema;
|
|
223
|
+
const props = isRecord(expanded.properties) ? expanded.properties : {};
|
|
224
|
+
const required = new Set(Array.isArray(expanded.required)
|
|
225
|
+
? expanded.required.filter((k) => typeof k === 'string')
|
|
226
|
+
: []);
|
|
227
|
+
if (Object.keys(props).length === 0) {
|
|
228
|
+
log(pctx.ctx, `${indent}(no fields)`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
232
|
+
if (!isRecord(raw))
|
|
233
|
+
continue;
|
|
234
|
+
const tag = required.has(key) ? 'required' : 'optional';
|
|
235
|
+
const inlineType = schemaType(raw);
|
|
236
|
+
log(pctx.ctx, `${indent}${key} (${tag}, ${inlineType})`);
|
|
237
|
+
// Drill into nested structures one indent deeper, guarded by depth + cycle.
|
|
238
|
+
if (depth >= MAX_BODY_DEPTH)
|
|
239
|
+
continue;
|
|
240
|
+
const target = refName(raw) ?? refName(isRecord(raw.items) ? raw.items : undefined);
|
|
241
|
+
const nested = resolveSchemaRef(pctx.spec, raw) ?? raw;
|
|
242
|
+
// Array element refs: descend into the items' resolved schema.
|
|
243
|
+
const arrayItem = nested.type === 'array' && isRecord(nested.items)
|
|
244
|
+
? (resolveSchemaRef(pctx.spec, nested.items) ?? nested.items)
|
|
245
|
+
: undefined;
|
|
246
|
+
const next = arrayItem ?? nested;
|
|
247
|
+
if (!isStructural(next))
|
|
248
|
+
continue;
|
|
249
|
+
if (target && pctx.seen.has(target)) {
|
|
250
|
+
log(pctx.ctx, `${indent} (circular reference to ${target})`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (target)
|
|
254
|
+
pctx.seen.add(target);
|
|
255
|
+
printSchema(pctx, next, `${indent} `, depth + 1);
|
|
256
|
+
if (target)
|
|
257
|
+
pctx.seen.delete(target);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function printBodySchema(ctx, spec, schema) {
|
|
261
|
+
if (!schema)
|
|
262
|
+
return;
|
|
263
|
+
printSchema({ ctx, spec, seen: new Set() }, schema, '', 0);
|
|
264
|
+
}
|
|
106
265
|
function requestContent(operation) {
|
|
107
266
|
return Object.keys(operation.requestBody?.content ?? {});
|
|
108
267
|
}
|
|
@@ -177,8 +336,40 @@ export async function openApiDescribe(ctx, target, maybePath, options) {
|
|
|
177
336
|
if (contentTypes.length) {
|
|
178
337
|
log(ctx, '');
|
|
179
338
|
log(ctx, section('Request Body'));
|
|
180
|
-
|
|
181
|
-
|
|
339
|
+
const required = operation.requestBody?.required ? ' (required)' : ' (optional)';
|
|
340
|
+
for (const contentType of contentTypes) {
|
|
341
|
+
log(ctx, `${contentType}${required}`);
|
|
342
|
+
const bodySchema = operation.requestBody?.content?.[contentType]?.schema;
|
|
343
|
+
if (!isRecord(bodySchema))
|
|
344
|
+
continue;
|
|
345
|
+
// JSON, multipart, and url-encoded bodies all surface as object schemas
|
|
346
|
+
// with `properties` once form annotations are in place; render uniformly.
|
|
347
|
+
const isStructured = contentType === 'application/json'
|
|
348
|
+
|| contentType === 'multipart/form-data'
|
|
349
|
+
|| contentType === 'application/x-www-form-urlencoded';
|
|
350
|
+
if (isStructured) {
|
|
351
|
+
printBodySchema(ctx, loaded.spec, bodySchema);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (operation.responses && Object.keys(operation.responses).length) {
|
|
356
|
+
log(ctx, '');
|
|
357
|
+
log(ctx, section('Responses'));
|
|
358
|
+
const statuses = Object.keys(operation.responses).sort();
|
|
359
|
+
for (const status of statuses) {
|
|
360
|
+
const response = operation.responses[status];
|
|
361
|
+
const description = response?.description ? ` — ${response.description}` : '';
|
|
362
|
+
log(ctx, `${status}${description}`);
|
|
363
|
+
// Only drill the 2xx success body; error responses share a common
|
|
364
|
+
// `{errors: [{message, code}]}` envelope and just clutter the output.
|
|
365
|
+
const isSuccess = status.startsWith('2');
|
|
366
|
+
if (!isSuccess)
|
|
367
|
+
continue;
|
|
368
|
+
const successJson = response?.content?.['application/json']?.schema;
|
|
369
|
+
if (isRecord(successJson)) {
|
|
370
|
+
printBodySchema(ctx, loaded.spec, successJson);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
182
373
|
}
|
|
183
374
|
}
|
|
184
375
|
function applyPathParams(path, params) {
|
|
@@ -218,13 +409,46 @@ export async function openApiCall(ctx, target, options) {
|
|
|
218
409
|
file: options.file,
|
|
219
410
|
header: options.header,
|
|
220
411
|
};
|
|
412
|
+
// Validate the JSON body against the operation's request schema first —
|
|
413
|
+
// local check, useful regardless of deployment state. Skips multipart
|
|
414
|
+
// (JSON Schema can't validate FormData) and anything not parseable as JSON.
|
|
415
|
+
if (apiOptions.body && options.validate !== false) {
|
|
416
|
+
const sourceBody = apiOptions.body.startsWith('@')
|
|
417
|
+
? await readFile(path.resolve(apiOptions.body.slice(1)), 'utf8')
|
|
418
|
+
: apiOptions.body;
|
|
419
|
+
let parsed;
|
|
420
|
+
try {
|
|
421
|
+
parsed = JSON.parse(sourceBody);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
parsed = undefined;
|
|
425
|
+
}
|
|
426
|
+
const bodySchema = operation.requestBody?.content?.['application/json']?.schema;
|
|
427
|
+
if (parsed !== undefined && bodySchema) {
|
|
428
|
+
const errors = validateBody(loaded.spec, bodySchema, parsed);
|
|
429
|
+
if (errors && errors.length) {
|
|
430
|
+
log(ctx, warning(`Request body did not match schema for ${operation.operationId}:`));
|
|
431
|
+
for (const e of errors)
|
|
432
|
+
log(ctx, ` - ${e}`);
|
|
433
|
+
log(ctx, '');
|
|
434
|
+
log(ctx, `Pass --no-validate to skip local validation, or \`wacht api describe ${operation.operationId}\` to see the schema.`);
|
|
435
|
+
throw new Error('Validation failed.');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
221
439
|
const pathWithParams = appendQueryParams(applyPathParams(operation.path, params), params, operation);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
440
|
+
const isProjectScoped = pathWithParams.startsWith('/project') || pathWithParams === '/projects';
|
|
441
|
+
let machinePath;
|
|
442
|
+
if (isProjectScoped) {
|
|
443
|
+
machinePath = pathWithParams;
|
|
444
|
+
}
|
|
445
|
+
else if (!deploymentId) {
|
|
226
446
|
throw new Error('Select an active deployment first, or pass raw paths with `wacht api METHOD /path`.');
|
|
227
447
|
}
|
|
448
|
+
else {
|
|
449
|
+
const base = `/deployments/${deploymentId}`;
|
|
450
|
+
machinePath = pathWithParams === '/' ? base : `${base}${pathWithParams}`;
|
|
451
|
+
}
|
|
228
452
|
const { body, headers } = await requestBody(apiOptions);
|
|
229
453
|
const data = await machineRequest(machinePath, {
|
|
230
454
|
method: operation.method,
|
package/dist/skills.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { SKILLS_SOURCE } from './config.js';
|
|
3
3
|
import { valueAfter } from './util.js';
|
|
4
|
-
export function installSkills(
|
|
4
|
+
export function installSkills(options = {}) {
|
|
5
5
|
const installArgs = ['skills', 'add', SKILLS_SOURCE];
|
|
6
|
-
if (
|
|
7
|
-
installArgs.push('
|
|
6
|
+
if (options.allAgents) {
|
|
7
|
+
installArgs.push('-a', '*');
|
|
8
|
+
}
|
|
9
|
+
else if (options.agents && options.agents.length > 0) {
|
|
10
|
+
installArgs.push('-a', ...options.agents);
|
|
11
|
+
}
|
|
12
|
+
if (options.skill)
|
|
13
|
+
installArgs.push('-s', options.skill);
|
|
14
|
+
if (options.global)
|
|
15
|
+
installArgs.push('-g');
|
|
16
|
+
if (options.yes)
|
|
17
|
+
installArgs.push('-y');
|
|
18
|
+
if (options.copy)
|
|
19
|
+
installArgs.push('--copy');
|
|
8
20
|
return new Promise((resolve, reject) => {
|
|
9
21
|
const child = spawn('npx', installArgs, {
|
|
10
22
|
stdio: 'inherit',
|
|
@@ -25,5 +37,5 @@ export function installSkills(skill) {
|
|
|
25
37
|
});
|
|
26
38
|
}
|
|
27
39
|
export async function skillsInstall(args) {
|
|
28
|
-
await installSkills(valueAfter(args, '--skill'));
|
|
40
|
+
await installSkills({ skill: valueAfter(args, '--skill') });
|
|
29
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wacht/bench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI for Wacht Bench, the AI development workbench for Wacht.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"node": ">=20"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"ajv": "^8.20.0",
|
|
24
25
|
"commander": "^12.1.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|