create-pracht 0.6.0 → 0.6.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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/skills/add-auth/SKILL.md +63 -143
  3. package/skills/add-capabilities/SKILL.md +409 -0
  4. package/skills/add-content/SKILL.md +242 -0
  5. package/skills/add-db/SKILL.md +93 -202
  6. package/skills/add-i18n/SKILL.md +178 -217
  7. package/skills/add-images/SKILL.md +203 -0
  8. package/skills/add-observability/SKILL.md +118 -15
  9. package/skills/add-openapi/SKILL.md +209 -0
  10. package/skills/audit-a11y/SKILL.md +8 -9
  11. package/skills/audit-agent-surface/SKILL.md +335 -0
  12. package/skills/audit-auth/SKILL.md +16 -11
  13. package/skills/audit-bundles/SKILL.md +56 -12
  14. package/skills/audit-csrf/SKILL.md +9 -10
  15. package/skills/audit-deps/SKILL.md +8 -8
  16. package/skills/audit-headers/SKILL.md +9 -10
  17. package/skills/audit-islands/SKILL.md +9 -10
  18. package/skills/audit-loaders/SKILL.md +23 -8
  19. package/skills/audit-redirects/SKILL.md +9 -10
  20. package/skills/audit-secrets/SKILL.md +6 -6
  21. package/skills/audit-seo/SKILL.md +8 -8
  22. package/skills/audit-shells/SKILL.md +8 -9
  23. package/skills/configure-isg/SKILL.md +9 -10
  24. package/skills/migrate-nextjs/SKILL.md +200 -415
  25. package/skills/pracht-debug/SKILL.md +165 -120
  26. package/skills/pracht-deploy/SKILL.md +248 -329
  27. package/skills/pracht-scaffold/SKILL.md +123 -146
  28. package/skills/pracht-test-api/SKILL.md +10 -10
  29. package/skills/pre-deploy/SKILL.md +166 -195
  30. package/skills/scaffold-e2e/SKILL.md +11 -12
  31. package/skills/scaffold-tests/SKILL.md +10 -12
  32. package/skills/tune-render-mode/SKILL.md +7 -8
  33. package/skills/typed-routes/SKILL.md +15 -11
  34. package/skills/upgrade-pracht/SKILL.md +12 -10
  35. package/src/index.js +43 -0
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: add-images
3
+ version: 1.0.1
4
+ description: |
5
+ Wire `@pracht/image`: the CLS-safe zero-runtime `<Image>`, the loader for your
6
+ target (sharp endpoint, Cloudflare, Vercel, passthrough), `?pracht` build-time
7
+ imports with blur placeholders, prebuilt WebP variants, and the optimization
8
+ endpoint's security settings.
9
+ Use for "add images", "optimize images", "responsive images", "next/image
10
+ equivalent", "blur placeholders", "my images cause layout shift".
11
+ allowed-tools:
12
+ - Bash
13
+ - Read
14
+ - Write
15
+ - Edit
16
+ - Grep
17
+ - Glob
18
+ - AskUserQuestion
19
+ ---
20
+
21
+ # Pracht Add Images
22
+
23
+ `@pracht/image` splits the problem the way next/image does: the `<Image>`
24
+ component decides *which widths* to render, a loader decides *what URL* serves
25
+ each width (`docs/IMAGES.md`). The component renders a plain `<img>` — SSR-safe,
26
+ zero hydration, works with `hydration: "none"`.
27
+
28
+ ## Step 1: Choose the backend for the deployment target
29
+
30
+ MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
31
+ `inspect_routes`/`inspect_build`/`doctor`/`verify` tools over shelling out.
32
+
33
+ ```bash
34
+ pracht inspect build --json # adapterTarget (requires a prior `pracht build`)
35
+ ```
36
+
37
+ | Target | Loader | Notes |
38
+ | ------ | ------ | ----- |
39
+ | Node | `defaultLoader` + the built-in endpoint | needs `sharp`; put a CDN in front |
40
+ | Cloudflare | `cloudflareLoader` | Image Resizing must be enabled on the zone; sharp does not run on Workers |
41
+ | Vercel | `vercelLoader` | needs an `images` section in the project config; Vercel only serves widths listed in `images.sizes` |
42
+ | Static host | `passthroughLoader` | no image service — srcset is omitted |
43
+ | Any target, no runtime service | `?pracht&pracht-static` imports | prebuilt WebP variants, no loader involved |
44
+
45
+ Confirm the choice with `AskUserQuestion` when the app deploys to more than one
46
+ target — a per-target `configureImage()` behind an env flag is the usual answer
47
+ (`examples/basic` does exactly this).
48
+
49
+ ## Step 2: Install
50
+
51
+ ```bash
52
+ pnpm add @pracht/image
53
+ pnpm add sharp # only for the built-in Node endpoint
54
+ pnpm add -D sharp # only for `?pracht` imports / static variants
55
+ ```
56
+
57
+ sharp stays an optional peer dependency; the vite plugin fails with an install
58
+ hint when a `?pracht` import needs it, and the endpoint answers 500 with the
59
+ same hint at runtime.
60
+
61
+ ## Step 3: Configure the loader once
62
+
63
+ Put `configureImage()` where both the server and the browser evaluate it — the
64
+ top of `src/routes.ts` is the canonical spot:
65
+
66
+ ```ts
67
+ import { cloudflareLoader, configureImage } from "@pracht/image";
68
+
69
+ configureImage({
70
+ loader: cloudflareLoader,
71
+ // deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
72
+ // imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
73
+ // quality: 75,
74
+ });
75
+ ```
76
+
77
+ `createDefaultLoader("/my/endpoint")` builds a default-style loader for a custom
78
+ endpoint path. Root-absolute endpoints pick up Vite's deploy `base`
79
+ automatically; provider loaders (`cloudflareLoader`, `vercelLoader`)
80
+ deliberately stay at the origin root.
81
+
82
+ ## Step 4: Render images
83
+
84
+ ```tsx
85
+ import { Image } from "@pracht/image";
86
+
87
+ <Image
88
+ src="/banner.jpg"
89
+ alt="Product banner"
90
+ width={1200}
91
+ height={280}
92
+ sizes="(max-width: 1200px) 100vw, 1200px"
93
+ priority
94
+ />;
95
+ ```
96
+
97
+ - `alt` is required — use `alt=""` for decorative images.
98
+ - `width`/`height` are required unless `fill`; they reserve space and prevent
99
+ layout shift. Missing dimensions log a `console.error` in dev.
100
+ - `sizes` (or `fill`) switches the srcset to `w` descriptors across
101
+ `deviceSizes`; a fixed image gets `1x`/`2x` candidates snapped to the
102
+ breakpoint list so caches stay small.
103
+ - `priority` on above-the-fold images swaps `loading="lazy"` +
104
+ `decoding="async"` for `loading="eager"` + `fetchpriority="high"`. Use it for
105
+ the LCP image only.
106
+ - `fill` positions the image absolutely inside the nearest positioned ancestor
107
+ and defaults `sizes` to `100vw`; pair with `style={{ objectFit: "cover" }}`.
108
+
109
+ ## Step 5: Build-time imports and static variants
110
+
111
+ Register the plugin — the main `pracht()` plugin does **not** include it:
112
+
113
+ ```ts
114
+ // vite.config.ts
115
+ import { prachtImage } from "@pracht/image/vite";
116
+
117
+ export default { plugins: [prachtImage(), pracht({ /* … */ })] };
118
+ ```
119
+
120
+ ```tsx
121
+ import hero from "./hero.jpg?pracht"; // { src, width, height, blurDataURL }
122
+ import banner from "./banner.jpg?pracht&pracht-static"; // + prebuilt WebP variants
123
+
124
+ <Image src={hero} alt="Hero" placeholder="blur" />;
125
+ <Image src={banner} alt="Banner" sizes="(max-width: 960px) 100vw, 960px" />;
126
+ ```
127
+
128
+ - Dimensions come from sharp metadata with EXIF orientation applied, so a
129
+ rotated portrait reports its *display* size.
130
+ - `blurDataURL` is an 8px WebP data URI generated at build time. SVGs skip it,
131
+ animated GIFs blur their first frame.
132
+ - `pracht-static` emits content-hashed WebP files at `staticWidths`, takes
133
+ precedence over the global loader, and is cached in Vite's `cacheDir` keyed by
134
+ source bytes. It requires an **absolute** Vite `base`; SVG and animated
135
+ sources pass through unchanged.
136
+ - Root-relative `publicDir` imports keep stable public URLs and bypass the
137
+ runtime loader.
138
+ - Types: `"types": ["@pracht/image/client"]` in tsconfig, or a triple-slash
139
+ reference in any `.d.ts`.
140
+
141
+ `@pracht/markdown` uses `?pracht&pracht-static` automatically for relative
142
+ Markdown images and renders them with `getImageProps()` — see `/add-content`.
143
+
144
+ ## Step 6: Mount the optimization endpoint (Node-compatible runtimes only)
145
+
146
+ ```ts
147
+ // src/api/_pracht/image.ts
148
+ import { createImageHandler } from "@pracht/image/node";
149
+
150
+ const imageHandler = createImageHandler({
151
+ // Required in every environment, including dev. Your own env var, not a
152
+ // framework one — set it to the app's public origin and use the same value
153
+ // as nodeAdapter({ canonicalOrigin }).
154
+ localOrigin: process.env.PRACHT_ORIGIN,
155
+ // remotePatterns: [{ protocol: "https", hostname: "*.example.com", pathname: "/uploads" }],
156
+ // allowedWidths: [640, 1280], // required when configureImage() customizes the breakpoints
157
+ });
158
+
159
+ export const GET = imageHandler;
160
+ export const HEAD = imageHandler;
161
+ ```
162
+
163
+ That file maps to `/api/_pracht/image`, exactly what `defaultLoader` targets —
164
+ no further configuration. The endpoint negotiates WebP via `Accept` (pass
165
+ `formats: ["image/avif", "image/webp"]` for AVIF), never enlarges beyond the
166
+ source, passes SVG/GIF through, and answers
167
+ `Cache-Control: public, max-age=14400, must-revalidate` with `Vary: Accept`.
168
+
169
+ ## Step 7: Verify
170
+
171
+ ```bash
172
+ pracht dev # set PRACHT_ORIGIN to the exact origin it prints
173
+ pracht build
174
+ pracht verify --json
175
+ ```
176
+
177
+ Check in the browser: the `<img>` carries `srcset`/`sizes`, the LCP image is
178
+ `fetchpriority="high"`, and no layout shift occurs on reload. Then run
179
+ `/audit-bundles` and `/audit-seo` if image work was part of a performance pass.
180
+
181
+ ## Rules
182
+
183
+ 1. Never leave `localOrigin` unset — relative sources are resolved only against
184
+ it, so a forged `Host` cannot turn the endpoint into an open proxy. The
185
+ handler answers 500 until it is configured, including in dev.
186
+ 2. Always allowlist remote hosts with `remotePatterns`; never proxy arbitrary
187
+ URLs.
188
+ 3. When `configureImage()` customizes `deviceSizes`/`imageSizes`, pass the
189
+ matching `allowedWidths` — the endpoint rejects widths outside its allowlist
190
+ by design, and skipping this silently breaks the srcset.
191
+ 4. Do not use `priority` on more than the one above-the-fold image per route.
192
+ 5. `placeholder="blur"` writes an inline `style` attribute: a CSP without
193
+ `style-src-attr 'unsafe-inline'` silently drops it (and `fill` positioning),
194
+ and `img-src` must include `data:`. See `docs/CSP.md`.
195
+ 6. Platform loaders generate URLs that only resolve on the deployed platform —
196
+ fall back to `passthroughLoader` behind `import.meta.env.DEV` for local
197
+ previews.
198
+ 7. Use an immutable one-year `cacheControl` only when every source URL is
199
+ content-addressed.
200
+ 8. Never overwrite an existing `vite.config.ts`, image API route, or
201
+ `configureImage()` call — diff first and confirm with `AskUserQuestion`.
202
+
203
+ $ARGUMENTS
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: add-observability
3
- version: 1.1.0
3
+ version: 1.1.3
4
4
  description: |
5
- Wire error tracking, request tracing, and Web Vitals into a pracht app.
6
- Supports Sentry or OpenTelemetry on the server side (loader/middleware
7
- boundaries, API routes), and client-side Web Vitals reporting via the
8
- `web-vitals` package.
9
- Use when asked to "add observability", "wire Sentry", "set up tracing",
10
- "add OpenTelemetry", "monitor Web Vitals", or "track errors".
5
+ Wire Sentry or OpenTelemetry into pracht server boundaries (loaders, middleware,
6
+ API routes), client-side Web Vitals reporting, and a capability audit sink that
7
+ records every capability dispatch with transport, outcome, latency, and verified
8
+ identity.
9
+ Use for "add observability", "wire Sentry", "set up tracing", "add
10
+ OpenTelemetry", "monitor Web Vitals", "track errors", "log capability calls",
11
+ or "are agents calling my app".
11
12
  allowed-tools:
12
13
  - Bash
13
14
  - Read
@@ -20,17 +21,19 @@ allowed-tools:
20
21
 
21
22
  # Pracht Add Observability
22
23
 
23
- Three layers, each opt-in:
24
+ Four layers, each opt-in:
24
25
 
25
26
  1. **Server error tracking** — capture loader/middleware/API exceptions.
26
27
  2. **Request tracing** — span per request with child spans per loader/db call.
27
28
  3. **Web Vitals (LCP/CLS/INP/FCP/TTFB)** — client-side, posted to a beacon
28
29
  endpoint.
30
+ 4. **Agent traffic** — one structured audit event per capability dispatch.
31
+ Only relevant when the app registers capabilities; skip it otherwise.
29
32
 
30
- If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
31
- (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
32
- `generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
33
- config with the pracht plugin registered.
33
+ MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
34
+ `inspect_agents`/`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/
35
+ `verify`/`generate_*` tools over shelling out. `pracht inspect` needs the pracht
36
+ plugin in the vite config.
34
37
 
35
38
  ## Step 1: Pick the stack
36
39
 
@@ -232,7 +235,98 @@ export async function POST({ request }: ApiRouteArgs) {
232
235
  For Sentry users, Sentry's browser SDK can capture Web Vitals natively —
233
236
  prefer that over a custom beacon if you've gone the Sentry route.
234
237
 
235
- ## Step 6: Sampling and PII
238
+ ## Step 6: Agent traffic (capability apps only)
239
+
240
+ Skip when `pracht inspect agents --json` reports an empty `capabilities` list.
241
+ That only means there are no capability operations to audit; `llms.txt`, MCP,
242
+ or Web Bot Auth may still expose other agent-facing surfaces. When capabilities
243
+ do exist, every dispatch already emits a structured `CapabilityAuditEvent` —
244
+ nothing is instrumented per capability, a sink just has to be registered.
245
+
246
+ ```ts [src/server/audit.ts]
247
+ import { addCapabilityAuditListener } from "@pracht/core/server";
248
+
249
+ const stopAuditLog = addCapabilityAuditListener("audit-log", (event) => {
250
+ console.log(
251
+ JSON.stringify({
252
+ msg: "capability",
253
+ at: new Date().toISOString(),
254
+ capability: event.capability,
255
+ effect: event.effect,
256
+ transport: event.transport, // "http" | "webmcp" | "mcp" | "server"
257
+ via: event.via, // causal transport for nested invokeCapability()
258
+ outcome: event.outcome, // "ok" or the envelope error code
259
+ status: event.status,
260
+ durationMs: Math.round(event.durationMs),
261
+ agent: event.agent?.agentDomain ?? event.agent?.keyId ?? null,
262
+ }),
263
+ );
264
+ });
265
+
266
+ if (import.meta.hot) {
267
+ import.meta.hot.dispose(stopAuditLog);
268
+ }
269
+ ```
270
+
271
+ The OTel version records a counter and a histogram keyed on
272
+ capability/transport/outcome, and backdates a span with
273
+ `startTime: Date.now() - event.durationMs` (the dispatch has already
274
+ finished when the sink runs). The full snippet is on the agent-trust docs page.
275
+
276
+ Import the module from an eagerly loaded server module. The adapter's configured
277
+ `createContextFrom` module is one portable option: add `import "./audit.ts"`
278
+ there so the generated entry registers the sink before request handling. A
279
+ custom server entry can import it directly. Do not rely on an unrelated route,
280
+ API route, middleware, or `src/server/` registry module; those modules are lazy
281
+ and can miss earlier capability calls.
282
+
283
+ Key properties to state when scaffolding this:
284
+
285
+ - Sinks are invoked synchronously, so keep work before the callback returns or
286
+ reaches its first `await` cheap. A returned promise is never awaited, and a
287
+ synchronous throw is swallowed (first failure per sink reported via
288
+ `console.warn`, naming it), so a broken exporter cannot fail the call.
289
+ - **Always pass a stable name** as the first argument. Registering the same
290
+ name again replaces that sink, which is what keeps a module-scope call safe
291
+ under dev HMR: Vite re-executes importers on every save, so an unkeyed
292
+ registration would add a fresh closure per keystroke and deliver every event
293
+ N times. Never compute the name. Register the returned unsubscribe with
294
+ `import.meta.hot.dispose()` too, so removing the module or renaming the sink
295
+ cannot leave the old name active until the dev server restarts.
296
+ - `setCapabilityAuditHook()` is a **single slot** — a second call replaces the
297
+ first. Use `addCapabilityAuditListener()` whenever more than one sink exists;
298
+ it returns an unsubscribe handle that removes only its own registration.
299
+ - Warning suppression is per named registration, so differently named sinks
300
+ still report independently when they reuse one callback.
301
+ - Delivery snapshots the registered sinks before callbacks run. Adding or
302
+ replacing a sink from inside a callback takes effect on the next dispatch,
303
+ so the current event is never delivered twice to one name.
304
+ - On Cloudflare Workers, a batching exporter must flush within the request or
305
+ be handed the execution context by app code
306
+ (`context.executionContext.waitUntil(exporter.flush())`). Pracht does not
307
+ call `ctx.waitUntil()` for a sink.
308
+ - Audit events cover *dispatch* only. A cross-origin 403, an unknown-capability
309
+ 404, and an unknown MCP tool name all return before dispatch and emit
310
+ nothing, so do not build a reconnaissance alert on these events — use the
311
+ HTTP access log for that.
312
+
313
+ In dev the same events are already collected: the **Agents** section of
314
+ `/_pracht` shows recent dispatches with transport, `via`, verified identity,
315
+ outcome, and duration, and `/_pracht.json` exposes all of them under
316
+ `agentTraffic`. The page counts verified identities, MCP, and MCP-caused
317
+ composition as agent-attributed; shows top-level unsigned HTTP, HTTP-caused
318
+ composition, and client-declared WebMCP markers separately as unverified client
319
+ dispatches; and hides only `invokeCapability()` work with no served-request
320
+ provenance behind a first-party toggle, so the panel's visible count can be
321
+ lower than the sink's.
322
+ Counts and empty-state conclusions only cover the retained window when older
323
+ events have been dropped. Use it to confirm the sink sees what the panel sees
324
+ before wiring a paid backend. Adapter-owned dev servers do not register this
325
+ middleware: on Cloudflare `workerd`, `/_pracht` and `/_pracht.json` return 404.
326
+ Validate the sink from its own output there; a missing panel is not a failed
327
+ audit hook.
328
+
329
+ ## Step 7: Sampling and PII
236
330
 
237
331
  - Set `SENTRY_TRACES_SAMPLE_RATE` to a small number (0.05–0.10) in
238
332
  production.
@@ -242,10 +336,14 @@ prefer that over a custom beacon if you've gone the Sentry route.
242
336
  ```
243
337
  - Never send loader return values verbatim — they often contain user data.
244
338
 
245
- ## Step 7: Verify
339
+ ## Step 8: Verify
246
340
 
247
341
  - Trigger a deliberate error in dev and confirm it lands in Sentry/OTel.
248
342
  - Open a route, check the Web Vitals beacon fires (Network tab).
343
+ - If an audit sink was added: call a capability in dev and confirm the event
344
+ reaches both the sink and, when the adapter exposes it, the Agents panel at
345
+ `/_pracht`. On Cloudflare's adapter-owned dev server, validate the sink output
346
+ directly because the panel does not exist.
249
347
  - Confirm `pnpm test` and `pnpm e2e` still pass.
250
348
  - Run `pracht typegen` if any routes were added (the beacon API route does
251
349
  not affect page-route types, but re-run when in doubt).
@@ -265,6 +363,11 @@ prefer that over a custom beacon if you've gone the Sentry route.
265
363
  routes still benefit but the values reflect the post-bootstrap state.
266
364
  4. Sample traces (≤ 10%) in production; full sampling in dev.
267
365
  5. Never send raw cookies, auth headers, or full loader payloads to a
268
- third-party SaaS.
366
+ third-party SaaS. The same applies to audit events: log the capability
367
+ name, effect, transport, outcome, and agent domain — not the capability's
368
+ input, which is application data.
369
+ 6. Keep the synchronous part of an audit sink cheap: no CPU-heavy work or
370
+ synchronous network call. Returned promises are fire-and-forget; the runtime
371
+ does not await them or catch their rejections.
269
372
 
270
373
  $ARGUMENTS
@@ -0,0 +1,209 @@
1
+ ---
2
+ name: add-openapi
3
+ version: 1.0.1
4
+ description: |
5
+ Wire `@pracht/openapi`: generate an OpenAPI 3.1 document from `defineApi()`
6
+ routes, attach response contracts with `defineOpenApi()`, serve an optional
7
+ Scalar or Swagger UI, handle deploy-base and CSP, and gate completeness in CI.
8
+ Use for "add OpenAPI", "generate an API spec", "add Swagger", "publish API
9
+ docs", "document my API routes".
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Write
14
+ - Edit
15
+ - Grep
16
+ - Glob
17
+ - AskUserQuestion
18
+ ---
19
+
20
+ # Pracht Add OpenAPI
21
+
22
+ `@pracht/openapi` is an opt-in companion package (`docs/OPENAPI.md`). Ordinary
23
+ `defineApi()` routes keep their authoring, runtime behavior, and `apiFetch()`
24
+ type inference — the package only reads the resolved server graph and owns its
25
+ own descriptor.
26
+
27
+ ## Step 1: Inventory the API first
28
+
29
+ MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
30
+ `inspect_api`/`inspect_routes`/`doctor`/`verify` tools over shelling out.
31
+
32
+ ```bash
33
+ pracht inspect api --json # endpoint paths, methods, hasDefaultHandler
34
+ ```
35
+
36
+ Note two things before generating anything:
37
+
38
+ - **Default-export handlers** are not expanded — their supported methods cannot
39
+ be inferred honestly. Split them into named method exports if they belong in
40
+ the document.
41
+ - **Catch-all paths** become a single `{path}` parameter and emit a warning
42
+ about slash encoding.
43
+
44
+ Ask the user whether the document should be public, and whether they want a
45
+ reference UI at all (`ui: false` is the default).
46
+
47
+ ## Step 2: Install and register the plugin
48
+
49
+ ```bash
50
+ pnpm add @pracht/openapi
51
+ ```
52
+
53
+ ```ts
54
+ // vite.config.ts — the companion plugin goes after pracht()
55
+ import { prachtOpenApi } from "@pracht/openapi/vite";
56
+ import { pracht } from "@pracht/vite-plugin";
57
+ import { defineConfig } from "vite";
58
+
59
+ export default defineConfig({
60
+ plugins: [
61
+ pracht(),
62
+ prachtOpenApi({
63
+ info: {
64
+ title: "Acme API",
65
+ version: "1.0.0",
66
+ description: "Public HTTP API for Acme.",
67
+ },
68
+ ui: "scalar", // or "swagger", an object, or omit for no UI
69
+ }),
70
+ ],
71
+ });
72
+ ```
73
+
74
+ This reserves `/openapi.json` and (with a UI) `/docs`. Both are live in the
75
+ Vite dev server; `pracht build` writes `dist/client/openapi.json` and
76
+ `dist/client/docs/index.html`, which Node, Cloudflare, Netlify, and Vercel serve
77
+ through their existing static-asset paths. Paths are configurable via
78
+ `documentPath` and `ui.path`; the document path **must** end in `.json` so
79
+ static hosts assign the right media type.
80
+
81
+ Reserved paths shadow app routes: development logs a warning on a collision and
82
+ production generation replaces the colliding public/build file and reports the
83
+ replacement. Grep the manifest and `public/` for `/docs` before enabling the UI
84
+ on an app that already documents itself there.
85
+
86
+ ## Step 3: Attach the metadata that cannot be inferred
87
+
88
+ Paths, methods, path params, and convertible request validators come from the
89
+ routes. Status codes and payload shapes cannot be recovered from an arbitrary
90
+ `Response` or an erased TypeScript type — declare them:
91
+
92
+ ```ts
93
+ import { defineApi, json } from "@pracht/core";
94
+ import { defineOpenApi } from "@pracht/openapi";
95
+
96
+ export const POST = defineOpenApi(
97
+ defineApi({
98
+ body: createItemSchema,
99
+ handler: ({ body }) => json({ id: createItem(body) }, { status: 201 }),
100
+ }),
101
+ {
102
+ operationId: "createItem",
103
+ summary: "Create an item",
104
+ tags: ["items"],
105
+ responses: {
106
+ 201: { description: "Item created", body: itemSchema },
107
+ },
108
+ },
109
+ );
110
+ ```
111
+
112
+ `defineOpenApi()` mutates and returns the same handler — validation, dispatch,
113
+ and client inference stay with pracht. Generation adds the framework-known 400
114
+ and 422 validation responses itself, marks a request body optional when the
115
+ validator accepts the empty-body `undefined`, and emits a valid undocumented
116
+ `default` response plus a scoped warning when the contract is unknown.
117
+
118
+ ## Step 4: Shared document metadata
119
+
120
+ ```ts
121
+ prachtOpenApi({
122
+ info: { title: "Acme API", version: "1.0.0" },
123
+ document: {
124
+ servers: [
125
+ { url: "https://api.example.com", description: "Production" },
126
+ { url: "http://localhost:3000", description: "Local development" },
127
+ ],
128
+ tags: [{ name: "items", description: "Item lifecycle" }],
129
+ components: {
130
+ securitySchemes: {
131
+ bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
132
+ },
133
+ },
134
+ security: [{ bearerAuth: [] }],
135
+ },
136
+ });
137
+ ```
138
+
139
+ A security scheme referenced by an operation's `security` must exist in
140
+ `document.components.securitySchemes`. `document.security` applies globally; set
141
+ `security: []` on an operation to mark it public.
142
+
143
+ **Deploy base:** with Vite `base: "/app/"`, the UI loads its document from
144
+ `/app/openapi.json`, and generation adds `servers: [{ url: "/app" }]` when
145
+ `document.servers` is omitted so "Try it out" reaches base-prefixed routes. An
146
+ explicit `document.servers` always wins, including an empty array.
147
+
148
+ ## Step 5: Make it a CI gate
149
+
150
+ Conversion failures are warnings by default, so one unsupported handler does not
151
+ erase the document. Once every public operation has an explicit response
152
+ contract, tighten it:
153
+
154
+ ```ts
155
+ prachtOpenApi({ info: { title: "Acme API", version: "1.0.0" }, failOnWarnings: true });
156
+ ```
157
+
158
+ Any warning then fails the dev request and `pracht build`. There is no dedicated
159
+ diff/check command yet — deterministic build output plus `failOnWarnings` is how
160
+ drift is caught.
161
+
162
+ ## Step 6: UI provider, CDN, and CSP
163
+
164
+ - `ui: "scalar"` — modern reference with request examples and an API client.
165
+ - `ui: "swagger"` — Swagger UI, with deep links on and the remote validator
166
+ disabled so internal documents are never sent to `validator.swagger.io`.
167
+ - Both shells load pinned browser assets from jsDelivr. Apps with offline
168
+ requirements or a no-third-party-CDN policy should self-host and override
169
+ (`scriptUrl` for both providers, `styleUrl` for Swagger), copying the pinned
170
+ distribution into `public/vendor/`.
171
+ - The shell also contains a small inline bootstrap script. A strict CSP must
172
+ allow that exact script by hash (and the asset origin), or the page must be
173
+ replaced with an app-owned route following the app's nonce policy —
174
+ self-hosting the bundle alone does not fix an inline-script ban. See
175
+ `docs/CSP.md` and `/audit-headers`.
176
+
177
+ ## Step 7: Verify
178
+
179
+ ```bash
180
+ pracht dev # GET /openapi.json and the UI path
181
+ pracht build
182
+ pracht verify --json
183
+ ```
184
+
185
+ Confirm the document parses, every intended operation is present with a real
186
+ response contract, and no `500`-only `default` responses remain for public
187
+ endpoints. Then run `/audit-headers` if a CSP is in place and
188
+ `/audit-agent-surface` if the document is part of a deliberate agent-facing
189
+ surface.
190
+
191
+ ## Rules
192
+
193
+ 1. Treat the document and UI as public unless the hosting layer protects the
194
+ emitted static files. No secrets, internal hostnames, or credentials in
195
+ descriptions or examples.
196
+ 2. Never embed an OAuth client secret in UI configuration — browser-delivered
197
+ configuration is observable by every visitor.
198
+ 3. Keep the UI and JSON on the same origin.
199
+ 4. "Try it out" sends real requests: mutation endpoints must carry the same
200
+ authentication, authorization, CSRF, rate-limit, and confirmation policies as
201
+ any other client.
202
+ 5. Set a sane static cache policy for the document; immutable caching is wrong
203
+ unless the URL is versioned or deployments purge it.
204
+ 6. Do not document a capability HTTP projection here — capability endpoints are
205
+ not included in generation yet (`/add-capabilities` owns that contract).
206
+ 7. Never overwrite an existing `vite.config.ts` or a hand-written
207
+ `public/openapi.json` — diff first and confirm with `AskUserQuestion`.
208
+
209
+ $ARGUMENTS
@@ -1,13 +1,12 @@
1
1
  ---
2
2
  name: audit-a11y
3
- version: 1.1.0
3
+ version: 1.1.1
4
4
  description: |
5
- Per-route accessibility audit for a pracht app. Drives a headless browser
6
- through every route in the manifest, runs axe-core, and reports issues
7
- grouped by severity and route. Catches alt-text gaps, contrast failures,
8
- missing landmarks, focus-order bugs, and form-label problems.
9
- Use when asked to "audit a11y", "check accessibility", "axe my app",
10
- "WCAG compliance", or "screen reader test".
5
+ Per-route axe-core audit of a pracht app in a headless browser: alt text,
6
+ contrast, landmarks, focus order, and form labels, grouped by severity and
7
+ route.
8
+ Use for "audit a11y", "check accessibility", "axe my app", "WCAG compliance",
9
+ "screen reader test".
11
10
  allowed-tools:
12
11
  - Bash
13
12
  - Read
@@ -61,8 +60,8 @@ project tooling.
61
60
 
62
61
  ## Step 3: Enumerate routes
63
62
 
64
- If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
65
- (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
63
+ MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
64
+ `inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
66
65
  shelling out.
67
66
 
68
67
  ```bash