@jay-framework/jay-stack-cli 0.24.1 → 0.24.3

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.
@@ -22,12 +22,17 @@ The validator processes each `.jay-html` file with:
22
22
 
23
23
  So a warning about an `<img>` might come from a headfull component's template, not your page template. A CSS warning might come from a linked stylesheet.
24
24
 
25
+ ## Errors vs Warnings
26
+
27
+ - **Errors** block the build. They must be fixed — there is no way to suppress them.
28
+ - **Warnings** must be either fixed or explicitly suppressed. Do not ignore warnings — each one has a clear resolution path (add the missing attribute, or suppress via `<script type="application/jay-validations">`).
29
+
25
30
  ## How to Read Warnings
26
31
 
27
32
  Each warning has:
28
33
 
29
34
  - **Message** — what was found and why it matters
30
- - **Suggestion** — how to fix it
35
+ - **Suggestion** — how to fix it, and how to suppress it if the warning is intentional
31
36
  - **Element** — which HTML element triggered it (some include the full tag with attributes)
32
37
 
33
38
  ### Acting on Warnings
@@ -74,6 +79,32 @@ For design-system token warnings, add `/* design-system: allow */` as a comment
74
79
  padding: 96px 0; /* design-system: allow */
75
80
  ```
76
81
 
82
+ ### Page-level validation overrides
83
+
84
+ For page-level warnings that can't be fixed by adding an attribute (e.g., "no LCP image" on a text-first page), use a `<script type="application/jay-validations">` tag in the `<head>`:
85
+
86
+ ```html
87
+ <head>
88
+ <script type="application/jay-validations">
89
+ seo:
90
+ no-lcp-image: true
91
+ </script>
92
+ </head>
93
+ ```
94
+
95
+ The YAML body is keyed by plugin name. Each plugin defines its own suppressible rules. This tag is a build-time directive — it's parsed during validation and never rendered in the page output.
96
+
97
+ Multiple plugins can be configured in one tag:
98
+
99
+ ```html
100
+ <script type="application/jay-validations">
101
+ seo:
102
+ no-lcp-image: true
103
+ design-system:
104
+ allow-undefined-vars: true
105
+ </script>
106
+ ```
107
+
77
108
  ### When you can't suppress
78
109
 
79
110
  If a warning comes from dynamic content (`{post.content}`) or a generated file, you can't suppress it in the template. This is a validator limitation — the warning is a false positive. Don't loop trying to fix it.
@@ -96,15 +96,15 @@ These APIs are also exposed via the editor protocol (Socket.IO) for design board
96
96
  // Server emits: { type: 'routeParamsBatch', route: '...', params: [], hasMore: false }
97
97
  ```
98
98
 
99
- ### Freeze Changed Event
99
+ ### Frozen Page Refresh (Dev)
100
100
 
101
- The `freezeChanged` socket event is emitted when jay-html or CSS files change. Design board applications should listen for this to refresh their frozen views:
101
+ In development, **full-page** frozen views (`?_jay_freeze=<id>` in iframe or tab) self-reload when jay-html or CSS changes — same `jay:page-reload` Hot Module Replacement (HMR) path as live pages. No host application wiring required. Saved ViewState is preserved across reload.
102
102
 
103
- ```typescript
104
- socket.on('freezeChanged', () => {
105
- // Re-fetch frozen page fragments
106
- });
107
- ```
103
+ Fragment format (`format=fragment` for shadow DOM embedders) does **not** auto-reload; those hosts must re-fetch the fragment.
104
+
105
+ ### Freeze Changed Event (Fragment Hosts)
106
+
107
+ The `freezeChanged` socket event was described for design board fragment refresh but is **not** emitted by the current dev server. Fragment embedders should re-fetch on file changes themselves, or wait for a future fetch-and-swap protocol. Dev full-page frozen views do not need this event.
108
108
 
109
109
  ## Iframe / Embed Mode
110
110
 
@@ -87,7 +87,7 @@ The script body is YAML. Values are passed to the component as props alongside r
87
87
  </script>
88
88
  ```
89
89
 
90
- > **Note:** `<script type="application/jay-params">` is deprecated. Move param values into the headless component's script tag body.
90
+ > **Note:** `<script type="application/jay-params">` is no longer supported — it is silently ignored by the route scanner and reported as an error by `jay-stack validate`. Move param values into the headless component's script tag body.
91
91
 
92
92
  ## Page Files
93
93
 
@@ -2,6 +2,16 @@
2
2
 
3
3
  Actions provide RPC-style server endpoints for client-to-server communication.
4
4
 
5
+ > **Actions are compiler-free and live on the `.` entry.** They run in production, so their handlers
6
+ > must not import the compiler (`@jay-framework/compiler-*`). A handler that needs the compiler is
7
+ > either a [CLI command](commands-guide.md) or a **`devOnly` action** (see below) — not a regular
8
+ > action. `validate-plugin`'s leak scan fails a compiler import in `dist/index.js`.
9
+ >
10
+ > **`devOnly` actions** (`actions[].devOnly: true`) — browser-callable handlers for settings/admin
11
+ > pages that may use the compiler and are **excluded from production**. Their handlers live in the
12
+ > `./tools` entry (`lib/tools.ts`), and the dev server registers them normally. See the settings-page
13
+ > pattern in [plugin-routes.md](plugin-routes.md).
14
+
5
15
  ## makeJayAction — Mutations (POST)
6
16
 
7
17
  ```typescript
@@ -75,6 +75,21 @@ commands:
75
75
  command: commands/upload-public.jay-command
76
76
  ```
77
77
 
78
+ ### 4. Export from `./tools`
79
+
80
+ Commands are the **tools** primitive — they load only from the `./tools` entry (`lib/tools.ts`), which
81
+ may use the compiler. Re-export each command handler there, and add the `./tools` export to
82
+ `package.json`:
83
+
84
+ ```typescript
85
+ // lib/tools.ts (./tools) — toolchain-only, compiler allowed
86
+ export { uploadPublic } from './commands/upload-public.js';
87
+ ```
88
+
89
+ Do **not** re-export commands from `lib/index.ts` — the serve entry must stay compiler-free. (An
90
+ operation that must be callable from a browser page is an [action](actions-guide.md), not a command;
91
+ if it also needs the compiler, mark the action `devOnly`.)
92
+
78
93
  ## `CONSOLE_CONTEXT` Service
79
94
 
80
95
  A framework-provided service with project info and a logger:
@@ -4,6 +4,13 @@ For the full contract syntax, decision tree, and examples, see the shared [Contr
4
4
 
5
5
  This file covers plugin-specific contract concerns. Contracts (`.jay-contract` files) are the source of truth for a component's data shape. Define the contract before implementing the component.
6
6
 
7
+ > **Interactive components need a `./client` export.** If a component built from this contract declares
8
+ > an interactive phase (`.withInteractive(...)`) — or the plugin declares `contexts` — the package
9
+ > must expose a `./client` export (`./dist/index.client.js`) for browser hydration. `validate-plugin`
10
+ > detects interactivity by scanning the built server bundle and errors if `./client` is missing.
11
+ > Server-only (slow/fast) component plugins need no `./client`. See
12
+ > [plugin-structure.md](plugin-structure.md).
13
+
7
14
  ## Basic Structure
8
15
 
9
16
  ```yaml
@@ -107,15 +107,15 @@ These APIs are also exposed via the editor protocol (Socket.IO) for design board
107
107
  // Server emits: { type: 'routeParamsBatch', route: '...', params: [], hasMore: false }
108
108
  ```
109
109
 
110
- ### Freeze Changed Event
110
+ ### Frozen Page Refresh (Dev)
111
111
 
112
- The `freezeChanged` socket event is emitted when jay-html or CSS files change. Design board applications should listen for this to refresh their frozen views:
112
+ In development, **full-page** frozen views (`?_jay_freeze=<id>` in iframe or tab) self-reload when jay-html or CSS changes — same `jay:page-reload` Hot Module Replacement (HMR) path as live pages. No host application wiring required. Saved ViewState is preserved across reload.
113
113
 
114
- ```typescript
115
- socket.on('freezeChanged', () => {
116
- // Re-fetch frozen page fragments
117
- });
118
- ```
114
+ Fragment format (`format=fragment` for shadow DOM embedders) does **not** auto-reload; those hosts must re-fetch the fragment.
115
+
116
+ ### Freeze Changed Event (Fragment Hosts)
117
+
118
+ The `freezeChanged` socket event was described for design board fragment refresh but is **not** emitted by the current dev server. Fragment embedders should re-fetch on file changes themselves, or wait for a future fetch-and-swap protocol. Dev full-page frozen views do not need this event.
119
119
 
120
120
  ## Iframe / Embed Mode
121
121
 
@@ -149,7 +149,7 @@ There is no enforced convention — just pick a prefix that's unique and descrip
149
149
 
150
150
  ## Dev-only routes
151
151
 
152
- Some plugin pages are **dev-server tooling** — internal dashboards, QA fixtures, builder settings UIs. Mark them with `devOnly: true` so consumers of `listRoutes()` can distinguish them from public site pages. Production builds do not yet exclude `devOnly` routes that is planned for a future framework release.
152
+ Some plugin pages are **dev-server tooling** — internal dashboards, QA fixtures, builder settings UIs. Mark them with `devOnly: true` so they are served by the dev server, distinguishable via `listRoutes()`, and **excluded from production builds** (not compiled, not in the route manifest).
153
153
 
154
154
  ```yaml
155
155
  routes:
@@ -168,7 +168,48 @@ routes:
168
168
  | `listRoutes()` / `RouteInfo` | Includes route with `devOnly: true` |
169
169
  | Page navigation UIs | **Consumer choice** — tools may filter `devOnly` routes from pickers |
170
170
  | Routes loaded by explicit path | **Unaffected** — embed/host tools pass a known route URL |
171
- | Production build | **Deferred** — future task excludes dev-only routes |
171
+ | Production build | **Excluded** — the route is not compiled or bundled |
172
+ | Component entry (compiler) | Resolved from **`./tools`** when the page uses the compiler |
173
+
174
+ ## Settings pages (devOnly route + devOnly actions)
175
+
176
+ A common pattern: a `devOnly` route whose interactive form calls plugin server handlers to run
177
+ analysis, rebuild a catalog, etc. Because a settings page runs in the browser it invokes handlers via
178
+ the **action RPC** — so these handlers must be **actions**, not CLI commands. When the handler uses the
179
+ compiler (e.g. it parses jay-html), mark the action **`devOnly: true`**: its handler then lives in
180
+ `./tools` (compiler allowed) and is excluded from production alongside the route.
181
+
182
+ ```yaml
183
+ routes:
184
+ - path: /my-plugin/settings
185
+ jayHtml: ./dist/pages/settings/page.jay-html
186
+ component: mySettingsPage
187
+ devOnly: true
188
+ actions:
189
+ - name: runAnalysis
190
+ action: run-analysis.jay-action
191
+ devOnly: true # handler in ./tools, may use the compiler, excluded from production
192
+ - name: fontFallback
193
+ action: font-fallback.jay-action # normal production action — compiler-free, stays on `.`
194
+ ```
195
+
196
+ ```typescript
197
+ // lib/tools.ts (./tools) — compiler allowed, never in the production serve bundle
198
+ export { runAnalysis } from './actions/run-analysis.js'; // uses the compiler
199
+ export { mySettingsPage } from './pages/settings/page.js';
200
+
201
+ // lib/index.ts (.) — compiler-free serve entry
202
+ export { fontFallback } from './actions/font-fallback.js';
203
+ ```
204
+
205
+ - The dev server registers **all** actions (devOnly from `./tools`, regular from `.`) and serves the
206
+ route end-to-end with the compiler present.
207
+ - Production build **excludes** the `devOnly` route (not compiled) and **skips** `devOnly` actions
208
+ (not registered/dispatchable). A compiler-using action left **without** `devOnly` would leak the
209
+ compiler into `dist/index.js` and fail the leak scan — that is the signal to mark it `devOnly` (or
210
+ reclassify it as a command).
211
+ - `devOnly` is orthogonal to compiler use: a compiler-free admin action can still be `devOnly` purely
212
+ to keep it out of production.
172
213
 
173
214
  ### Standalone access
174
215
 
@@ -2,6 +2,53 @@
2
2
 
3
3
  A plugin provides headless components, contracts, and actions. It can be a standalone npm package or inline within a project.
4
4
 
5
+ ## Capabilities & the runtime/tools split
6
+
7
+ A plugin package runs code in two very different phases, and they must **not** share one module graph:
8
+
9
+ - **Serve-time (runtime)** — headless components, production routes, **server actions**, the client
10
+ bundle, global `init`. Loaded on the production request path. **Must stay compiler-free** — the
11
+ serve bundle ships to production.
12
+ - **Tools-time** — validators, **CLI commands**, agent-kit generators, `setup` handlers, and
13
+ `devOnly` route components/actions. Run only under the Jay toolchain (`jay-stack
14
+ validate`/`agent-kit`/`setup`/`run`, dev server). These **may** use the compiler.
15
+
16
+ **Actions vs commands** — the load-bearing distinction:
17
+
18
+ - **Actions** are the _serving_ primitive: request-time handlers that run in production. They live on
19
+ the `.` entry and **must be compiler-free**.
20
+ - **CLI commands** are the _tools_ primitive: invoked under the toolchain. They live on `./tools` and
21
+ **may use the compiler**.
22
+
23
+ Rule of thumb: **if a handler needs the compiler, it is a command (or a `devOnly` action), not a
24
+ regular action.**
25
+
26
+ ### Capability → required export
27
+
28
+ Derive a plugin's required `package.json` exports from the capabilities it declares:
29
+
30
+ | Capability (plugin.yaml) | Needs `./client`? | Handler/export loaded from |
31
+ | --------------------------- | -------------------------- | ------------------------------- |
32
+ | `contracts` | only if interactive phase | `.` (+ `./<contract>` per item) |
33
+ | `dynamic_contracts` | only if interactive phase | `.` |
34
+ | `routes` | only if interactive phase | `.` (or `./tools` if `devOnly`) |
35
+ | `contexts` | ✅ always (client by def.) | `.`, `./client` |
36
+ | `actions` | ❌ | `.` (or `./tools` if `devOnly`) |
37
+ | `services`, `init` / global | ❌ | `.` |
38
+ | `validators` | ❌ | **`./tools`** |
39
+ | `commands` | ❌ | **`./tools`** |
40
+ | `agentkit` / `setup` | ❌ | **`./tools`** |
41
+
42
+ Rules `jay-stack validate-plugin` enforces:
43
+
44
+ - **≥1 capability.** A plugin declaring none is flagged (a `global: true` plugin counts iff it exports
45
+ a resolvable `init`/`setup` handler).
46
+ - **`./tools` required** iff any tools capability (`validators`, `commands`, `agentkit`, `setup`) or a
47
+ `devOnly` action is declared.
48
+ - **`./client` required** iff a provided component has an interactive phase, or `contexts` is declared.
49
+ Server-only and tools-only plugins need no `./client`.
50
+ - **Leak scan:** `dist/index.js` (the `.` entry) must contain no `@jay-framework/compiler-` import.
51
+
5
52
  ## plugin.yaml
6
53
 
7
54
  The plugin manifest declares all contracts, actions, services, contexts, and configuration:
@@ -125,6 +172,10 @@ tags:
125
172
 
126
173
  - `name` — Action name (used with `jay-stack action <plugin>/<action>`)
127
174
  - `action` — Path to `.jay-action` metadata file
175
+ - `devOnly` — (optional, boolean) When `true`, the action's handler lives in `./tools` (compiler
176
+ allowed), is served by the dev server, and is **excluded from production builds**. Use for
177
+ settings-page / admin handlers. A non-`devOnly` action's handler must be compiler-free on `.`. See
178
+ [plugin-routes.md](plugin-routes.md) for the settings-page pattern.
128
179
 
129
180
  ### Webhook Entry Fields
130
181
 
@@ -177,6 +228,9 @@ services:
177
228
  - `css` — (optional) Path to the page's CSS file
178
229
  - `component` — Path to the page component (relative to plugin root, or exported member name for NPM)
179
230
  - `description` — What this page does
231
+ - `devOnly` — (optional, boolean) When `true`, the route is dev-server tooling (e.g. a settings UI):
232
+ served by the dev server, **excluded from production builds**. A `devOnly` route whose page
233
+ component uses the compiler resolves its component from `./tools`.
180
234
 
181
235
  Plugin routes are served by the dev server alongside project routes. If a project defines the same route path, the project's page takes precedence.
182
236
 
@@ -193,7 +247,7 @@ Commands are CLI operations run via `jay-stack run`. Use `makeCliCommand()` to c
193
247
  - `handler` — Export name (NPM plugins) or relative path (local plugins) to the validator function
194
248
  - `description` — (optional) What this validator checks
195
249
 
196
- **NPM plugins:** `handler` is the export name from the package entry point (e.g., `validateMediaOptimization`). The function must be exported from `lib/index.ts`.
250
+ **NPM plugins:** `handler` is the export name from the **`./tools`** entry (e.g., `validateMediaOptimization`). The function must be exported from `lib/tools.ts` — **never re-export a validator (or any compiler-using handler) from `lib/index.ts`**, or the compiler leaks into the serve bundle.
197
251
  **Local plugins:** `handler` is a relative path to the module (e.g., `./validators/media-validator`). The module must export a `validate` function.
198
252
 
199
253
  Validators run during `jay-stack validate` against every parsed jay-html file in the project. See [validation.md](validation.md) for implementation details.
@@ -204,7 +258,7 @@ Validators run during `jay-stack validate` against every parsed jay-html file in
204
258
  - `agentkit` — Export name (NPM) or relative path (local) for `jay-stack agent-kit`. Generates discovery data: add-menu catalogs, reference files, skills, thumbnails.
205
259
  - `description` — (optional, top-level) Human-readable description of what setup validates
206
260
 
207
- **NPM plugins:** `setup` and `agentkit` are export names from the package entry point.
261
+ **NPM plugins:** `setup` and `agentkit` are export names from the **`./tools`** entry (`lib/tools.ts`) — they are tools-time handlers and may use the compiler.
208
262
  **Local plugins:** relative paths to the handler modules.
209
263
 
210
264
  `jay-stack validate-plugin` checks that declared handlers exist and are correctly exported.
@@ -263,21 +317,29 @@ my-project/
263
317
 
264
318
  See `examples/jay-stack/fake-shop` for a working example.
265
319
 
266
- ## Dual Entry Points
320
+ ## Entry Points
267
321
 
268
- Jay plugins are fullstack — they run on both server and client. The build produces two bundles:
322
+ Jay plugins run in three contexts. The build produces up to three bundles:
269
323
 
270
- - **Server** (`dist/index.js`) — actions, services, SSR rendering, `init()`. Built with `vite build --ssr`.
271
- - **Client** (`dist/index.client.js`) components for hydration, context tokens, `init()`. Built with `vite build`.
324
+ - **Server / serve** (`dist/index.js`, `.`) — actions, services, SSR rendering, `init()`. Loaded on
325
+ the production request path. **Compiler-free.** Built with `vite build --ssr`.
326
+ - **Client** (`dist/index.client.js`, `./client`) — components for hydration, context tokens,
327
+ `init()`. Built with `vite build`.
328
+ - **Tools** (`dist/tools.js`, `./tools`) — validators, commands, agent-kit/setup handlers, and any
329
+ `devOnly` route component/action. **Compiler-allowed** (toolchain-only, never in a serve bundle).
330
+ Built alongside the server bundle (`vite build --ssr`).
272
331
 
273
- Create two entry files:
332
+ Create the entry files:
274
333
 
275
- | File | Exports |
276
- | --------------------- | ---------------------------------------------------------- |
277
- | `lib/index.ts` | Actions, services, components (SSR), init, service markers |
278
- | `lib/index.client.ts` | Components (hydration), context markers, init |
334
+ | File | Exports |
335
+ | --------------------- | ------------------------------------------------------------------------------------------------------ |
336
+ | `lib/index.ts` | Actions, services, components (SSR), init, service markers — **compiler-free** |
337
+ | `lib/index.client.ts` | Components (hydration), context markers, init |
338
+ | `lib/tools.ts` | Validators, commands, agent-kit/setup handlers, `devOnly` route components + actions — **compiler OK** |
279
339
 
280
- Actions and service providers are server-only. Components appear in **both** entries.
340
+ Actions and service providers are server-only. Components appear in **both** `index.ts` and
341
+ `index.client.ts`. **`index.ts` must never import `tools.ts`** — that is what keeps the compiler out of
342
+ the serve bundle. A validator-only or tools-only plugin may have a near-empty `index.ts`.
281
343
 
282
344
  ## Build Scripts
283
345
 
@@ -289,7 +351,7 @@ Actions and service providers are server-only. Components appear in **both** ent
289
351
  "build:client": "vite build",
290
352
  "build:server": "vite build --ssr",
291
353
  "build:copy-assets": "cp lib/*.jay-contract* dist/",
292
- "build:types": "tsup lib/index.ts lib/index.client.ts --dts-only --format esm",
354
+ "build:types": "tsup lib/index.ts lib/index.client.ts lib/tools.ts --dts-only --format esm",
293
355
  "validate": "jay-stack-cli validate-plugin",
294
356
  "clean": "rimraf dist"
295
357
  }
@@ -313,7 +375,12 @@ export default defineConfig(({ isSsrBuild }) => ({
313
375
  emptyOutDir: false,
314
376
  lib: {
315
377
  entry: isSsrBuild
316
- ? { index: resolve(__dirname, 'lib/index.ts') }
378
+ ? {
379
+ index: resolve(__dirname, 'lib/index.ts'),
380
+ // Tools entry (compiler-allowed, toolchain-only). Omit if the plugin has no
381
+ // validators/commands/agentkit/setup/devOnly surfaces.
382
+ tools: resolve(__dirname, 'lib/tools.ts'),
383
+ }
317
384
  : { 'index.client': resolve(__dirname, 'lib/index.client.ts') },
318
385
  formats: ['es'],
319
386
  },
@@ -325,6 +392,9 @@ export default defineConfig(({ isSsrBuild }) => ({
325
392
  '@jay-framework/stack-server-runtime',
326
393
  '@jay-framework/reactive',
327
394
  '@jay-framework/runtime',
395
+ // Externalize the compiler namespace: any leak into `.` then shows up as a literal
396
+ // import string in dist/index.js, which validate-plugin's leak scan catches.
397
+ /^@jay-framework\/compiler-/,
328
398
  ],
329
399
  },
330
400
  },
@@ -349,6 +419,10 @@ For NPM packages, declare exports for both server and client entry points:
349
419
  "types": "./dist/index.client.d.ts",
350
420
  "default": "./dist/index.client.js"
351
421
  },
422
+ "./tools": {
423
+ "types": "./dist/tools.d.ts",
424
+ "default": "./dist/tools.js"
425
+ },
352
426
  "./plugin.yaml": "./plugin.yaml",
353
427
  "./my-contract.jay-contract": "./dist/my-contract.jay-contract"
354
428
  },
@@ -356,7 +430,19 @@ For NPM packages, declare exports for both server and client entry points:
356
430
  }
357
431
  ```
358
432
 
359
- The `./client` export is required — the framework uses it for browser-side hydration code. The `.` export handles server-side rendering and action execution.
433
+ - The `.` export handles server-side rendering and action execution — it must be **compiler-free**.
434
+ - The `./client` export is required **only** when a component has an interactive phase or the plugin
435
+ declares `contexts` (browser-side hydration / client contexts).
436
+ - The `./tools` export is required **only** when the plugin declares a tools capability (`validators`,
437
+ `commands`, `agentkit`, `setup`) or a `devOnly` action — those handlers load exclusively from
438
+ `./tools`.
439
+
440
+ ### compiler-\* dependencies
441
+
442
+ If `./tools` uses the compiler (`@jay-framework/compiler-jay-html`, `compiler-shared`), declare those
443
+ packages as **`peerDependencies`** (provided by the toolchain at tools time) plus **`devDependencies`**
444
+ (so the plugin's own build/test resolve them). Never put them in `dependencies` — that would pull the
445
+ compiler into runtime installs.
360
446
 
361
447
  ## Plugin-Contributed Agent-Kit Guides
362
448
 
@@ -23,7 +23,7 @@ agentkit: generateMyAgentKit # export name (NPM) or ./path (local) — optional
23
23
  description: Validate credentials and install config # optional, top-level
24
24
  ```
25
25
 
26
- **NPM plugins:** `setup` and `agentkit` are export names from the package entry point (`lib/index.ts`).
26
+ **NPM plugins:** `setup` and `agentkit` are export names from the **`./tools`** entry (`lib/tools.ts`). They are tools-time handlers (may use the compiler) and load only from `./tools` — do not re-export them from `lib/index.ts`, or the compiler can leak into the serve bundle.
27
27
  **Local plugins:** relative paths to handler modules (e.g. `agentkit: ./agentkit` — export `agentkit` or `default` from that module).
28
28
 
29
29
  `jay-stack validate-plugin` checks that declared handlers exist and are correctly exported.
@@ -266,13 +266,16 @@ See [aiditor-settings-guide.md](aiditor-settings-guide.md) for the full checklis
266
266
 
267
267
  ## Exporting Handlers
268
268
 
269
- For NPM plugins, export handlers from the package entry point:
269
+ For NPM plugins, export setup/agent-kit handlers from the **`./tools`** entry (they may use the
270
+ compiler and must stay out of the serve bundle):
270
271
 
271
272
  ```typescript
272
- // lib/index.ts
273
+ // lib/tools.ts (./tools) — compiler allowed, toolchain-only
273
274
  export { setupMyPlugin } from './setup.js';
274
275
  export { generateMyAgentKit } from './agentkit.js';
275
- // ... other exports (components, actions, services)
276
+
277
+ // lib/index.ts (.) — serve entry, compiler-free
278
+ // ... components, actions, services, init (NOT setup/agentkit)
276
279
  ```
277
280
 
278
281
  For local plugins, use relative paths in `plugin.yaml` and export `agentkit` or `default` from the handler module.
@@ -43,6 +43,24 @@ jay-stack validate-plugin -v
43
43
  - Component export names are valid strings (not file paths)
44
44
  - Action metadata files (`.jay-action`) exist
45
45
 
46
+ ### Capability-aware structure
47
+
48
+ `validate-plugin` derives required exports from the capabilities the plugin declares (see the
49
+ capability matrix in [plugin-structure.md](plugin-structure.md)):
50
+
51
+ - **≥1 capability** — a plugin declaring none is warned (a `global: true` plugin counts iff it exports
52
+ a resolvable `init`/`setup` handler).
53
+ - **`./tools` required** iff any tools capability (`validators`, `commands`, `agentkit`, `setup`) or a
54
+ `devOnly` action is declared — those handlers load only from `./tools`. Validator/setup/agentkit
55
+ handler names are checked against the **`./tools`** entry, not `.`.
56
+ - **`./client` required** iff a component has an interactive phase (detected by scanning the built
57
+ server `.` bundle for the interactive mark) or `contexts` is declared. Server-only and tools-only
58
+ plugins need no `./client`.
59
+ - **Leak scan** — the serve entry `dist/index.js` must contain no `@jay-framework/compiler-` import.
60
+ A compiler-using handler re-exported from `index.ts` fails this check; move it to `lib/tools.ts`.
61
+ - **`devOnly`** — `actions[].devOnly` / `routes[].devOnly` must be booleans; a `devOnly` action
62
+ requires the `./tools` export.
63
+
46
64
  ### Type Generation
47
65
 
48
66
  - Contracts compile to valid TypeScript types
@@ -133,20 +151,20 @@ Plugins can provide custom jay-html validation rules that run during `jay-stack
133
151
  ```yaml
134
152
  validators:
135
153
  - name: media-optimization
136
- handler: validateMediaOptimization # export name from package entry point
154
+ handler: validateMediaOptimization # export name from the ./tools entry
137
155
  description: Ensures media URLs use resize parameters
138
156
  ```
139
157
 
140
158
  **Handler format:**
141
159
 
142
- - **NPM plugins** — `handler` is an export name from the package entry point (e.g., `validateMediaOptimization`). The function must be exported from `lib/index.ts`.
160
+ - **NPM plugins** — `handler` is an export name from the **`./tools`** entry (e.g., `validateMediaOptimization`). The function must be exported from `lib/tools.ts`. **Never re-export a validator from `lib/index.ts`** — validators use compiler APIs (`walkElements`, `parseTemplateParts`, …), and re-exporting from the serve entry pulls the compiler into the production bundle (caught by the leak scan).
143
161
  - **Local plugins** (`src/plugins/`) — `handler` is a relative path to the module (e.g., `./validators/media-validator`). The module must export a `validate` function.
144
162
 
145
- `jay-stack validate-plugin` checks that the handler exists and is correctly exported.
163
+ `jay-stack validate-plugin` checks that the handler exists and is correctly exported from `./tools`.
146
164
 
147
165
  ### Writing a Validator
148
166
 
149
- Export the validator function from the package entry point (for NPM) or from the handler module (for local):
167
+ Export the validator function from the **`./tools`** entry (`lib/tools.ts`, for NPM) or from the handler module (for local):
150
168
 
151
169
  ```typescript
152
170
  import type { JayHtmlValidatorFn, JayHtmlValidationFinding } from '@jay-framework/compiler-shared';