@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 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(options.skill);
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')
@@ -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 add')} when you want to install/update the pack`));
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({
@@ -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 url = new URL(pathname, auth.machine_api_url || MACHINE_API_URL);
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 machinePath = pathWithParams.startsWith('/project') || pathWithParams === '/projects'
223
- ? pathWithParams
224
- : `/deployments/${deploymentId ?? ''}${pathWithParams}`;
225
- if (machinePath.includes('/deployments//')) {
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(skill) {
4
+ export function installSkills(options = {}) {
5
5
  const installArgs = ['skills', 'add', SKILLS_SOURCE];
6
- if (skill)
7
- installArgs.push('--skill', skill);
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.1",
3
+ "version": "0.1.2",
4
4
  "description": "CLI for Wacht Bench, the AI development workbench for Wacht.",
5
5
  "type": "module",
6
6
  "bin": {