@wacht/bench 0.1.0
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/auth-store.js +19 -0
- package/dist/browser.js +15 -0
- package/dist/commands.js +389 -0
- package/dist/completion.js +132 -0
- package/dist/config-workflow.js +474 -0
- package/dist/config.js +18 -0
- package/dist/context-store.js +28 -0
- package/dist/deployment-context.js +205 -0
- package/dist/guards.js +23 -0
- package/dist/init.js +535 -0
- package/dist/machine-api.js +272 -0
- package/dist/mcp.js +21 -0
- package/dist/oauth-callback.js +104 -0
- package/dist/oauth.js +236 -0
- package/dist/openapi.js +259 -0
- package/dist/pkce.js +14 -0
- package/dist/project-detect.js +64 -0
- package/dist/prompts.js +74 -0
- package/dist/resources.js +204 -0
- package/dist/skills.js +29 -0
- package/dist/types.js +1 -0
- package/dist/ui.js +104 -0
- package/dist/util.js +6 -0
- package/dist/wacht.js +18 -0
- package/package.json +33 -0
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { AUTH_DIR, OPENAPI_CACHE_FILE, PLATFORM_OPENAPI_URL } from './config.js';
|
|
3
|
+
import { readBenchContext } from './context-store.js';
|
|
4
|
+
import { entries, requestBody, machineRequest } from './machine-api.js';
|
|
5
|
+
import { field, log, printBannerFor, printJson, section, warning } from './ui.js';
|
|
6
|
+
const OPENAPI_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function asSpec(value) {
|
|
12
|
+
if (!isRecord(value) || !isRecord(value.paths)) {
|
|
13
|
+
throw new Error('OpenAPI document is missing paths.');
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
async function readCache() {
|
|
18
|
+
try {
|
|
19
|
+
const [file, metadata] = await Promise.all([
|
|
20
|
+
readFile(OPENAPI_CACHE_FILE, 'utf8'),
|
|
21
|
+
stat(OPENAPI_CACHE_FILE),
|
|
22
|
+
]);
|
|
23
|
+
const ageMs = Date.now() - metadata.mtimeMs;
|
|
24
|
+
return {
|
|
25
|
+
spec: asSpec(JSON.parse(file)),
|
|
26
|
+
source: ageMs > OPENAPI_CACHE_TTL_MS ? 'cache-stale' : 'cache',
|
|
27
|
+
cacheAgeMs: ageMs,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function fetchSpec() {
|
|
35
|
+
const response = await fetch(PLATFORM_OPENAPI_URL);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`OpenAPI fetch failed: HTTP ${response.status}`);
|
|
38
|
+
}
|
|
39
|
+
return asSpec(await response.json());
|
|
40
|
+
}
|
|
41
|
+
export async function loadOpenApiSpec(ctx, options = {}) {
|
|
42
|
+
const cached = await readCache();
|
|
43
|
+
if (cached && cached.source === 'cache' && !options.refresh)
|
|
44
|
+
return cached;
|
|
45
|
+
try {
|
|
46
|
+
const spec = await fetchSpec();
|
|
47
|
+
await mkdir(AUTH_DIR, { recursive: true });
|
|
48
|
+
await writeFile(OPENAPI_CACHE_FILE, `${JSON.stringify(spec, null, 2)}\n`, { mode: 0o600 });
|
|
49
|
+
return { spec, source: 'network', cacheAgeMs: 0 };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (cached) {
|
|
53
|
+
log(ctx, warning(`Using stale OpenAPI cache: ${error instanceof Error ? error.message : String(error)}`));
|
|
54
|
+
return { ...cached, source: 'cache-stale' };
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function operations(spec) {
|
|
60
|
+
const result = [];
|
|
61
|
+
for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
62
|
+
if (!isRecord(pathItem))
|
|
63
|
+
continue;
|
|
64
|
+
for (const method of HTTP_METHODS) {
|
|
65
|
+
const operation = pathItem[method];
|
|
66
|
+
if (!isRecord(operation))
|
|
67
|
+
continue;
|
|
68
|
+
const typed = operation;
|
|
69
|
+
result.push({
|
|
70
|
+
operationId: typed.operationId ?? `${method}_${path.replace(/[^a-zA-Z0-9]+/g, '_')}`,
|
|
71
|
+
method: method.toUpperCase(),
|
|
72
|
+
path,
|
|
73
|
+
summary: typed.summary ?? '',
|
|
74
|
+
tags: typed.tags ?? [],
|
|
75
|
+
parameters: typed.parameters ?? [],
|
|
76
|
+
requestBody: typed.requestBody,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result.sort((a, b) => `${a.tags[0] ?? ''}:${a.path}:${a.method}`.localeCompare(`${b.tags[0] ?? ''}:${b.path}:${b.method}`));
|
|
81
|
+
}
|
|
82
|
+
function findOperation(spec, target, maybePath) {
|
|
83
|
+
const all = operations(spec);
|
|
84
|
+
if (maybePath) {
|
|
85
|
+
const method = target.toUpperCase();
|
|
86
|
+
return all.find((operation) => operation.method === method && operation.path === maybePath) ?? null;
|
|
87
|
+
}
|
|
88
|
+
const normalized = target.trim();
|
|
89
|
+
const methodPath = normalized.match(/^([A-Za-z]+)\s+(.+)$/);
|
|
90
|
+
if (methodPath) {
|
|
91
|
+
return all.find((operation) => operation.method === methodPath[1].toUpperCase() && operation.path === methodPath[2]) ?? null;
|
|
92
|
+
}
|
|
93
|
+
return all.find((operation) => operation.operationId === normalized) ?? null;
|
|
94
|
+
}
|
|
95
|
+
function schemaType(schema) {
|
|
96
|
+
if (!schema)
|
|
97
|
+
return 'unknown';
|
|
98
|
+
if (typeof schema.type === 'string')
|
|
99
|
+
return schema.type;
|
|
100
|
+
if (typeof schema.$ref === 'string')
|
|
101
|
+
return schema.$ref.split('/').pop() ?? schema.$ref;
|
|
102
|
+
if (Array.isArray(schema.anyOf))
|
|
103
|
+
return schema.anyOf.map((item) => isRecord(item) ? schemaType(item) : 'unknown').join(' | ');
|
|
104
|
+
return 'object';
|
|
105
|
+
}
|
|
106
|
+
function requestContent(operation) {
|
|
107
|
+
return Object.keys(operation.requestBody?.content ?? {});
|
|
108
|
+
}
|
|
109
|
+
function printSchemaSource(ctx, loaded) {
|
|
110
|
+
const age = loaded.cacheAgeMs === undefined ? '' : ` (${Math.round(loaded.cacheAgeMs / 1000)}s old)`;
|
|
111
|
+
log(ctx, field('Schema', `${loaded.source}${age}`));
|
|
112
|
+
}
|
|
113
|
+
export async function openApiList(ctx, options) {
|
|
114
|
+
const loaded = await loadOpenApiSpec(ctx, options);
|
|
115
|
+
const all = operations(loaded.spec).filter((operation) => {
|
|
116
|
+
const tagMatch = !options.tag || operation.tags.some((tag) => tag.toLowerCase() === options.tag?.toLowerCase());
|
|
117
|
+
const search = options.search?.toLowerCase();
|
|
118
|
+
const searchMatch = !search
|
|
119
|
+
|| operation.operationId.toLowerCase().includes(search)
|
|
120
|
+
|| operation.path.toLowerCase().includes(search)
|
|
121
|
+
|| operation.summary.toLowerCase().includes(search)
|
|
122
|
+
|| operation.tags.some((tag) => tag.toLowerCase().includes(search));
|
|
123
|
+
return tagMatch && searchMatch;
|
|
124
|
+
});
|
|
125
|
+
if (ctx.json) {
|
|
126
|
+
printJson({
|
|
127
|
+
ok: true,
|
|
128
|
+
schemaSource: loaded.source,
|
|
129
|
+
count: all.length,
|
|
130
|
+
operations: all,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
printBannerFor(ctx);
|
|
135
|
+
log(ctx, section('API Operations'));
|
|
136
|
+
printSchemaSource(ctx, loaded);
|
|
137
|
+
log(ctx, field('Count', String(all.length)));
|
|
138
|
+
log(ctx, '');
|
|
139
|
+
for (const operation of all) {
|
|
140
|
+
const tag = operation.tags[0] ? `[${operation.tags[0]}] ` : '';
|
|
141
|
+
log(ctx, `${operation.method.padEnd(6)} ${operation.path.padEnd(58)} ${tag}${operation.operationId}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function openApiDescribe(ctx, target, maybePath, options) {
|
|
145
|
+
const loaded = await loadOpenApiSpec(ctx, options);
|
|
146
|
+
const operation = findOperation(loaded.spec, target, maybePath);
|
|
147
|
+
if (!operation) {
|
|
148
|
+
throw new Error('Operation not found. Use `wacht api ls` to find operation IDs and paths.');
|
|
149
|
+
}
|
|
150
|
+
if (ctx.json) {
|
|
151
|
+
printJson({
|
|
152
|
+
ok: true,
|
|
153
|
+
schemaSource: loaded.source,
|
|
154
|
+
operation,
|
|
155
|
+
requestContentTypes: requestContent(operation),
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
printBannerFor(ctx);
|
|
160
|
+
log(ctx, section(operation.operationId));
|
|
161
|
+
printSchemaSource(ctx, loaded);
|
|
162
|
+
log(ctx, field('Method', operation.method));
|
|
163
|
+
log(ctx, field('Path', operation.path));
|
|
164
|
+
if (operation.summary)
|
|
165
|
+
log(ctx, field('Summary', operation.summary));
|
|
166
|
+
if (operation.tags.length)
|
|
167
|
+
log(ctx, field('Tags', operation.tags.join(', ')));
|
|
168
|
+
if (operation.parameters.length) {
|
|
169
|
+
log(ctx, '');
|
|
170
|
+
log(ctx, section('Parameters'));
|
|
171
|
+
for (const parameter of operation.parameters) {
|
|
172
|
+
const required = parameter.required ? 'required' : 'optional';
|
|
173
|
+
log(ctx, `${parameter.name ?? 'unknown'} (${parameter.in ?? 'unknown'}, ${required}, ${schemaType(parameter.schema)})`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const contentTypes = requestContent(operation);
|
|
177
|
+
if (contentTypes.length) {
|
|
178
|
+
log(ctx, '');
|
|
179
|
+
log(ctx, section('Request Body'));
|
|
180
|
+
for (const contentType of contentTypes)
|
|
181
|
+
log(ctx, contentType);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function applyPathParams(path, params) {
|
|
185
|
+
let next = path;
|
|
186
|
+
for (const [key, value] of params) {
|
|
187
|
+
next = next.replaceAll(`{${key}}`, encodeURIComponent(value));
|
|
188
|
+
}
|
|
189
|
+
const missing = next.match(/{[^}]+}/g);
|
|
190
|
+
if (missing) {
|
|
191
|
+
throw new Error(`Missing path parameter(s): ${missing.map((item) => item.slice(1, -1)).join(', ')}. Pass --param key=value.`);
|
|
192
|
+
}
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
function appendQueryParams(path, params, operation) {
|
|
196
|
+
const queryNames = new Set(operation.parameters.filter((parameter) => parameter.in === 'query').map((parameter) => parameter.name));
|
|
197
|
+
const url = new URL(path, 'https://machine.local');
|
|
198
|
+
for (const [key, value] of params) {
|
|
199
|
+
if (queryNames.has(key)) {
|
|
200
|
+
url.searchParams.set(key, value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return `${url.pathname}${url.search}`;
|
|
204
|
+
}
|
|
205
|
+
export async function openApiCall(ctx, target, options) {
|
|
206
|
+
const loaded = await loadOpenApiSpec(ctx, options);
|
|
207
|
+
const operation = findOperation(loaded.spec, target);
|
|
208
|
+
if (!operation) {
|
|
209
|
+
throw new Error('Operation not found. Use `wacht api ls` to find operation IDs.');
|
|
210
|
+
}
|
|
211
|
+
const context = await readBenchContext();
|
|
212
|
+
const deploymentId = options.deployment ?? context?.deployment_id;
|
|
213
|
+
const params = entries(options.param, '--param');
|
|
214
|
+
const apiOptions = {
|
|
215
|
+
body: options.body,
|
|
216
|
+
field: options.field,
|
|
217
|
+
form: options.form,
|
|
218
|
+
file: options.file,
|
|
219
|
+
header: options.header,
|
|
220
|
+
};
|
|
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//')) {
|
|
226
|
+
throw new Error('Select an active deployment first, or pass raw paths with `wacht api METHOD /path`.');
|
|
227
|
+
}
|
|
228
|
+
const { body, headers } = await requestBody(apiOptions);
|
|
229
|
+
const data = await machineRequest(machinePath, {
|
|
230
|
+
method: operation.method,
|
|
231
|
+
body,
|
|
232
|
+
headers,
|
|
233
|
+
});
|
|
234
|
+
if (ctx.json) {
|
|
235
|
+
printJson({
|
|
236
|
+
ok: true,
|
|
237
|
+
schemaSource: loaded.source,
|
|
238
|
+
operationId: operation.operationId,
|
|
239
|
+
method: operation.method,
|
|
240
|
+
path: machinePath,
|
|
241
|
+
data,
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
printJson(data);
|
|
246
|
+
}
|
|
247
|
+
export async function openApiRefresh(ctx) {
|
|
248
|
+
const loaded = await loadOpenApiSpec(ctx, { refresh: true });
|
|
249
|
+
const count = operations(loaded.spec).length;
|
|
250
|
+
if (ctx.json) {
|
|
251
|
+
printJson({ ok: true, schemaSource: loaded.source, count, cacheFile: OPENAPI_CACHE_FILE });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
printBannerFor(ctx);
|
|
255
|
+
log(ctx, section('OpenAPI Schema'));
|
|
256
|
+
log(ctx, field('Source', loaded.source));
|
|
257
|
+
log(ctx, field('Operations', String(count)));
|
|
258
|
+
log(ctx, field('Cache', OPENAPI_CACHE_FILE));
|
|
259
|
+
}
|
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
export function base64Url(input) {
|
|
3
|
+
return Buffer.from(input)
|
|
4
|
+
.toString('base64')
|
|
5
|
+
.replaceAll('+', '-')
|
|
6
|
+
.replaceAll('/', '_')
|
|
7
|
+
.replaceAll('=', '');
|
|
8
|
+
}
|
|
9
|
+
export function randomToken(bytes = 32) {
|
|
10
|
+
return base64Url(randomBytes(bytes));
|
|
11
|
+
}
|
|
12
|
+
export function codeChallengeFor(verifier) {
|
|
13
|
+
return base64Url(createHash('sha256').update(verifier).digest());
|
|
14
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
async function fileText(root, relativePath) {
|
|
4
|
+
try {
|
|
5
|
+
return await readFile(path.join(root, relativePath), 'utf8');
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
async function readPackageJson(root) {
|
|
12
|
+
const raw = await fileText(root, 'package.json');
|
|
13
|
+
if (!raw)
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function detectPackageManager(root) {
|
|
23
|
+
if (await fileText(root, 'pnpm-lock.yaml'))
|
|
24
|
+
return 'pnpm';
|
|
25
|
+
if (await fileText(root, 'yarn.lock'))
|
|
26
|
+
return 'yarn';
|
|
27
|
+
if (await fileText(root, 'bun.lock') || await fileText(root, 'bun.lockb'))
|
|
28
|
+
return 'bun';
|
|
29
|
+
if (await fileText(root, 'package-lock.json'))
|
|
30
|
+
return 'npm';
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
function hasDependency(pkg, name) {
|
|
34
|
+
return !!pkg?.dependencies?.[name] || !!pkg?.devDependencies?.[name];
|
|
35
|
+
}
|
|
36
|
+
function unique(values) {
|
|
37
|
+
return [...new Set(values)];
|
|
38
|
+
}
|
|
39
|
+
export async function detectProject(root) {
|
|
40
|
+
const pkg = await readPackageJson(root);
|
|
41
|
+
const frameworks = [];
|
|
42
|
+
const suggestedSkills = ['wacht', 'wacht-setup'];
|
|
43
|
+
if (hasDependency(pkg, 'next')) {
|
|
44
|
+
frameworks.push('Next.js');
|
|
45
|
+
suggestedSkills.push('wacht-nextjs-patterns');
|
|
46
|
+
}
|
|
47
|
+
if (hasDependency(pkg, 'react-router') || hasDependency(pkg, '@react-router/dev')) {
|
|
48
|
+
frameworks.push('React Router');
|
|
49
|
+
suggestedSkills.push('wacht-react-router-patterns');
|
|
50
|
+
}
|
|
51
|
+
if (hasDependency(pkg, '@tanstack/react-router')) {
|
|
52
|
+
frameworks.push('TanStack Router');
|
|
53
|
+
suggestedSkills.push('wacht-tanstack-router-patterns');
|
|
54
|
+
}
|
|
55
|
+
if (hasDependency(pkg, '@wacht/backend')) {
|
|
56
|
+
suggestedSkills.push('wacht-backend-js', 'wacht-api-auth');
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
root,
|
|
60
|
+
packageManager: await detectPackageManager(root),
|
|
61
|
+
frameworks: unique(frameworks),
|
|
62
|
+
suggestedSkills: unique(suggestedSkills),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
export function canPrompt(ctx) {
|
|
4
|
+
return ctx.interactive && process.stdin.isTTY && process.stdout.isTTY;
|
|
5
|
+
}
|
|
6
|
+
async function ask(question) {
|
|
7
|
+
const rl = createInterface({ input, output });
|
|
8
|
+
try {
|
|
9
|
+
return (await rl.question(question)).trim();
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
rl.close();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function promptText(ctx, value, question, missingMessage) {
|
|
16
|
+
if (value && value.trim())
|
|
17
|
+
return value.trim();
|
|
18
|
+
if (!canPrompt(ctx))
|
|
19
|
+
throw new Error(missingMessage);
|
|
20
|
+
const answer = await ask(question);
|
|
21
|
+
if (!answer)
|
|
22
|
+
throw new Error(missingMessage);
|
|
23
|
+
return answer;
|
|
24
|
+
}
|
|
25
|
+
export async function promptOptionalText(ctx, value, question) {
|
|
26
|
+
if (value && value.trim())
|
|
27
|
+
return value.trim();
|
|
28
|
+
if (!canPrompt(ctx))
|
|
29
|
+
return undefined;
|
|
30
|
+
const answer = await ask(question);
|
|
31
|
+
return answer || undefined;
|
|
32
|
+
}
|
|
33
|
+
export async function promptChoice(ctx, value, choices, question, missingMessage) {
|
|
34
|
+
if (value) {
|
|
35
|
+
if (!choices.includes(value)) {
|
|
36
|
+
throw new Error(`${missingMessage} Allowed values: ${choices.join(', ')}.`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (!canPrompt(ctx))
|
|
41
|
+
throw new Error(missingMessage);
|
|
42
|
+
for (const [index, choice] of choices.entries()) {
|
|
43
|
+
console.log(`${index + 1}. ${choice}`);
|
|
44
|
+
}
|
|
45
|
+
console.log('');
|
|
46
|
+
const answer = await ask(question);
|
|
47
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
48
|
+
if (Number.isInteger(index) && index >= 0 && index < choices.length) {
|
|
49
|
+
return choices[index];
|
|
50
|
+
}
|
|
51
|
+
if (choices.includes(answer))
|
|
52
|
+
return answer;
|
|
53
|
+
throw new Error(`${missingMessage} Allowed values: ${choices.join(', ')}.`);
|
|
54
|
+
}
|
|
55
|
+
export async function promptList(ctx, values, question, fallback) {
|
|
56
|
+
if (values && values.length)
|
|
57
|
+
return values;
|
|
58
|
+
if (!canPrompt(ctx))
|
|
59
|
+
return fallback;
|
|
60
|
+
const answer = await ask(question);
|
|
61
|
+
if (!answer)
|
|
62
|
+
return fallback;
|
|
63
|
+
return answer.split(',').map((item) => item.trim()).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
export async function promptOptionalList(ctx, values, question) {
|
|
66
|
+
if (values && values.length)
|
|
67
|
+
return values;
|
|
68
|
+
if (!canPrompt(ctx))
|
|
69
|
+
return [];
|
|
70
|
+
const answer = await ask(question);
|
|
71
|
+
if (!answer)
|
|
72
|
+
return [];
|
|
73
|
+
return answer.split(',').map((item) => item.trim()).filter(Boolean);
|
|
74
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { readBenchContext } from './context-store.js';
|
|
2
|
+
import { entries, machineRequest, requestBody } from './machine-api.js';
|
|
3
|
+
import { field, log, printBannerFor, printJson, section } from './ui.js';
|
|
4
|
+
async function deploymentScopedPath(pathname, override) {
|
|
5
|
+
const ctx = await readBenchContext();
|
|
6
|
+
const deploymentId = override ?? ctx?.deployment_id;
|
|
7
|
+
if (!deploymentId) {
|
|
8
|
+
throw new Error('No active deployment. Run `wacht deployments select` or pass --deployment <id>.');
|
|
9
|
+
}
|
|
10
|
+
return `/deployments/${deploymentId}${pathname.startsWith('/') ? pathname : `/${pathname}`}`;
|
|
11
|
+
}
|
|
12
|
+
async function callDeployment(pathname, override, init = {}) {
|
|
13
|
+
let resolved = await deploymentScopedPath(pathname, override);
|
|
14
|
+
if (init.query) {
|
|
15
|
+
const query = new URLSearchParams();
|
|
16
|
+
for (const [key, value] of Object.entries(init.query)) {
|
|
17
|
+
if (value !== undefined && value !== '')
|
|
18
|
+
query.set(key, value);
|
|
19
|
+
}
|
|
20
|
+
const search = query.toString();
|
|
21
|
+
if (search)
|
|
22
|
+
resolved = `${resolved}?${search}`;
|
|
23
|
+
}
|
|
24
|
+
const { query: _query, ...rest } = init;
|
|
25
|
+
return machineRequest(resolved, rest);
|
|
26
|
+
}
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function pickArray(value) {
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return value;
|
|
33
|
+
if (isRecord(value)) {
|
|
34
|
+
if (Array.isArray(value.data))
|
|
35
|
+
return value.data;
|
|
36
|
+
if (Array.isArray(value.items))
|
|
37
|
+
return value.items;
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
function pad(value, width) {
|
|
42
|
+
return value.padEnd(width, ' ');
|
|
43
|
+
}
|
|
44
|
+
function truncate(value, max) {
|
|
45
|
+
if (value.length <= max)
|
|
46
|
+
return value;
|
|
47
|
+
return `${value.slice(0, Math.max(0, max - 1))}~`;
|
|
48
|
+
}
|
|
49
|
+
function shortDate(value) {
|
|
50
|
+
if (typeof value !== 'string')
|
|
51
|
+
return '-';
|
|
52
|
+
const date = new Date(value);
|
|
53
|
+
return Number.isNaN(date.getTime()) ? value : date.toISOString().slice(0, 10);
|
|
54
|
+
}
|
|
55
|
+
function printRows(ctx, title, rows, headers) {
|
|
56
|
+
printBannerFor(ctx);
|
|
57
|
+
log(ctx, section(title));
|
|
58
|
+
log(ctx, field('Count', String(rows.length)));
|
|
59
|
+
log(ctx, '');
|
|
60
|
+
if (rows.length === 0) {
|
|
61
|
+
log(ctx, '(none)');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const widths = headers.map((h) => Math.max(h.label.length, ...rows.map((row) => (row[h.key] ?? '').length), h.width ?? 0));
|
|
65
|
+
log(ctx, headers.map((h, i) => pad(h.label, widths[i])).join(' '));
|
|
66
|
+
log(ctx, headers.map((_, i) => '-'.repeat(widths[i])).join(' '));
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
log(ctx, headers.map((h, i) => pad(row[h.key] ?? '', widths[i])).join(' '));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ─── Users ──────────────────────────────────────────────────────────
|
|
72
|
+
export async function listUsers(ctx, options) {
|
|
73
|
+
const data = await callDeployment('/users', options.deployment, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
query: { limit: options.limit ?? '', offset: options.offset ?? '', search: options.search ?? '' },
|
|
76
|
+
});
|
|
77
|
+
const items = pickArray(data);
|
|
78
|
+
if (ctx.json) {
|
|
79
|
+
printJson({ ok: true, data });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const rows = items.filter(isRecord).map((u) => ({
|
|
83
|
+
id: truncate(String(u.id ?? ''), 22),
|
|
84
|
+
email: truncate(String(u.primary_email_address?.toString().includes('@') ? u.primary_email_address : (u.email ?? '-')), 32),
|
|
85
|
+
name: truncate([u.first_name, u.last_name].filter(Boolean).join(' ') || '-', 24),
|
|
86
|
+
created: shortDate(u.created_at),
|
|
87
|
+
}));
|
|
88
|
+
printRows(ctx, 'Users', rows, [
|
|
89
|
+
{ key: 'id', label: 'ID' },
|
|
90
|
+
{ key: 'email', label: 'Email' },
|
|
91
|
+
{ key: 'name', label: 'Name' },
|
|
92
|
+
{ key: 'created', label: 'Created' },
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
export async function getUser(ctx, userId, options) {
|
|
96
|
+
const data = await callDeployment(`/users/${encodeURIComponent(userId)}/details`, options.deployment, { method: 'GET' });
|
|
97
|
+
if (ctx.json) {
|
|
98
|
+
printJson({ ok: true, data });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
printJson(data);
|
|
102
|
+
}
|
|
103
|
+
export async function createUser(ctx, options) {
|
|
104
|
+
const path = await deploymentScopedPath('/users', options.deployment);
|
|
105
|
+
const { body, headers } = await requestBody(options);
|
|
106
|
+
const data = await machineRequest(path, { method: 'POST', body, headers });
|
|
107
|
+
if (ctx.json) {
|
|
108
|
+
printJson({ ok: true, data });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
printJson(data);
|
|
112
|
+
}
|
|
113
|
+
// ─── Organizations ──────────────────────────────────────────────────
|
|
114
|
+
export async function listOrgs(ctx, options) {
|
|
115
|
+
const data = await callDeployment('/organizations', options.deployment, {
|
|
116
|
+
method: 'GET',
|
|
117
|
+
query: { limit: options.limit ?? '', offset: options.offset ?? '', search: options.search ?? '' },
|
|
118
|
+
});
|
|
119
|
+
const items = pickArray(data);
|
|
120
|
+
if (ctx.json) {
|
|
121
|
+
printJson({ ok: true, data });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const rows = items.filter(isRecord).map((o) => ({
|
|
125
|
+
id: truncate(String(o.id ?? ''), 22),
|
|
126
|
+
name: truncate(String(o.name ?? '-'), 32),
|
|
127
|
+
members: String(o.member_count ?? o.members_count ?? '-'),
|
|
128
|
+
created: shortDate(o.created_at),
|
|
129
|
+
}));
|
|
130
|
+
printRows(ctx, 'Organizations', rows, [
|
|
131
|
+
{ key: 'id', label: 'ID' },
|
|
132
|
+
{ key: 'name', label: 'Name' },
|
|
133
|
+
{ key: 'members', label: 'Members' },
|
|
134
|
+
{ key: 'created', label: 'Created' },
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
export async function getOrg(ctx, orgId, options) {
|
|
138
|
+
const data = await callDeployment(`/organizations/${encodeURIComponent(orgId)}`, options.deployment, { method: 'GET' });
|
|
139
|
+
if (ctx.json) {
|
|
140
|
+
printJson({ ok: true, data });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
printJson(data);
|
|
144
|
+
}
|
|
145
|
+
export async function createOrg(ctx, options) {
|
|
146
|
+
const path = await deploymentScopedPath('/organizations', options.deployment);
|
|
147
|
+
const { body, headers } = await requestBody(options);
|
|
148
|
+
const data = await machineRequest(path, { method: 'POST', body, headers });
|
|
149
|
+
if (ctx.json) {
|
|
150
|
+
printJson({ ok: true, data });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
printJson(data);
|
|
154
|
+
}
|
|
155
|
+
// ─── Workspaces ─────────────────────────────────────────────────────
|
|
156
|
+
export async function listWorkspaces(ctx, options) {
|
|
157
|
+
const pathname = options.org
|
|
158
|
+
? `/organizations/${encodeURIComponent(options.org)}/workspaces`
|
|
159
|
+
: '/workspaces';
|
|
160
|
+
const data = await callDeployment(pathname, options.deployment, {
|
|
161
|
+
method: 'GET',
|
|
162
|
+
query: { limit: options.limit ?? '', offset: options.offset ?? '', search: options.search ?? '' },
|
|
163
|
+
});
|
|
164
|
+
const items = pickArray(data);
|
|
165
|
+
if (ctx.json) {
|
|
166
|
+
printJson({ ok: true, data });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const rows = items.filter(isRecord).map((w) => ({
|
|
170
|
+
id: truncate(String(w.id ?? ''), 22),
|
|
171
|
+
name: truncate(String(w.name ?? '-'), 28),
|
|
172
|
+
org: truncate(String(w.organization_id ?? '-'), 22),
|
|
173
|
+
created: shortDate(w.created_at),
|
|
174
|
+
}));
|
|
175
|
+
printRows(ctx, 'Workspaces', rows, [
|
|
176
|
+
{ key: 'id', label: 'ID' },
|
|
177
|
+
{ key: 'name', label: 'Name' },
|
|
178
|
+
{ key: 'org', label: 'Org ID' },
|
|
179
|
+
{ key: 'created', label: 'Created' },
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
export async function getWorkspace(ctx, workspaceId, options) {
|
|
183
|
+
const data = await callDeployment(`/workspaces/${encodeURIComponent(workspaceId)}`, options.deployment, { method: 'GET' });
|
|
184
|
+
if (ctx.json) {
|
|
185
|
+
printJson({ ok: true, data });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
printJson(data);
|
|
189
|
+
}
|
|
190
|
+
export async function createWorkspace(ctx, options) {
|
|
191
|
+
if (!options.org) {
|
|
192
|
+
throw new Error('Workspaces are scoped to an organization. Pass --org <organization_id>.');
|
|
193
|
+
}
|
|
194
|
+
const path = await deploymentScopedPath(`/organizations/${encodeURIComponent(options.org)}/workspaces`, options.deployment);
|
|
195
|
+
const { body, headers } = await requestBody(options);
|
|
196
|
+
const data = await machineRequest(path, { method: 'POST', body, headers });
|
|
197
|
+
if (ctx.json) {
|
|
198
|
+
printJson({ ok: true, data });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
printJson(data);
|
|
202
|
+
}
|
|
203
|
+
// re-exported for ergonomics
|
|
204
|
+
export { entries };
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { SKILLS_SOURCE } from './config.js';
|
|
3
|
+
import { valueAfter } from './util.js';
|
|
4
|
+
export function installSkills(skill) {
|
|
5
|
+
const installArgs = ['skills', 'add', SKILLS_SOURCE];
|
|
6
|
+
if (skill)
|
|
7
|
+
installArgs.push('--skill', skill);
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const child = spawn('npx', installArgs, {
|
|
10
|
+
stdio: 'inherit',
|
|
11
|
+
shell: process.platform === 'win32',
|
|
12
|
+
});
|
|
13
|
+
child.on('error', reject);
|
|
14
|
+
child.on('exit', (code, signal) => {
|
|
15
|
+
if (signal) {
|
|
16
|
+
reject(new Error(`Skill install interrupted by ${signal}`));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (code === 0) {
|
|
20
|
+
resolve();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
reject(new Error(`Skill install failed with exit code ${code ?? 1}`));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function skillsInstall(args) {
|
|
28
|
+
await installSkills(valueAfter(args, '--skill'));
|
|
29
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|