@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
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { PLATFORM_OPENAPI_URL } from './config.js';
|
|
3
|
+
import { readBenchContext } from './context-store.js';
|
|
4
|
+
import { machineRequest } from './machine-api.js';
|
|
5
|
+
import { loadOpenApiSpec } from './openapi.js';
|
|
6
|
+
import { field, log, printBannerFor, printJson, section, success, warning } from './ui.js';
|
|
7
|
+
const DEFAULT_CONFIG_FILE = 'wacht.config.json';
|
|
8
|
+
const CONFIG_SCHEMA_URL = 'https://wacht.dev/schemas/wacht.config.schema.json';
|
|
9
|
+
const METADATA_KEYS = new Set(['id', 'deployment_id', 'created_at', 'updated_at', 'deleted_at']);
|
|
10
|
+
const SETTINGS_SECTIONS = ['auth', 'display', 'b2b', 'restrictions'];
|
|
11
|
+
const AUTH_UPDATE_KEYS = new Set([
|
|
12
|
+
'email',
|
|
13
|
+
'phone',
|
|
14
|
+
'username',
|
|
15
|
+
'password',
|
|
16
|
+
'name',
|
|
17
|
+
'authentication_factors',
|
|
18
|
+
'second_factor_policy',
|
|
19
|
+
'first_factor',
|
|
20
|
+
'backup_code',
|
|
21
|
+
'web3_wallet',
|
|
22
|
+
'multi_session_support',
|
|
23
|
+
'session_token_lifetime',
|
|
24
|
+
'session_validity_period',
|
|
25
|
+
'session_inactive_timeout',
|
|
26
|
+
]);
|
|
27
|
+
const DISPLAY_UPDATE_KEYS = new Set([
|
|
28
|
+
'app_name',
|
|
29
|
+
'tos_page_url',
|
|
30
|
+
'sign_in_page_url',
|
|
31
|
+
'sign_up_page_url',
|
|
32
|
+
'after_sign_out_one_page_url',
|
|
33
|
+
'after_sign_out_all_page_url',
|
|
34
|
+
'favicon_image_url',
|
|
35
|
+
'logo_image_url',
|
|
36
|
+
'privacy_policy_url',
|
|
37
|
+
'signup_terms_statement',
|
|
38
|
+
'signup_terms_statement_shown',
|
|
39
|
+
'light_mode_settings',
|
|
40
|
+
'dark_mode_settings',
|
|
41
|
+
'after_logo_click_url',
|
|
42
|
+
'organization_profile_url',
|
|
43
|
+
'create_organization_url',
|
|
44
|
+
'default_user_profile_image_url',
|
|
45
|
+
'default_organization_profile_image_url',
|
|
46
|
+
'default_workspace_profile_image_url',
|
|
47
|
+
'use_initials_for_user_profile_image',
|
|
48
|
+
'use_initials_for_organization_profile_image',
|
|
49
|
+
'after_signup_redirect_url',
|
|
50
|
+
'after_signin_redirect_url',
|
|
51
|
+
'user_profile_url',
|
|
52
|
+
'after_create_organization_redirect_url',
|
|
53
|
+
'waitlist_page_url',
|
|
54
|
+
'support_page_url',
|
|
55
|
+
]);
|
|
56
|
+
const B2B_UPDATE_KEYS = new Set([
|
|
57
|
+
'organizations_enabled',
|
|
58
|
+
'workspaces_enabled',
|
|
59
|
+
'ip_allowlist_per_org_enabled',
|
|
60
|
+
'ip_allowlist_per_workspace_enabled',
|
|
61
|
+
'enforce_mfa_per_org_enabled',
|
|
62
|
+
'enforce_mfa_per_workspace_enabled',
|
|
63
|
+
'enterprise_sso_enabled',
|
|
64
|
+
'allow_users_to_create_orgs',
|
|
65
|
+
'max_allowed_org_members',
|
|
66
|
+
'max_allowed_workspace_members',
|
|
67
|
+
'allow_org_deletion',
|
|
68
|
+
'allow_workspace_deletion',
|
|
69
|
+
'custom_org_role_enabled',
|
|
70
|
+
'custom_workspace_role_enabled',
|
|
71
|
+
'default_workspace_creator_role_id',
|
|
72
|
+
'default_workspace_member_role_id',
|
|
73
|
+
'default_org_creator_role_id',
|
|
74
|
+
'default_org_member_role_id',
|
|
75
|
+
'limit_org_creation_per_user',
|
|
76
|
+
'limit_workspace_creation_per_org',
|
|
77
|
+
'org_creation_per_user_count',
|
|
78
|
+
'workspaces_per_org_count',
|
|
79
|
+
'workspace_permissions',
|
|
80
|
+
'organization_permissions',
|
|
81
|
+
'workspace_permission_catalog',
|
|
82
|
+
'organization_permission_catalog',
|
|
83
|
+
]);
|
|
84
|
+
const RESTRICTIONS_UPDATE_KEYS = new Set([
|
|
85
|
+
'allowlist_enabled',
|
|
86
|
+
'blocklist_enabled',
|
|
87
|
+
'block_subaddresses',
|
|
88
|
+
'block_disposable_emails',
|
|
89
|
+
'block_voip_numbers',
|
|
90
|
+
'country_restrictions',
|
|
91
|
+
'banned_keywords',
|
|
92
|
+
'allowlisted_resources',
|
|
93
|
+
'blocklisted_resources',
|
|
94
|
+
'sign_up_mode',
|
|
95
|
+
'waitlist_collect_names',
|
|
96
|
+
]);
|
|
97
|
+
function isRecord(value) {
|
|
98
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function asJsonObject(value, label) {
|
|
101
|
+
if (!isRecord(value))
|
|
102
|
+
throw new Error(`${label} must be an object.`);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function removeMetadata(value) {
|
|
106
|
+
const source = asJsonObject(value ?? {}, 'settings section');
|
|
107
|
+
const next = {};
|
|
108
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
109
|
+
if (!METADATA_KEYS.has(key))
|
|
110
|
+
next[key] = entry;
|
|
111
|
+
}
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
function pickKeys(value, allowed) {
|
|
115
|
+
const source = removeMetadata(value);
|
|
116
|
+
const next = {};
|
|
117
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
118
|
+
if (allowed.has(key))
|
|
119
|
+
next[key] = entry;
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
function validateKeys(sectionName, value, allowed) {
|
|
124
|
+
if (!value)
|
|
125
|
+
return undefined;
|
|
126
|
+
const unsupported = Object.keys(value).filter((key) => !allowed.has(key));
|
|
127
|
+
if (unsupported.length) {
|
|
128
|
+
throw new Error(`Unsupported settings.${sectionName} key(s): ${unsupported.join(', ')}.`);
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
function authToPatch(value) {
|
|
133
|
+
if (!isRecord(value))
|
|
134
|
+
return undefined;
|
|
135
|
+
const auth = removeMetadata(value);
|
|
136
|
+
const next = {};
|
|
137
|
+
if (auth.email_address !== undefined)
|
|
138
|
+
next.email = auth.email_address;
|
|
139
|
+
if (auth.phone_number !== undefined)
|
|
140
|
+
next.phone = auth.phone_number;
|
|
141
|
+
if (auth.username !== undefined)
|
|
142
|
+
next.username = auth.username;
|
|
143
|
+
if (auth.password !== undefined)
|
|
144
|
+
next.password = auth.password;
|
|
145
|
+
if (auth.backup_code !== undefined)
|
|
146
|
+
next.backup_code = auth.backup_code;
|
|
147
|
+
if (auth.web3_wallet !== undefined)
|
|
148
|
+
next.web3_wallet = auth.web3_wallet;
|
|
149
|
+
if (auth.first_name !== undefined || auth.last_name !== undefined) {
|
|
150
|
+
const first = isRecord(auth.first_name) ? auth.first_name : {};
|
|
151
|
+
const last = isRecord(auth.last_name) ? auth.last_name : {};
|
|
152
|
+
next.name = {
|
|
153
|
+
first_name_enabled: first.enabled,
|
|
154
|
+
first_name_required: first.required,
|
|
155
|
+
last_name_enabled: last.enabled,
|
|
156
|
+
last_name_required: last.required,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (auth.auth_factors_enabled !== undefined || auth.magic_link !== undefined || auth.passkey !== undefined) {
|
|
160
|
+
const factors = isRecord(auth.auth_factors_enabled) ? auth.auth_factors_enabled : {};
|
|
161
|
+
next.authentication_factors = {
|
|
162
|
+
email_password_enabled: factors.email_password,
|
|
163
|
+
username_password_enabled: factors.username_password,
|
|
164
|
+
sso_enabled: factors.sso,
|
|
165
|
+
web3_wallet_enabled: factors.web3_wallet,
|
|
166
|
+
email_otp_enabled: factors.email_otp,
|
|
167
|
+
phone_otp_enabled: factors.phone_otp,
|
|
168
|
+
second_factor_authenticator_enabled: factors.authenticator,
|
|
169
|
+
second_factor_backup_code_enabled: factors.backup_code,
|
|
170
|
+
magic_link: auth.magic_link,
|
|
171
|
+
passkey: auth.passkey,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
for (const key of ['second_factor_policy', 'first_factor', 'multi_session_support', 'session_token_lifetime', 'session_validity_period', 'session_inactive_timeout']) {
|
|
175
|
+
if (auth[key] !== undefined)
|
|
176
|
+
next[key] = auth[key];
|
|
177
|
+
}
|
|
178
|
+
return pruneUndefined(next);
|
|
179
|
+
}
|
|
180
|
+
function pruneUndefined(value) {
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
return value.map((item) => pruneUndefined(item)).filter((item) => item !== undefined);
|
|
183
|
+
}
|
|
184
|
+
if (isRecord(value)) {
|
|
185
|
+
const next = {};
|
|
186
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
187
|
+
const pruned = pruneUndefined(entry);
|
|
188
|
+
if (pruned !== undefined)
|
|
189
|
+
next[key] = pruned;
|
|
190
|
+
}
|
|
191
|
+
return next;
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
function configFromDeployment(deployment, fallback) {
|
|
196
|
+
const source = asJsonObject(deployment, 'deployment settings response');
|
|
197
|
+
const settings = {};
|
|
198
|
+
const auth = authToPatch(source.auth_settings);
|
|
199
|
+
if (auth && Object.keys(auth).length)
|
|
200
|
+
settings.auth = auth;
|
|
201
|
+
if (source.ui_settings !== undefined)
|
|
202
|
+
settings.display = removeMetadata(source.ui_settings);
|
|
203
|
+
if (source.b2b_settings !== undefined)
|
|
204
|
+
settings.b2b = pickKeys(source.b2b_settings, B2B_UPDATE_KEYS);
|
|
205
|
+
if (source.restrictions !== undefined)
|
|
206
|
+
settings.restrictions = removeMetadata(source.restrictions);
|
|
207
|
+
return {
|
|
208
|
+
$schema: CONFIG_SCHEMA_URL,
|
|
209
|
+
version: 1,
|
|
210
|
+
deployment: {
|
|
211
|
+
id: String(source.id ?? fallback.id ?? ''),
|
|
212
|
+
mode: typeof source.mode === 'string' ? source.mode : fallback.mode,
|
|
213
|
+
project_id: fallback.project_id,
|
|
214
|
+
project_name: fallback.project_name,
|
|
215
|
+
},
|
|
216
|
+
settings,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function normalizeConfig(value) {
|
|
220
|
+
const source = asJsonObject(value, 'config file');
|
|
221
|
+
if (source.version !== 1)
|
|
222
|
+
throw new Error('Config file version must be 1.');
|
|
223
|
+
const settings = asJsonObject(source.settings, 'config settings');
|
|
224
|
+
const unknown = Object.keys(settings).filter((key) => !SETTINGS_SECTIONS.includes(key));
|
|
225
|
+
if (unknown.length) {
|
|
226
|
+
throw new Error(`Unsupported config section(s): ${unknown.join(', ')}.`);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
$schema: typeof source.$schema === 'string' ? source.$schema : CONFIG_SCHEMA_URL,
|
|
230
|
+
version: 1,
|
|
231
|
+
deployment: isRecord(source.deployment) ? {
|
|
232
|
+
id: typeof source.deployment.id === 'string' ? source.deployment.id : undefined,
|
|
233
|
+
mode: typeof source.deployment.mode === 'string' ? source.deployment.mode : undefined,
|
|
234
|
+
project_id: typeof source.deployment.project_id === 'string' ? source.deployment.project_id : undefined,
|
|
235
|
+
project_name: typeof source.deployment.project_name === 'string' ? source.deployment.project_name : undefined,
|
|
236
|
+
} : undefined,
|
|
237
|
+
settings: {
|
|
238
|
+
auth: validateKeys('auth', settings.auth === undefined ? undefined : pruneUndefined(asJsonObject(settings.auth, 'settings.auth')), AUTH_UPDATE_KEYS),
|
|
239
|
+
display: validateKeys('display', settings.display === undefined ? undefined : pruneUndefined(asJsonObject(settings.display, 'settings.display')), DISPLAY_UPDATE_KEYS),
|
|
240
|
+
b2b: validateKeys('b2b', settings.b2b === undefined ? undefined : pruneUndefined(asJsonObject(settings.b2b, 'settings.b2b')), B2B_UPDATE_KEYS),
|
|
241
|
+
restrictions: validateKeys('restrictions', settings.restrictions === undefined ? undefined : pruneUndefined(asJsonObject(settings.restrictions, 'settings.restrictions')), RESTRICTIONS_UPDATE_KEYS),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function sorted(value) {
|
|
246
|
+
if (Array.isArray(value))
|
|
247
|
+
return value.map((item) => sorted(item));
|
|
248
|
+
if (isRecord(value)) {
|
|
249
|
+
const next = {};
|
|
250
|
+
for (const key of Object.keys(value).sort()) {
|
|
251
|
+
next[key] = sorted(value[key]);
|
|
252
|
+
}
|
|
253
|
+
return next;
|
|
254
|
+
}
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
function jsonEqual(a, b) {
|
|
258
|
+
return JSON.stringify(sorted(a)) === JSON.stringify(sorted(b));
|
|
259
|
+
}
|
|
260
|
+
function diffValues(section, path, before, after, changes) {
|
|
261
|
+
if (jsonEqual(before, after))
|
|
262
|
+
return;
|
|
263
|
+
if (isRecord(before) && isRecord(after) && !Array.isArray(before) && !Array.isArray(after)) {
|
|
264
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
265
|
+
for (const key of [...keys].sort()) {
|
|
266
|
+
diffValues(section, path ? `${path}.${key}` : key, before[key], after[key], changes);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
changes.push({ section, path, before, after });
|
|
271
|
+
}
|
|
272
|
+
function diffConfigs(current, desired) {
|
|
273
|
+
const changes = [];
|
|
274
|
+
for (const sectionName of SETTINGS_SECTIONS) {
|
|
275
|
+
diffValues(sectionName, sectionName, current.settings[sectionName], desired.settings[sectionName], changes);
|
|
276
|
+
}
|
|
277
|
+
return changes;
|
|
278
|
+
}
|
|
279
|
+
function sectionChanges(changes) {
|
|
280
|
+
return [...new Set(changes.map((change) => change.section))];
|
|
281
|
+
}
|
|
282
|
+
function configSchema(openApiSchemas = {}) {
|
|
283
|
+
return {
|
|
284
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
285
|
+
$id: CONFIG_SCHEMA_URL,
|
|
286
|
+
type: 'object',
|
|
287
|
+
additionalProperties: false,
|
|
288
|
+
required: ['version', 'settings'],
|
|
289
|
+
properties: {
|
|
290
|
+
$schema: { type: 'string' },
|
|
291
|
+
version: { const: 1 },
|
|
292
|
+
deployment: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
properties: {
|
|
296
|
+
id: { type: 'string' },
|
|
297
|
+
mode: { enum: ['staging', 'production'] },
|
|
298
|
+
project_id: { type: 'string' },
|
|
299
|
+
project_name: { type: 'string' },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
settings: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
additionalProperties: false,
|
|
305
|
+
properties: {
|
|
306
|
+
auth: openApiSchemas.DeploymentAuthSettingsUpdates ?? { type: 'object' },
|
|
307
|
+
display: openApiSchemas.DeploymentDisplaySettingsUpdates ?? { type: 'object' },
|
|
308
|
+
b2b: openApiSchemas.DeploymentB2bSettingsUpdates ?? { type: 'object' },
|
|
309
|
+
restrictions: openApiSchemas.DeploymentRestrictionsUpdates ?? { type: 'object' },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
$defs: openApiSchemas,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async function readDesiredConfig(filePath) {
|
|
317
|
+
return normalizeConfig(JSON.parse(await readFile(filePath, 'utf8')));
|
|
318
|
+
}
|
|
319
|
+
async function deploymentTarget(options, desired) {
|
|
320
|
+
const context = await readBenchContext();
|
|
321
|
+
const id = options.deployment ?? desired?.deployment?.id ?? context?.deployment_id;
|
|
322
|
+
if (!id)
|
|
323
|
+
throw new Error('Select an active deployment first, pass --deployment <id>, or include deployment.id in the config file.');
|
|
324
|
+
return {
|
|
325
|
+
id,
|
|
326
|
+
mode: desired?.deployment?.mode ?? context?.deployment_mode,
|
|
327
|
+
project_id: desired?.deployment?.project_id ?? context?.project_id,
|
|
328
|
+
project_name: desired?.deployment?.project_name ?? context?.project_name,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async function pullConfigForTarget(ctx, target) {
|
|
332
|
+
const data = await machineRequest(`/deployments/${target.id}`);
|
|
333
|
+
return configFromDeployment(data, target);
|
|
334
|
+
}
|
|
335
|
+
function assertCanApply(target, options) {
|
|
336
|
+
if (target.mode === 'production') {
|
|
337
|
+
if (!options.production) {
|
|
338
|
+
throw new Error('Refusing to apply config to production without --production.');
|
|
339
|
+
}
|
|
340
|
+
if (options.confirm !== target.id) {
|
|
341
|
+
throw new Error(`Refusing to apply config to production without --confirm ${target.id}.`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!options.yes) {
|
|
345
|
+
throw new Error('Refusing to apply config without --yes. Use --dry-run to preview changes.');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function printChanges(ctx, changes) {
|
|
349
|
+
if (!changes.length) {
|
|
350
|
+
log(ctx, 'No config changes.');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
for (const change of changes) {
|
|
354
|
+
log(ctx, `${change.path}:`);
|
|
355
|
+
log(ctx, ` before: ${JSON.stringify(change.before)}`);
|
|
356
|
+
log(ctx, ` after: ${JSON.stringify(change.after)}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function applySection(deploymentId, sectionName, payload) {
|
|
360
|
+
const routes = {
|
|
361
|
+
auth: { method: 'PATCH', path: '/settings/auth' },
|
|
362
|
+
display: { method: 'PATCH', path: '/settings/display' },
|
|
363
|
+
b2b: { method: 'PATCH', path: '/settings/b2b' },
|
|
364
|
+
restrictions: { method: 'PATCH', path: '/settings/restrictions' },
|
|
365
|
+
};
|
|
366
|
+
const route = routes[sectionName];
|
|
367
|
+
if (!route)
|
|
368
|
+
throw new Error(`Unsupported config section: ${sectionName}`);
|
|
369
|
+
await machineRequest(`/deployments/${deploymentId}${route.path}`, {
|
|
370
|
+
method: route.method,
|
|
371
|
+
headers: { 'content-type': 'application/json' },
|
|
372
|
+
body: JSON.stringify(payload),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
export async function configPull(ctx, options) {
|
|
376
|
+
const target = await deploymentTarget(options);
|
|
377
|
+
const config = await pullConfigForTarget(ctx, target);
|
|
378
|
+
if (ctx.json) {
|
|
379
|
+
printJson({ ok: true, config });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const filePath = options.file ?? DEFAULT_CONFIG_FILE;
|
|
383
|
+
await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
384
|
+
printBannerFor(ctx);
|
|
385
|
+
log(ctx, section('Config Pulled'));
|
|
386
|
+
log(ctx, field('Deployment', `${config.deployment?.mode ?? 'unknown'} (${config.deployment?.id ?? target.id})`));
|
|
387
|
+
log(ctx, field('File', filePath));
|
|
388
|
+
log(ctx, warning('Secrets and provider credentials are not written to config.'));
|
|
389
|
+
}
|
|
390
|
+
export async function configSchemaCommand(ctx, options) {
|
|
391
|
+
const loaded = await loadOpenApiSpec(ctx, { refresh: options.refresh });
|
|
392
|
+
const schemas = isRecord(loaded.spec.components) && isRecord(loaded.spec.components.schemas)
|
|
393
|
+
? loaded.spec.components.schemas
|
|
394
|
+
: {};
|
|
395
|
+
printJson(configSchema(schemas));
|
|
396
|
+
}
|
|
397
|
+
export async function configDiff(ctx, options) {
|
|
398
|
+
const filePath = options.file ?? DEFAULT_CONFIG_FILE;
|
|
399
|
+
const desired = await readDesiredConfig(filePath);
|
|
400
|
+
const target = await deploymentTarget(options, desired);
|
|
401
|
+
const current = await pullConfigForTarget(ctx, target);
|
|
402
|
+
const changes = diffConfigs(current, desired);
|
|
403
|
+
if (ctx.json) {
|
|
404
|
+
printJson({ ok: true, deployment: target, file: filePath, changes });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
printBannerFor(ctx);
|
|
408
|
+
log(ctx, section('Config Diff'));
|
|
409
|
+
log(ctx, field('Deployment', `${target.mode ?? 'unknown'} (${target.id})`));
|
|
410
|
+
log(ctx, field('File', filePath));
|
|
411
|
+
log(ctx, '');
|
|
412
|
+
printChanges(ctx, changes);
|
|
413
|
+
}
|
|
414
|
+
export async function configApply(ctx, options) {
|
|
415
|
+
const filePath = options.file ?? DEFAULT_CONFIG_FILE;
|
|
416
|
+
const desired = await readDesiredConfig(filePath);
|
|
417
|
+
const target = await deploymentTarget(options, desired);
|
|
418
|
+
const current = await pullConfigForTarget(ctx, target);
|
|
419
|
+
const changes = diffConfigs(current, desired);
|
|
420
|
+
const changedSections = sectionChanges(changes);
|
|
421
|
+
if (options.dryRun) {
|
|
422
|
+
if (ctx.json) {
|
|
423
|
+
printJson({ ok: true, dryRun: true, deployment: target, file: filePath, changedSections, changes });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
printBannerFor(ctx);
|
|
427
|
+
log(ctx, section('Config Apply Dry Run'));
|
|
428
|
+
log(ctx, field('Deployment', `${target.mode ?? 'unknown'} (${target.id})`));
|
|
429
|
+
log(ctx, field('File', filePath));
|
|
430
|
+
log(ctx, '');
|
|
431
|
+
printChanges(ctx, changes);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
assertCanApply(target, options);
|
|
435
|
+
for (const sectionName of changedSections) {
|
|
436
|
+
const payload = desired.settings[sectionName];
|
|
437
|
+
if (payload)
|
|
438
|
+
await applySection(target.id, sectionName, payload);
|
|
439
|
+
}
|
|
440
|
+
if (ctx.json) {
|
|
441
|
+
printJson({ ok: true, dryRun: false, deployment: target, file: filePath, changedSections, changes });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
printBannerFor(ctx);
|
|
445
|
+
log(ctx, section('Config Applied'));
|
|
446
|
+
log(ctx, field('Deployment', `${target.mode ?? 'unknown'} (${target.id})`));
|
|
447
|
+
log(ctx, field('Sections', changedSections.length ? changedSections.join(', ') : 'none'));
|
|
448
|
+
log(ctx, success('Wacht config is up to date.'));
|
|
449
|
+
}
|
|
450
|
+
export function printConfigTemplate(ctx) {
|
|
451
|
+
const template = {
|
|
452
|
+
$schema: CONFIG_SCHEMA_URL,
|
|
453
|
+
version: 1,
|
|
454
|
+
deployment: {
|
|
455
|
+
id: '',
|
|
456
|
+
mode: 'staging',
|
|
457
|
+
},
|
|
458
|
+
settings: {
|
|
459
|
+
display: {
|
|
460
|
+
app_name: 'My App',
|
|
461
|
+
sign_in_page_url: '/sign-in',
|
|
462
|
+
sign_up_page_url: '/sign-up',
|
|
463
|
+
},
|
|
464
|
+
restrictions: {
|
|
465
|
+
sign_up_mode: 'public',
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
if (ctx.json) {
|
|
470
|
+
printJson({ ok: true, template, openApiUrl: PLATFORM_OPENAPI_URL });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
printJson(template);
|
|
474
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const MCP_URL = 'https://wacht.dev/docs/mcp';
|
|
4
|
+
export const SKILLS_SOURCE = 'wacht-platform/bench';
|
|
5
|
+
export const OAUTH_CLIENT_ID = 'oc_SCoNL5oNiIiELWFhknqQsUvQ9FDrfMBC';
|
|
6
|
+
export const OAUTH_AUTHORIZE_URL = 'https://m2ma.wacht.dev/oauth/authorize';
|
|
7
|
+
export const OAUTH_TOKEN_URL = 'https://m2ma.wacht.dev/oauth/token';
|
|
8
|
+
export const OAUTH_REVOCATION_URL = 'https://m2ma.wacht.dev/oauth/revoke';
|
|
9
|
+
export const OAUTH_ISSUER = 'https://m2ma.wacht.dev';
|
|
10
|
+
export const OAUTH_SCOPES = 'read write';
|
|
11
|
+
export const MACHINE_API_URL = 'https://machine.wacht.dev';
|
|
12
|
+
export const PLATFORM_OPENAPI_URL = 'https://wacht.dev/docs/openapi/platform-api.json';
|
|
13
|
+
export const REDIRECT_PORT = 37819;
|
|
14
|
+
export const REDIRECT_URI = `http://127.0.0.1:${REDIRECT_PORT}/callback`;
|
|
15
|
+
export const AUTH_DIR = path.join(os.homedir(), '.wacht');
|
|
16
|
+
export const AUTH_FILE = path.join(AUTH_DIR, 'bench-auth.json');
|
|
17
|
+
export const CONTEXT_FILE = path.join(AUTH_DIR, 'bench-context.json');
|
|
18
|
+
export const OPENAPI_CACHE_FILE = path.join(AUTH_DIR, 'platform-api.openapi.json');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { AUTH_DIR, CONTEXT_FILE } from './config.js';
|
|
3
|
+
function isStoredBenchContext(value) {
|
|
4
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
5
|
+
return false;
|
|
6
|
+
const record = value;
|
|
7
|
+
return typeof record.project_id === 'string'
|
|
8
|
+
&& typeof record.project_name === 'string'
|
|
9
|
+
&& typeof record.deployment_id === 'string'
|
|
10
|
+
&& typeof record.deployment_mode === 'string'
|
|
11
|
+
&& typeof record.updated_at === 'number';
|
|
12
|
+
}
|
|
13
|
+
export async function readBenchContext() {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(await readFile(CONTEXT_FILE, 'utf8'));
|
|
16
|
+
return isStoredBenchContext(parsed) ? parsed : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function writeBenchContext(context) {
|
|
23
|
+
await mkdir(AUTH_DIR, { recursive: true });
|
|
24
|
+
await writeFile(CONTEXT_FILE, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
export async function clearBenchContext() {
|
|
27
|
+
await rm(CONTEXT_FILE, { force: true });
|
|
28
|
+
}
|