@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/init.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { MACHINE_API_URL, MCP_URL, OAUTH_CLIENT_ID, OAUTH_ISSUER, OAUTH_SCOPES, REDIRECT_URI, SKILLS_SOURCE, PLATFORM_OPENAPI_URL, } from './config.js';
|
|
5
|
+
import { readBenchContext } from './context-store.js';
|
|
6
|
+
import { detectProject } from './project-detect.js';
|
|
7
|
+
import { installSkills } from './skills.js';
|
|
8
|
+
import { command, field, log, printBannerFor, printJson, section, success } from './ui.js';
|
|
9
|
+
const AGENTS_START = '<!-- WACHT BENCH START -->';
|
|
10
|
+
const AGENTS_END = '<!-- WACHT BENCH END -->';
|
|
11
|
+
function hasFlag(args, flag) {
|
|
12
|
+
return args.includes(flag);
|
|
13
|
+
}
|
|
14
|
+
function valueAfter(args, flag) {
|
|
15
|
+
const index = args.indexOf(flag);
|
|
16
|
+
return index === -1 ? undefined : args[index + 1];
|
|
17
|
+
}
|
|
18
|
+
export function parseInitOptions(args) {
|
|
19
|
+
return {
|
|
20
|
+
client: valueAfter(args, '--client') ?? 'cursor',
|
|
21
|
+
installSkills: hasFlag(args, '--install-skills'),
|
|
22
|
+
skipAgents: hasFlag(args, '--skip-agents'),
|
|
23
|
+
skipConfig: hasFlag(args, '--skip-config'),
|
|
24
|
+
skipEnv: hasFlag(args, '--skip-env'),
|
|
25
|
+
skipGuide: hasFlag(args, '--skip-guide'),
|
|
26
|
+
skipTemplates: hasFlag(args, '--skip-templates'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function readOptional(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
return await readFile(filePath, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function agentsBlock(profile) {
|
|
38
|
+
const frameworks = profile.frameworks.length ? profile.frameworks.join(', ') : 'Unknown';
|
|
39
|
+
const skills = profile.suggestedSkills.join(', ');
|
|
40
|
+
return `${AGENTS_START}
|
|
41
|
+
## Wacht Bench
|
|
42
|
+
|
|
43
|
+
This project is configured for AI-assisted Wacht development.
|
|
44
|
+
|
|
45
|
+
**Use the Wacht Bench CLI (\`wacht\`) for anything that touches Wacht state.** Don't write one-off scripts to call the Machine API — run a CLI command. Don't ask the user to click through the console for things the CLI can do.
|
|
46
|
+
|
|
47
|
+
- Detected project shape: \`${frameworks}\`.
|
|
48
|
+
- Suggested Wacht skills for this project: \`${skills}\`.
|
|
49
|
+
- Active skill router: \`wacht\` (always start there). For CLI work specifically, use the \`wacht-bench-cli\` skill.
|
|
50
|
+
- Before coding Wacht behavior, consult Wacht Docs MCP at \`${MCP_URL}\`.
|
|
51
|
+
- Install or update skills with \`npx skills add ${SKILLS_SOURCE}\`.
|
|
52
|
+
|
|
53
|
+
### Default CLI workflow
|
|
54
|
+
|
|
55
|
+
| Need | Command |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| Sign in / check session | \`wacht login\` · \`wacht auth status\` |
|
|
58
|
+
| Switch deployment | \`wacht deployments select\` · \`wacht deployments current\` |
|
|
59
|
+
| Manage users | \`wacht users list\` · \`wacht users get <id>\` · \`wacht users create --field …\` |
|
|
60
|
+
| Manage orgs / workspaces | \`wacht orgs list\` · \`wacht workspaces list --org <id>\` |
|
|
61
|
+
| Pull / diff / apply config | \`wacht config pull\` · \`wacht config diff\` · \`wacht config apply --yes\` |
|
|
62
|
+
| Discover any Machine API operation | \`wacht api ls --search <text>\` · \`wacht api describe <op>\` · \`wacht api call <op>\` |
|
|
63
|
+
|
|
64
|
+
Always pass \`--json\` and \`--no-interactive\` when running commands inside an agent loop. Confirm the active deployment with \`wacht deployments current\` before any deployment-scoped change. Production config applies require \`--production --confirm <deployment_id> --yes\`.
|
|
65
|
+
|
|
66
|
+
${AGENTS_END}`;
|
|
67
|
+
}
|
|
68
|
+
async function upsertAgentsBlock(root, profile) {
|
|
69
|
+
const agentsPath = path.join(root, 'AGENTS.md');
|
|
70
|
+
const existing = await readOptional(agentsPath);
|
|
71
|
+
const nextBlock = agentsBlock(profile);
|
|
72
|
+
if (!existing) {
|
|
73
|
+
await writeFile(agentsPath, `${nextBlock}\n`, 'utf8');
|
|
74
|
+
return agentsPath;
|
|
75
|
+
}
|
|
76
|
+
const start = existing.indexOf(AGENTS_START);
|
|
77
|
+
const end = existing.indexOf(AGENTS_END);
|
|
78
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
79
|
+
const before = existing.slice(0, start).trimEnd();
|
|
80
|
+
const after = existing.slice(end + AGENTS_END.length).trimStart();
|
|
81
|
+
const next = [before, nextBlock, after].filter(Boolean).join('\n\n');
|
|
82
|
+
await writeFile(agentsPath, `${next.trimEnd()}\n`, 'utf8');
|
|
83
|
+
return agentsPath;
|
|
84
|
+
}
|
|
85
|
+
await writeFile(agentsPath, `${existing.trimEnd()}\n\n${nextBlock}\n`, 'utf8');
|
|
86
|
+
return agentsPath;
|
|
87
|
+
}
|
|
88
|
+
async function writeBenchConfig(root, profile, options) {
|
|
89
|
+
const wachtDir = path.join(root, '.wacht');
|
|
90
|
+
await mkdir(wachtDir, { recursive: true });
|
|
91
|
+
const configPath = path.join(wachtDir, 'bench.json');
|
|
92
|
+
const config = {
|
|
93
|
+
version: 1,
|
|
94
|
+
client: options.client,
|
|
95
|
+
skillsSource: SKILLS_SOURCE,
|
|
96
|
+
docsMcpUrl: MCP_URL,
|
|
97
|
+
machineApiUrl: MACHINE_API_URL,
|
|
98
|
+
openApiUrl: PLATFORM_OPENAPI_URL,
|
|
99
|
+
oauth: {
|
|
100
|
+
issuer: OAUTH_ISSUER,
|
|
101
|
+
clientId: OAUTH_CLIENT_ID,
|
|
102
|
+
redirectUri: REDIRECT_URI,
|
|
103
|
+
scopes: OAUTH_SCOPES.split(' '),
|
|
104
|
+
},
|
|
105
|
+
project: {
|
|
106
|
+
packageManager: profile.packageManager,
|
|
107
|
+
frameworks: profile.frameworks,
|
|
108
|
+
suggestedSkills: profile.suggestedSkills,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
112
|
+
return configPath;
|
|
113
|
+
}
|
|
114
|
+
function frameworkChecklist(profile) {
|
|
115
|
+
const frameworks = new Set(profile.frameworks);
|
|
116
|
+
if (frameworks.has('Next.js')) {
|
|
117
|
+
return [
|
|
118
|
+
'Review `.wacht/templates/nextjs/wacht-provider.tsx` as a provider reference only.',
|
|
119
|
+
'Review `.wacht/templates/nextjs/middleware.ts` as a protected-route reference only.',
|
|
120
|
+
'Use `wacht-nextjs-patterns` and Wacht Docs MCP before making app edits.',
|
|
121
|
+
'Let the assistant adapt the template to the existing layout, route groups, and Next.js version.',
|
|
122
|
+
'Keep publishable/frontend values separate from server-only secrets.',
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
if (frameworks.has('React Router')) {
|
|
126
|
+
return [
|
|
127
|
+
'Review `.wacht/templates/react-router/wacht-provider.tsx` as a provider reference only.',
|
|
128
|
+
'Review `.wacht/templates/react-router/protected-loader.ts` as a loader/action reference only.',
|
|
129
|
+
'Use `wacht-react-router-patterns` and Wacht Docs MCP before making app edits.',
|
|
130
|
+
'Let the assistant adapt the template to the existing root route and data APIs.',
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
if (frameworks.has('TanStack Router')) {
|
|
134
|
+
return [
|
|
135
|
+
'Review `.wacht/templates/tanstack-router/wacht-provider.tsx` as a provider reference only.',
|
|
136
|
+
'Review `.wacht/templates/tanstack-router/protected-request.ts` as a request-auth reference only.',
|
|
137
|
+
'Use `wacht-tanstack-router-patterns` and Wacht Docs MCP before making app edits.',
|
|
138
|
+
'Let the assistant adapt the template to the existing router context and route tree.',
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
return [
|
|
142
|
+
'Review `.wacht/templates/wacht-contract.ts` for deployment/env assumptions.',
|
|
143
|
+
'Select an active deployment with `wacht deployments select`.',
|
|
144
|
+
'Use Wacht Docs MCP and the suggested skills before choosing framework-specific app edits.',
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
async function writeEnvTemplate(root) {
|
|
148
|
+
const context = await readBenchContext();
|
|
149
|
+
const envPath = path.join(root, '.env.wacht.example');
|
|
150
|
+
const lines = [
|
|
151
|
+
'# Generated by Wacht Bench. Copy values into your local env file as needed.',
|
|
152
|
+
`WACHT_MACHINE_API_URL=${MACHINE_API_URL}`,
|
|
153
|
+
`WACHT_OPENAPI_URL=${PLATFORM_OPENAPI_URL}`,
|
|
154
|
+
`WACHT_PROJECT_ID=${context?.project_id ?? ''}`,
|
|
155
|
+
`WACHT_DEPLOYMENT_ID=${context?.deployment_id ?? ''}`,
|
|
156
|
+
`WACHT_DEPLOYMENT_MODE=${context?.deployment_mode ?? ''}`,
|
|
157
|
+
`WACHT_BACKEND_HOST=${context?.deployment_backend_host ?? ''}`,
|
|
158
|
+
`WACHT_FRONTEND_HOST=${context?.deployment_frontend_host ?? ''}`,
|
|
159
|
+
'',
|
|
160
|
+
'# Framework SDK keys. Fill the publishable key from your Wacht deployment.',
|
|
161
|
+
'NEXT_PUBLIC_WACHT_PUBLISHABLE_KEY=',
|
|
162
|
+
'VITE_WACHT_PUBLISHABLE_KEY=',
|
|
163
|
+
'WACHT_API_KEY=',
|
|
164
|
+
'',
|
|
165
|
+
];
|
|
166
|
+
await writeFile(envPath, lines.join('\n'), 'utf8');
|
|
167
|
+
return envPath;
|
|
168
|
+
}
|
|
169
|
+
async function writeBootstrapGuide(root, profile) {
|
|
170
|
+
const guidePath = path.join(root, '.wacht', 'BOOTSTRAP.md');
|
|
171
|
+
await mkdir(path.dirname(guidePath), { recursive: true });
|
|
172
|
+
const context = await readBenchContext();
|
|
173
|
+
const checklist = frameworkChecklist(profile).map((item) => `- ${item}`).join('\n');
|
|
174
|
+
const active = context
|
|
175
|
+
? `- Active project: ${context.project_name} (${context.project_id})
|
|
176
|
+
- Active deployment: ${context.deployment_mode} (${context.deployment_id})
|
|
177
|
+
- Backend host: ${context.deployment_backend_host ?? ''}
|
|
178
|
+
- Frontend host: ${context.deployment_frontend_host ?? ''}`
|
|
179
|
+
: '- No active deployment selected. Run `wacht deployments select`.';
|
|
180
|
+
const content = `# Wacht Bootstrap
|
|
181
|
+
|
|
182
|
+
## Project
|
|
183
|
+
|
|
184
|
+
- Detected frameworks: ${profile.frameworks.length ? profile.frameworks.join(', ') : 'unknown'}
|
|
185
|
+
- Package manager: ${profile.packageManager}
|
|
186
|
+
- Suggested skills: ${profile.suggestedSkills.join(', ')}
|
|
187
|
+
|
|
188
|
+
## Active Deployment
|
|
189
|
+
|
|
190
|
+
${active}
|
|
191
|
+
|
|
192
|
+
## Checklist
|
|
193
|
+
|
|
194
|
+
${checklist}
|
|
195
|
+
|
|
196
|
+
## Templates
|
|
197
|
+
|
|
198
|
+
Bench writes framework starter templates under \`.wacht/templates\`. These files are not integrated into the app automatically. Use the suggested Wacht skills and Docs MCP to adapt them to the existing project structure.
|
|
199
|
+
|
|
200
|
+
## Useful Commands
|
|
201
|
+
|
|
202
|
+
\`\`\`bash
|
|
203
|
+
npx skills add ${SKILLS_SOURCE}
|
|
204
|
+
wacht login
|
|
205
|
+
wacht deployments select
|
|
206
|
+
wacht deployments current
|
|
207
|
+
wacht api ls --search users
|
|
208
|
+
wacht api describe createUser
|
|
209
|
+
wacht api call createUser --body '{"email_address":"person@example.com"}'
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
## API Discovery
|
|
213
|
+
|
|
214
|
+
Bench reads the Wacht Platform OpenAPI schema from:
|
|
215
|
+
|
|
216
|
+
\`\`\`text
|
|
217
|
+
${PLATFORM_OPENAPI_URL}
|
|
218
|
+
\`\`\`
|
|
219
|
+
|
|
220
|
+
Schema cache TTL is 24 hours. Refresh it with:
|
|
221
|
+
|
|
222
|
+
\`\`\`bash
|
|
223
|
+
wacht api schema refresh
|
|
224
|
+
\`\`\`
|
|
225
|
+
`;
|
|
226
|
+
await writeFile(guidePath, content, 'utf8');
|
|
227
|
+
return guidePath;
|
|
228
|
+
}
|
|
229
|
+
function nextProviderTemplate() {
|
|
230
|
+
return `'use client';
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Template only. Bench does not import this file or patch your layout.
|
|
234
|
+
* Adapt it with the wacht-nextjs-patterns skill after checking Docs MCP.
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
import type { ReactNode } from 'react';
|
|
238
|
+
import { DeploymentInitialized, DeploymentProvider } from '@wacht/nextjs';
|
|
239
|
+
|
|
240
|
+
export function WachtProvider({ children }: { children: ReactNode }) {
|
|
241
|
+
return (
|
|
242
|
+
<DeploymentProvider publicKey={process.env.NEXT_PUBLIC_WACHT_PUBLISHABLE_KEY!}>
|
|
243
|
+
<DeploymentInitialized>{children}</DeploymentInitialized>
|
|
244
|
+
</DeploymentProvider>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
function nextMiddlewareTemplate() {
|
|
250
|
+
return `/**
|
|
251
|
+
* Template only. Bench does not install this file into the app.
|
|
252
|
+
* For Next.js 16, adapt this into proxy.ts. For older Next.js, adapt into middleware.ts.
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
import { NextResponse } from 'next/server';
|
|
256
|
+
import { createRouteMatcher, wachtMiddleware } from '@wacht/nextjs/server';
|
|
257
|
+
|
|
258
|
+
const isProtected = createRouteMatcher(['/account(.*)', '/dashboard(.*)']);
|
|
259
|
+
|
|
260
|
+
export default wachtMiddleware(
|
|
261
|
+
async (auth, req) => {
|
|
262
|
+
if (!isProtected(req)) return NextResponse.next();
|
|
263
|
+
await auth.protect();
|
|
264
|
+
return NextResponse.next();
|
|
265
|
+
},
|
|
266
|
+
{ apiRoutePrefixes: ['/api', '/trpc'] },
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
export const config = {
|
|
270
|
+
matcher: [
|
|
271
|
+
'/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
|
272
|
+
'/(api|trpc)(.*)',
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
function reactProviderTemplate(adapterImport) {
|
|
278
|
+
return `/**
|
|
279
|
+
* Template only. Bench does not import this file or patch your route root.
|
|
280
|
+
* Adapt it with the framework skill after checking Docs MCP.
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
import type { ReactNode } from 'react';
|
|
284
|
+
import { DeploymentProvider } from '${adapterImport}';
|
|
285
|
+
|
|
286
|
+
export function WachtDeploymentProvider({ children }: { children: ReactNode }) {
|
|
287
|
+
return (
|
|
288
|
+
<DeploymentProvider publicKey={import.meta.env.VITE_WACHT_PUBLISHABLE_KEY}>
|
|
289
|
+
{children}
|
|
290
|
+
</DeploymentProvider>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
295
|
+
function reactRouterMiddlewareTemplate() {
|
|
296
|
+
return `/**
|
|
297
|
+
* Template only. Bench does not attach this loader to any route.
|
|
298
|
+
* Adapt it with the wacht-react-router-patterns skill after checking Docs MCP.
|
|
299
|
+
*/
|
|
300
|
+
|
|
301
|
+
import { redirect, type LoaderFunctionArgs } from 'react-router';
|
|
302
|
+
import { authenticateRequest } from '@wacht/react-router/server';
|
|
303
|
+
|
|
304
|
+
export async function protectedLoader({ request }: LoaderFunctionArgs) {
|
|
305
|
+
const result = await authenticateRequest(request);
|
|
306
|
+
|
|
307
|
+
if (!result.auth.isAuthenticated) {
|
|
308
|
+
throw redirect('/sign-in', { headers: result.headers });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return Response.json({ userId: result.auth.userId }, { headers: result.headers });
|
|
312
|
+
}
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
function tanstackMiddlewareTemplate() {
|
|
316
|
+
return `/**
|
|
317
|
+
* Template only. Bench does not attach this helper to any route.
|
|
318
|
+
* Adapt it with the wacht-tanstack-router-patterns skill after checking Docs MCP.
|
|
319
|
+
*/
|
|
320
|
+
|
|
321
|
+
import { authenticateRequest } from '@wacht/tanstack-router/server';
|
|
322
|
+
|
|
323
|
+
export async function getProtectedUserId(request: Request) {
|
|
324
|
+
const result = await authenticateRequest(request);
|
|
325
|
+
|
|
326
|
+
if (!result.auth.isAuthenticated) {
|
|
327
|
+
throw new Response(null, {
|
|
328
|
+
status: 302,
|
|
329
|
+
headers: {
|
|
330
|
+
...Object.fromEntries(result.headers.entries()),
|
|
331
|
+
Location: '/sign-in',
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return result.auth.userId;
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
function contractWrapperTemplate(profile) {
|
|
341
|
+
const framework = profile.frameworks[0] ?? 'unknown';
|
|
342
|
+
return `/**
|
|
343
|
+
* Wacht contract wrapper template.
|
|
344
|
+
*
|
|
345
|
+
* This file is generated for AI-assisted development. It is not imported
|
|
346
|
+
* anywhere by Bench. Move/adapt it into a server-only module after reading
|
|
347
|
+
* the active Wacht skill and Wacht Docs MCP pages for this framework.
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
export type WachtRuntimeContract = {
|
|
351
|
+
framework: string;
|
|
352
|
+
deploymentId: string;
|
|
353
|
+
deploymentMode: string;
|
|
354
|
+
backendHost: string;
|
|
355
|
+
frontendHost: string;
|
|
356
|
+
openApiUrl: string;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export function readWachtRuntimeContract(env: Record<string, string | undefined> = process.env): WachtRuntimeContract {
|
|
360
|
+
return {
|
|
361
|
+
framework: '${framework}',
|
|
362
|
+
deploymentId: env.WACHT_DEPLOYMENT_ID ?? '',
|
|
363
|
+
deploymentMode: env.WACHT_DEPLOYMENT_MODE ?? '',
|
|
364
|
+
backendHost: env.WACHT_BACKEND_HOST ?? '',
|
|
365
|
+
frontendHost: env.WACHT_FRONTEND_HOST ?? '',
|
|
366
|
+
openApiUrl: env.WACHT_OPENAPI_URL ?? '${PLATFORM_OPENAPI_URL}',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function assertWachtRuntimeContract(contract = readWachtRuntimeContract()): WachtRuntimeContract {
|
|
371
|
+
const missing = Object.entries(contract)
|
|
372
|
+
.filter(([key, value]) => key !== 'framework' && key !== 'openApiUrl' && !value)
|
|
373
|
+
.map(([key]) => key);
|
|
374
|
+
|
|
375
|
+
if (missing.length) {
|
|
376
|
+
throw new Error(\`Missing Wacht runtime config: \${missing.join(', ')}\`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return contract;
|
|
380
|
+
}
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
async function writeTemplate(root, relativePath, content) {
|
|
384
|
+
const filePath = path.join(root, relativePath);
|
|
385
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
386
|
+
await writeFile(filePath, content, 'utf8');
|
|
387
|
+
return filePath;
|
|
388
|
+
}
|
|
389
|
+
async function writeStarterTemplates(root, profile) {
|
|
390
|
+
const frameworks = new Set(profile.frameworks);
|
|
391
|
+
const written = [];
|
|
392
|
+
if (frameworks.has('Next.js')) {
|
|
393
|
+
written.push(await writeTemplate(root, '.wacht/templates/nextjs/wacht-provider.tsx', nextProviderTemplate()));
|
|
394
|
+
written.push(await writeTemplate(root, '.wacht/templates/nextjs/middleware.ts', nextMiddlewareTemplate()));
|
|
395
|
+
}
|
|
396
|
+
else if (frameworks.has('React Router')) {
|
|
397
|
+
written.push(await writeTemplate(root, '.wacht/templates/react-router/wacht-provider.tsx', reactProviderTemplate('@wacht/react-router')));
|
|
398
|
+
written.push(await writeTemplate(root, '.wacht/templates/react-router/protected-loader.ts', reactRouterMiddlewareTemplate()));
|
|
399
|
+
}
|
|
400
|
+
else if (frameworks.has('TanStack Router')) {
|
|
401
|
+
written.push(await writeTemplate(root, '.wacht/templates/tanstack-router/wacht-provider.tsx', reactProviderTemplate('@wacht/tanstack-router')));
|
|
402
|
+
written.push(await writeTemplate(root, '.wacht/templates/tanstack-router/protected-request.ts', tanstackMiddlewareTemplate()));
|
|
403
|
+
}
|
|
404
|
+
written.push(await writeTemplate(root, '.wacht/templates/wacht-contract.ts', contractWrapperTemplate(profile)));
|
|
405
|
+
return written;
|
|
406
|
+
}
|
|
407
|
+
export async function initProject(args, ctx) {
|
|
408
|
+
const options = parseInitOptions(args);
|
|
409
|
+
const root = process.cwd();
|
|
410
|
+
const profile = await detectProject(root);
|
|
411
|
+
const written = [];
|
|
412
|
+
printBannerFor(ctx);
|
|
413
|
+
log(ctx, section('Bootstrap Project'));
|
|
414
|
+
log(ctx, field('Project root', root));
|
|
415
|
+
log(ctx, field('Detected', profile.frameworks.length ? profile.frameworks.join(', ') : 'unknown project shape'));
|
|
416
|
+
log(ctx, field('Suggested skills', profile.suggestedSkills.join(', ')));
|
|
417
|
+
log(ctx, '');
|
|
418
|
+
if (!options.skipConfig) {
|
|
419
|
+
written.push(await writeBenchConfig(root, profile, options));
|
|
420
|
+
}
|
|
421
|
+
if (!options.skipEnv) {
|
|
422
|
+
written.push(await writeEnvTemplate(root));
|
|
423
|
+
}
|
|
424
|
+
if (!options.skipGuide) {
|
|
425
|
+
written.push(await writeBootstrapGuide(root, profile));
|
|
426
|
+
}
|
|
427
|
+
if (!options.skipTemplates) {
|
|
428
|
+
written.push(...await writeStarterTemplates(root, profile));
|
|
429
|
+
}
|
|
430
|
+
if (!options.skipAgents) {
|
|
431
|
+
written.push(await upsertAgentsBlock(root, profile));
|
|
432
|
+
}
|
|
433
|
+
for (const filePath of written) {
|
|
434
|
+
log(ctx, field('Updated', path.relative(root, filePath)));
|
|
435
|
+
}
|
|
436
|
+
if (options.installSkills) {
|
|
437
|
+
log(ctx, '');
|
|
438
|
+
log(ctx, section('Install Skills'));
|
|
439
|
+
await installSkills();
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
log(ctx, '');
|
|
443
|
+
log(ctx, field('Skills', `run ${command(`npx skills add ${SKILLS_SOURCE}`)} when you want to install/update the pack`));
|
|
444
|
+
}
|
|
445
|
+
if (ctx.json) {
|
|
446
|
+
printJson({
|
|
447
|
+
ok: true,
|
|
448
|
+
root,
|
|
449
|
+
written: written.map((filePath) => path.relative(root, filePath)),
|
|
450
|
+
project: {
|
|
451
|
+
packageManager: profile.packageManager,
|
|
452
|
+
frameworks: profile.frameworks,
|
|
453
|
+
suggestedSkills: profile.suggestedSkills,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
log(ctx, '');
|
|
459
|
+
log(ctx, success('Wacht Bench project bootstrap complete.'));
|
|
460
|
+
}
|
|
461
|
+
// ─── Starter mode ───────────────────────────────────────────────────
|
|
462
|
+
const STARTERS = {
|
|
463
|
+
nextjs: {
|
|
464
|
+
repo: 'https://github.com/wacht-platform/starter-nextjs.git',
|
|
465
|
+
description: 'Next.js 16 App Router with Wacht middleware + provider',
|
|
466
|
+
},
|
|
467
|
+
'react-router': {
|
|
468
|
+
repo: 'https://github.com/wacht-platform/starter-react-router.git',
|
|
469
|
+
description: 'React Router with Wacht provider + loader auth',
|
|
470
|
+
},
|
|
471
|
+
tanstack: {
|
|
472
|
+
repo: 'https://github.com/wacht-platform/starter-tanstack.git',
|
|
473
|
+
description: 'TanStack Router with Wacht provider + beforeLoad auth',
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
async function pathExists(filePath) {
|
|
477
|
+
try {
|
|
478
|
+
await stat(filePath);
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function runGit(args) {
|
|
486
|
+
return new Promise((resolve, reject) => {
|
|
487
|
+
const child = spawn('git', args, { stdio: 'inherit' });
|
|
488
|
+
child.on('error', (error) => reject(new Error(`git error: ${error.message}`)));
|
|
489
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`git exited with code ${code}`))));
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
export async function initStarter(options, ctx) {
|
|
493
|
+
const framework = (options.framework ?? '').toLowerCase();
|
|
494
|
+
if (!framework || !STARTERS[framework]) {
|
|
495
|
+
const supported = Object.keys(STARTERS).join(', ');
|
|
496
|
+
throw new Error(`Pass --starter <framework>. Supported: ${supported}.`);
|
|
497
|
+
}
|
|
498
|
+
const starter = STARTERS[framework];
|
|
499
|
+
const target = options.target ?? `wacht-${framework}-starter`;
|
|
500
|
+
const absoluteTarget = path.resolve(process.cwd(), target);
|
|
501
|
+
if (await pathExists(absoluteTarget)) {
|
|
502
|
+
throw new Error(`Target directory already exists: ${path.relative(process.cwd(), absoluteTarget) || '.'}`);
|
|
503
|
+
}
|
|
504
|
+
printBannerFor(ctx);
|
|
505
|
+
log(ctx, section('Wacht Starter'));
|
|
506
|
+
log(ctx, field('Framework', framework));
|
|
507
|
+
log(ctx, field('Source', starter.repo));
|
|
508
|
+
log(ctx, field('Target', path.relative(process.cwd(), absoluteTarget) || '.'));
|
|
509
|
+
log(ctx, field('Notes', starter.description));
|
|
510
|
+
log(ctx, '');
|
|
511
|
+
await runGit(['clone', '--depth', '1', starter.repo, absoluteTarget]);
|
|
512
|
+
// Run normal init in the new directory so AGENTS.md / .wacht/ are present.
|
|
513
|
+
const previousCwd = process.cwd();
|
|
514
|
+
try {
|
|
515
|
+
process.chdir(absoluteTarget);
|
|
516
|
+
await initProject([
|
|
517
|
+
'--client',
|
|
518
|
+
options.client,
|
|
519
|
+
...(options.install ? ['--install-skills'] : []),
|
|
520
|
+
], ctx);
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
process.chdir(previousCwd);
|
|
524
|
+
}
|
|
525
|
+
if (ctx.json) {
|
|
526
|
+
printJson({ ok: true, framework, target: absoluteTarget, source: starter.repo });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
log(ctx, '');
|
|
530
|
+
log(ctx, success(`Starter ready at ${path.relative(process.cwd(), absoluteTarget) || '.'}`));
|
|
531
|
+
log(ctx, `Next: ${command(`cd ${path.relative(process.cwd(), absoluteTarget) || '.'} && pnpm install && pnpm dev`)}`);
|
|
532
|
+
}
|
|
533
|
+
export function listStarters() {
|
|
534
|
+
return Object.entries(STARTERS).map(([framework, info]) => ({ framework, description: info.description, repo: info.repo }));
|
|
535
|
+
}
|