@wacht/bench 0.1.1 → 0.1.2
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 +24 -2
- package/dist/env-pull.js +100 -0
- package/dist/init.js +2 -2
- package/dist/machine-api.js +29 -1
- package/dist/openapi.js +10 -4
- package/dist/skills.js +16 -4
- 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 { 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')
|
package/dist/env-pull.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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 pubVar = publishableKeyVar(new Set(profile.frameworks));
|
|
77
|
+
const filePath = options.file
|
|
78
|
+
? path.resolve(root, options.file)
|
|
79
|
+
: path.join(root, '.env.local');
|
|
80
|
+
const existing = await readFile(filePath, 'utf8').catch((err) => {
|
|
81
|
+
if (err.code === 'ENOENT')
|
|
82
|
+
return '';
|
|
83
|
+
throw err;
|
|
84
|
+
});
|
|
85
|
+
const updated = upsertEnvLines(existing, {
|
|
86
|
+
[pubVar]: data.publishable_key,
|
|
87
|
+
WACHT_API_KEY: data.api_key.secret,
|
|
88
|
+
});
|
|
89
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
90
|
+
await writeFile(filePath, updated, 'utf8');
|
|
91
|
+
printBannerFor(ctx);
|
|
92
|
+
log(ctx, section('Wrote credentials'));
|
|
93
|
+
log(ctx, field('File', path.relative(root, filePath) || filePath));
|
|
94
|
+
log(ctx, field(pubVar, `${data.publishable_key.slice(0, 12)}…`));
|
|
95
|
+
log(ctx, field('WACHT_API_KEY', `${data.api_key.prefix}…${data.api_key.suffix}`));
|
|
96
|
+
log(ctx, field('Frontend host', data.frontend_host));
|
|
97
|
+
log(ctx, field('Backend host', data.backend_host));
|
|
98
|
+
log(ctx, '');
|
|
99
|
+
log(ctx, 'Note: the API key secret is only shown once. Subsequent calls mint a new key.');
|
|
100
|
+
}
|
package/dist/init.js
CHANGED
|
@@ -140,11 +140,11 @@ export async function initProject(args, ctx) {
|
|
|
140
140
|
if (options.installSkills) {
|
|
141
141
|
log(ctx, '');
|
|
142
142
|
log(ctx, section('Install Skills'));
|
|
143
|
-
await installSkills();
|
|
143
|
+
await installSkills({ yes: true });
|
|
144
144
|
}
|
|
145
145
|
else {
|
|
146
146
|
log(ctx, '');
|
|
147
|
-
log(ctx, field('Skills', `run ${command('wacht skills
|
|
147
|
+
log(ctx, field('Skills', `run ${command('wacht skills install')} when you want to install/update the pack`));
|
|
148
148
|
}
|
|
149
149
|
if (ctx.json) {
|
|
150
150
|
printJson({
|
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');
|
package/dist/openapi.js
CHANGED
|
@@ -219,12 +219,18 @@ export async function openApiCall(ctx, target, options) {
|
|
|
219
219
|
header: options.header,
|
|
220
220
|
};
|
|
221
221
|
const pathWithParams = appendQueryParams(applyPathParams(operation.path, params), params, operation);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
const isProjectScoped = pathWithParams.startsWith('/project') || pathWithParams === '/projects';
|
|
223
|
+
let machinePath;
|
|
224
|
+
if (isProjectScoped) {
|
|
225
|
+
machinePath = pathWithParams;
|
|
226
|
+
}
|
|
227
|
+
else if (!deploymentId) {
|
|
226
228
|
throw new Error('Select an active deployment first, or pass raw paths with `wacht api METHOD /path`.');
|
|
227
229
|
}
|
|
230
|
+
else {
|
|
231
|
+
const base = `/deployments/${deploymentId}`;
|
|
232
|
+
machinePath = pathWithParams === '/' ? base : `${base}${pathWithParams}`;
|
|
233
|
+
}
|
|
228
234
|
const { body, headers } = await requestBody(apiOptions);
|
|
229
235
|
const data = await machineRequest(machinePath, {
|
|
230
236
|
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
|
}
|