@zerotal/arch 1.7.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
package/docs/view.md ADDED
@@ -0,0 +1,453 @@
1
+ ---
2
+ title: View
3
+ description: Render server-side HTML from typed JSX components that escape untrusted data by default.
4
+ ---
5
+
6
+ # View
7
+
8
+ Zerotal's view layer is a JSX server-side rendering engine built into
9
+ `@zerotal/core` — the same package that provides `ctx.view()`. It compiles JSX
10
+ to plain HTML strings at request time: no virtual DOM, no hydration, and no
11
+ client JavaScript unless you opt in.
12
+
13
+ ## Getting Started
14
+
15
+ The view runtime ships inside `@zerotal/core`, so there is no package to install
16
+ or provider to register — you only point the TypeScript JSX transform at it. Set
17
+ it once in `tsconfig.json` and every `.tsx` file in the project is covered:
18
+
19
+ ```json
20
+ // tsconfig.json
21
+ {
22
+ "compilerOptions": {
23
+ "jsx": "react-jsx",
24
+ "jsxImportSource": "zerotal"
25
+ }
26
+ }
27
+ ```
28
+
29
+ Scaffolded projects already have this — `bun create zerotal` writes it into the
30
+ template's `tsconfig.json`, so the JSX examples below compile as-is.
31
+
32
+ ## Basic usage
33
+
34
+ Components are plain TypeScript functions that return `SafeHtml`. Type them with
35
+ `FC<Props>` for the standard `(props, children) => SafeHtml` shape:
36
+
37
+ ```tsx
38
+ // resources/views/Card.tsx
39
+ import type { FC } from "zerotal/view";
40
+
41
+ interface CardProps {
42
+ title: string;
43
+ children?: unknown;
44
+ }
45
+
46
+ export const Card: FC<CardProps> = ({ title, children }) => (
47
+ <div class="card">
48
+ <h2>{title}</h2>
49
+ <div class="card-body">{children}</div>
50
+ </div>
51
+ );
52
+ ```
53
+
54
+ All string children are HTML-escaped automatically. To embed pre-rendered HTML,
55
+ use the [`safe()` helper or `Raw` component](#embedding-raw-html).
56
+
57
+ > **Tip** — Prefer native HTML attribute names (`class`, `for`, `tabindex`, …).
58
+ > React-style `className` and `htmlFor` are accepted and mapped to `class`/`for`,
59
+ > but native names are idiomatic here.
60
+
61
+ > **Warning** — SSR only, no client interactivity. The runtime emits plain HTML
62
+ > strings; event-handler props like `onClick` are not serialized and are silently
63
+ > dropped. For interactive UI, reach for [Flow](/docs/flow) (server-driven)
64
+ > or [Inertia](/docs/inertia) (React/Vue) instead.
65
+
66
+ ## Attributes
67
+
68
+ The runtime renders attributes from props with a few rules:
69
+
70
+ ```tsx
71
+ // in a component
72
+ <input type="text" value={name} disabled={isLocked} required={false} data-id={42} />
73
+ // → <input type="text" value="…" disabled data-id="42">
74
+ ```
75
+
76
+ - **Boolean `true`** renders the bare attribute (`disabled`); **`false`, `null`,
77
+ and `undefined`** omit it entirely — ideal for conditional attributes.
78
+ - Only **string** and **number** values are rendered as `key="value"` (both
79
+ escaped).
80
+ - `className` → `class` and `htmlFor` → `for` are mapped; every other key is used
81
+ verbatim.
82
+ - **No object values.** `style` must be a string (`style="color:red"`), not an
83
+ object — non-string, non-number values are dropped.
84
+ - `key` is ignored in output (it's a JSX hint, not an HTML attribute).
85
+
86
+ ## How children render
87
+
88
+ Children are rendered by type, which is the engine's security boundary:
89
+
90
+ | Child value | Output |
91
+ | --------------------------------------- | --------------------------------------- |
92
+ | `SafeHtml` (from JSX / `safe()`) | Passed through unescaped |
93
+ | `string` | **HTML-escaped** (treated as untrusted) |
94
+ | `number` | Stringified (inert — no escaping) |
95
+ | `true` / `false` / `null` / `undefined` | Renders nothing |
96
+ | `Array` | Each item rendered and concatenated |
97
+
98
+ Two consequences worth remembering:
99
+
100
+ ```tsx
101
+ // in a component
102
+ {
103
+ user && <Welcome name={user.name} />;
104
+ }
105
+ {
106
+ /* false/null → nothing, so guards just work */
107
+ }
108
+
109
+ {
110
+ count;
111
+ }
112
+ {
113
+ /* 0 renders "0" — numbers are never blank */
114
+ }
115
+ ```
116
+
117
+ Void elements (`<br>`, `<img>`, `<input>`, `<hr>`, `<meta>`, …) render without a
118
+ closing tag automatically.
119
+
120
+ ## Rendering in a controller
121
+
122
+ Call `view()` from a controller action to set the response. It accepts any value
123
+ whose `.toString()` returns HTML — a `SafeHtml` instance from JSX is the normal
124
+ case:
125
+
126
+ ```tsx
127
+ // app/controllers/PostController.tsx
128
+ import { view } from "zerotal";
129
+ import type { HttpContext } from "zerotal";
130
+ import { Post } from "../models/Post.ts";
131
+ import { PostList } from "../resources/views/PostList.tsx";
132
+
133
+ export class PostController {
134
+ async index(ctx: HttpContext) {
135
+ const posts = await Post.query().latest().get();
136
+ view(<PostList posts={posts} title="All Posts" />);
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### Passing a component and props
142
+
143
+ You can also hand `view()` (or `ctx.view()`) the **component itself** plus its
144
+ props. The component receives the request `HttpContext` as its first argument and
145
+ your props as its second; route params and model bindings reach it through
146
+ `ctx.params`:
147
+
148
+ ```tsx
149
+ // resources/views/Welcome.tsx
150
+ import type { HttpContext } from "zerotal";
151
+
152
+ export default function Welcome(ctx: HttpContext, { title }: { title: string }) {
153
+ return (
154
+ <div>
155
+ <h1>{title}</h1>
156
+ <p>{ctx.url.pathname}</p>
157
+ </div>
158
+ );
159
+ }
160
+ ```
161
+
162
+ ```ts
163
+ // routes/index.ts — the HttpContext is injected; you only pass the extra props
164
+ import { Router } from "zerotal";
165
+ import Welcome from "../resources/views/Welcome.tsx";
166
+
167
+ Router.get("/", () => view(Welcome, { title: "Welcome to Zerotal" }));
168
+ ```
169
+
170
+ The component may be async — `view()` awaits it before setting the response.
171
+
172
+ > **Warning** — JSX renders synchronously. A JSX expression evaluates to
173
+ > `SafeHtml` right away, so components used inline (`<PostList … />`) can't be
174
+ > `async`. If you need to load data while rendering, use the component + props
175
+ > form above (which awaits an async component), or do the loading in the
176
+ > controller or a [`Router.view`](#controller-less-pages) props factory and pass
177
+ > the resolved data in as props.
178
+
179
+ ### Which view form should I use?
180
+
181
+ | You have… | Use |
182
+ | ------------------------------------------------------------- | --------------------------------------- |
183
+ | Already-rendered JSX, data loaded synchronously | `view(<Page … />)` |
184
+ | An async component, or want the `HttpContext`/params injected | `view(Page, props)` |
185
+ | A page with no controller at all | [`Router.view`](#controller-less-pages) |
186
+ | A page resolved from the filesystem | [File-based pages](#file-based-pages) |
187
+
188
+ ## Layouts
189
+
190
+ Bind a shared layout to page components so every page doesn't need to manually
191
+ wrap its content. `defineLayout(Layout)` returns a `wrap(Page)` factory; the
192
+ wrapped page merges the layout's props (minus `children`) with its own:
193
+
194
+ ```tsx
195
+ // resources/views/layouts/AppLayout.tsx
196
+
197
+ interface AppLayoutProps {
198
+ title: string;
199
+ children?: unknown;
200
+ }
201
+
202
+ export function AppLayout({ title, children }: AppLayoutProps) {
203
+ return (
204
+ <html lang="en">
205
+ <head>
206
+ <meta charset="UTF-8" />
207
+ <title>{title} — My App</title>
208
+ <link rel="stylesheet" href="/assets/app.css" />
209
+ </head>
210
+ <body>{children}</body>
211
+ </html>
212
+ );
213
+ }
214
+ ```
215
+
216
+ ```tsx
217
+ // resources/views/About.tsx
218
+ import { defineLayout } from "zerotal/view";
219
+ import { AppLayout } from "./layouts/AppLayout.tsx";
220
+
221
+ const wrap = defineLayout(AppLayout);
222
+
223
+ // AboutPage receives { title } (from AppLayout) merged with its own props
224
+ export const AboutPage = wrap<{ heading: string }>(({ heading }) => (
225
+ <main>
226
+ <h1>{heading}</h1>
227
+ <p>We build things.</p>
228
+ </main>
229
+ ));
230
+ ```
231
+
232
+ ```ts
233
+ // routes/index.ts
234
+ import { Router } from "zerotal";
235
+ import { AboutPage } from "../resources/views/About.tsx";
236
+
237
+ Router.view("/about", AboutPage, { title: "About Us", heading: "Hello" });
238
+ ```
239
+
240
+ `Router.view()` calls the component and responds with the HTML string.
241
+
242
+ ## Controller-less pages
243
+
244
+ For server-rendered pages that don't need a controller, register the component
245
+ directly with `Router.view()`. The third argument is the props — a **static
246
+ object**, or a **per-request factory** that receives the `HttpContext` and may be
247
+ async:
248
+
249
+ ```ts
250
+ // routes/index.ts
251
+ // Static props — evaluated once at registration (marketing / info pages):
252
+ Router.view("/about", AboutPage, { title: "About Us" });
253
+
254
+ // Dynamic props — resolved per request, can be async:
255
+ Router.view("/dashboard", DashboardPage, async (ctx) => ({
256
+ user: ctx.user,
257
+ posts: await Post.query().where("user_id", ctx.user!.id).get(),
258
+ }));
259
+
260
+ // No props:
261
+ Router.view("/privacy", PrivacyPage);
262
+ ```
263
+
264
+ The returned registration is chainable:
265
+
266
+ ```ts
267
+ // routes/index.ts
268
+ Router.view("/terms", TermsPage)
269
+ .name("terms") // name the route for url() generation
270
+ .withLayout(AppLayout); // wrap the output in a layout component
271
+ ```
272
+
273
+ ## File-based pages
274
+
275
+ Under [file-based routing](/docs/routing), a page file's **default export**
276
+ becomes a `GET` route automatically. The page is a function of `(http, params)`
277
+ and may be async:
278
+
279
+ ```tsx
280
+ // app/views/posts/[slug].tsx → GET /posts/:slug
281
+ import type { HttpContext } from "zerotal";
282
+ import { Post } from "../../models/Post.ts";
283
+
284
+ export default async function PostPage(http: HttpContext, params: { slug: string }) {
285
+ const post = await Post.query().where("slug", params.slug).firstOrFail();
286
+ return (
287
+ <article>
288
+ <h1>{post.title}</h1>
289
+ <p>{post.excerpt}</p>
290
+ </article>
291
+ );
292
+ }
293
+
294
+ // Optional — wrap every page in this directory with a layout:
295
+ export { AppLayout as layout } from "../layouts/AppLayout.tsx";
296
+ ```
297
+
298
+ In a `.tsx`/`.jsx` file the default-exported function is detected as a page
299
+ automatically. If your page lives in a plain `.ts` file, mark it with
300
+ `definePage()` so the resolver recognizes it:
301
+
302
+ ```ts
303
+ // app/views/greet.ts
304
+ import { definePage } from "zerotal/view";
305
+
306
+ export default definePage((http, params) => `<h1>Hello ${params.name}</h1>`);
307
+ ```
308
+
309
+ ## Embedding raw HTML
310
+
311
+ When you have pre-rendered markup (e.g. from a Markdown renderer) use `safe()` or
312
+ `<Raw>` to bypass escaping:
313
+
314
+ ```tsx
315
+ // in a component
316
+ import { safe, Raw } from "zerotal/view";
317
+
318
+ // Option 1 — safe(): wrap a string as SafeHtml inline
319
+ <article>{safe(markdownToHtml(post.body))}</article>
320
+
321
+ // Option 2 — Raw component: composable, e.g. passed as a child
322
+ <article><Raw html={markdownToHtml(post.body)} /></article>
323
+
324
+ // Option 3 — dangerouslySetInnerHTML on an element (best for a single element)
325
+ <article dangerouslySetInnerHTML={{ __html: markdownToHtml(post.body) }} />
326
+ ```
327
+
328
+ `dangerouslySetInnerHTML` replaces the element's children entirely.
329
+
330
+ > **Danger** — All three bypass escaping. Only pass HTML you trust or have
331
+ > sanitized; rendering untrusted markup this way is an XSS hole.
332
+
333
+ ## Escaping outside JSX
334
+
335
+ `esc()` escapes a value for use in raw string templates, where automatic JSX
336
+ escaping isn't available:
337
+
338
+ ```ts
339
+ // in a helper
340
+ import { esc } from "zerotal/view";
341
+
342
+ const snippet = `<p>Hello, ${esc(user.name)}!</p>`;
343
+ ```
344
+
345
+ ## Fragments
346
+
347
+ Use `Fragment` (or the `<>...</>` shorthand) to return multiple root elements:
348
+
349
+ ```tsx
350
+ // in a component
351
+ import { Fragment } from "zerotal/view";
352
+
353
+ const Items = ({ items }: { items: string[] }) => (
354
+ <>
355
+ {items.map((item) => (
356
+ <li>{item}</li>
357
+ ))}
358
+ </>
359
+ );
360
+ ```
361
+
362
+ ## Testing
363
+
364
+ Set your suite up once as described in [Testing](/docs/testing). A view is a
365
+ function returning a node that stringifies, so most of it tests without a server.
366
+
367
+ **Render the component and assert on the string.** No request, no router, no
368
+ browser:
369
+
370
+ ```typescript
371
+ // tests/views/PostCard.test.ts
372
+ import { test, expect } from "bun:test";
373
+ import { PostCard } from "../../resources/views/PostCard.tsx";
374
+
375
+ test("renders the title and author", () => {
376
+ const html = String(PostCard({ post: { title: "Hello", author: "Jane" } }));
377
+
378
+ expect(html).toContain("Hello");
379
+ expect(html).toContain("Jane");
380
+ });
381
+ ```
382
+
383
+ **Escaping is the test that matters.** Every view that renders user input has one
384
+ job beyond looking right, and it is the job that becomes a security incident:
385
+
386
+ ```typescript
387
+ // tests/views/PostCard.test.ts
388
+ test("escapes markup in user-supplied text", () => {
389
+ const html = String(PostCard({ post: { title: "<script>alert(1)</script>" } }));
390
+
391
+ expect(html).not.toContain("<script>alert(1)</script>");
392
+ expect(html).toContain("&lt;script&gt;");
393
+ });
394
+ ```
395
+
396
+ Write the same test for anything passed through `safe()` or `<Raw>` — those opt
397
+ out of escaping deliberately, so the test documents that the value is trusted and
398
+ fails if someone later routes user input into it.
399
+
400
+ **Through a route, assert the rendered text** rather than the markup around it.
401
+ `assertSee` survives a class rename; a full-HTML comparison does not:
402
+
403
+ ```typescript
404
+ // tests/http/posts.test.ts
405
+ const res = await app.get("/posts");
406
+
407
+ res.assertOk();
408
+ res.assertSee("Hello");
409
+ res.assertDontSee("Draft"); // unpublished posts stay hidden
410
+ ```
411
+
412
+ > **Note** — `assertSee` matches anywhere in the body, so a short string can
413
+ > match an attribute or a class name and pass for the wrong reason. Assert on
414
+ > something distinctive enough to only appear in the content you mean.
415
+
416
+ ## References
417
+
418
+ ### Helpers
419
+
420
+ | Export | Signature | Description |
421
+ | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
422
+ | `safe(html)` | `(html: string) => SafeHtml` | Wrap a trusted HTML string as `SafeHtml` so it isn't re-escaped. |
423
+ | `Raw({ html })` | `(props: { html: string }) => SafeHtml` | Component form of `safe()` — composable as a child. |
424
+ | `esc(value)` | `(value: unknown) => string` | Escape a value for raw string templates (not needed inside JSX). |
425
+ | `Fragment` | `(props: { children?: unknown }) => SafeHtml` | Group multiple roots; `<>…</>` is shorthand. |
426
+ | `defineLayout(L)` | `(Layout) => (Page) => Component` | Bind a layout to pages; returns a `wrap(Page)` factory. |
427
+ | `definePage(fn)` | `(fn: (http, params) => SafeHtml \| string) => fn` | Mark a `(http, params)` function as a file-route page (`.ts` files). |
428
+
429
+ ### Response and routing helpers
430
+
431
+ | Member | Signature | Description |
432
+ | ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- |
433
+ | `view` | `view(markup, status?): void` | Set a pre-rendered view as the current response. |
434
+ | `view` | `view(component, props?, status?): void \| Promise<void>` | Render a component called with the `HttpContext` and your `props`. |
435
+ | `Router.view` | `Router.view(path, component, props?, middleware?): ViewRegistration` | Register a controller-less GET route that renders a view component. |
436
+
437
+ ### Types
438
+
439
+ | Export | Description |
440
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
441
+ | `SafeHtml` | Opaque wrapper for already-escaped HTML — the return type of every JSX expression. |
442
+ | `Html` | Alias for `SafeHtml` — useful in controller return-type annotations. |
443
+ | `FC<P>` | Functional component type — `(props: P & { children?: unknown }) => SafeHtml`. |
444
+ | `Children` | Type for the `children` prop. |
445
+ | `HttpContext<T>` | The request context a view component or controller action receives; route params and model bindings live on `ctx.params`. |
446
+
447
+ ## Next steps
448
+
449
+ - [Flow](/docs/flow) — server-driven interactive components over WebSocket.
450
+ - [Inertia](/docs/inertia) — React/Vue SPA pages backed by your controllers.
451
+ - [Routing](/docs/routing) — `Router.view()` and file-based page routing.
452
+ - [Responses](/docs/responses) — `view()` and the other response helpers.
453
+ - [Controllers](/docs/controllers) — move view logic into controller actions.
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@zerotal/arch",
3
+ "version": "1.7.0",
4
+ "license": "MIT",
5
+ "maturity": "beta",
6
+ "private": false,
7
+ "type": "module",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./mcp": "./src/mcp/index.ts"
13
+ },
14
+ "bin": {
15
+ "zerotal-arch": "./src/bin/mcp.ts"
16
+ },
17
+ "files": [
18
+ "CHANGELOG.md",
19
+ "api-surface.md",
20
+ "docs",
21
+ "src",
22
+ "!src/**/*.test.ts",
23
+ "!src/**/*.test.tsx",
24
+ "!src/**/*.spec.ts",
25
+ "!src/**/__fixtures__/**"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "engines": {
31
+ "bun": ">=1.3.14"
32
+ },
33
+ "scripts": {
34
+ "test": "bun test",
35
+ "typecheck": "tsc --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "@zerotal/core": "1.7.0"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.8.0",
42
+ "@zerotal/orm": "1.7.0"
43
+ },
44
+ "description": "The Zerotal agent surface — an MCP server that hands coding agents the framework's machine-readable truth: exact API signatures, live routes and schema, version-matched docs, and `zt doctor`.",
45
+ "keywords": [
46
+ "zerotal",
47
+ "bun",
48
+ "typescript",
49
+ "framework",
50
+ "mcp",
51
+ "agent"
52
+ ],
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/zerotaldev/zerotal.git",
56
+ "directory": "packages/arch"
57
+ },
58
+ "homepage": "https://github.com/zerotaldev/zerotal/tree/main/packages/arch#readme",
59
+ "bugs": "https://github.com/zerotaldev/zerotal/issues"
60
+ }
package/src/bin/mcp.ts ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * The MCP server, as a process.
4
+ *
5
+ * ## Why this is a bin and not a `zt` command
6
+ *
7
+ * The stdio binding says the server MUST NOT write anything to stdout that is
8
+ * not an MCP message, and a stray line does not degrade the session — it
9
+ * desynchronises the client's line parser and corrupts every frame after it.
10
+ * Every `bun zt <cmd>` boots the application first, and a booted application
11
+ * prints: dev banners, provider notices, warnings from packages that noticed
12
+ * something. There is no way to hold that guarantee from inside a command whose
13
+ * own boot has already happened.
14
+ *
15
+ * So this process never boots an app. It resolves nothing from the container,
16
+ * registers no providers, and reads no config. The tools that need a booted app
17
+ * spawn one — see `tools/_probe.ts` — which has the second, larger benefit: the
18
+ * answer describes the code as it is now, not as it was when a long-lived server
19
+ * started. The caller is an agent editing that code between calls.
20
+ *
21
+ * Run by `.mcp.json` as:
22
+ *
23
+ * bun node_modules/@zerotal/arch/src/bin/mcp.ts
24
+ */
25
+ import { dirname, resolve } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ import { McpServer } from "../mcp/server.ts";
28
+ import { serveStdio } from "../mcp/stdio.ts";
29
+ import { archTools } from "../tools/index.ts";
30
+ import { vendoredDocsDir } from "../tools/context.ts";
31
+ import { findApp, spawnProbe } from "../tools/_probe.ts";
32
+
33
+ /** This package's own version, for `serverInfo`. */
34
+ async function ownVersion(): Promise<string> {
35
+ try {
36
+ const manifest = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
37
+ const parsed = (await Bun.file(manifest).json()) as Record<string, unknown>;
38
+ return typeof parsed["version"] === "string" ? parsed["version"] : "0.0.0";
39
+ } catch {
40
+ return "0.0.0";
41
+ }
42
+ }
43
+
44
+ const cwd = process.cwd();
45
+ const app = await findApp(cwd);
46
+
47
+ // stderr, never stdout. The spec permits it and tells clients not to read it as
48
+ // failure, which makes it the only place a diagnostic can go.
49
+ if (app) {
50
+ process.stderr.write(`[arch] serving ${app.root}\n`);
51
+ } else {
52
+ process.stderr.write(
53
+ `[arch] no Zerotal app found at or above ${cwd}. Tools that need a booted app will say so; ` +
54
+ `docs search and API surface still work.\n`,
55
+ );
56
+ }
57
+
58
+ const root = app?.root ?? cwd;
59
+
60
+ await serveStdio({
61
+ server: new McpServer({
62
+ identity: {
63
+ name: "zerotal-arch",
64
+ title: "Zerotal",
65
+ version: await ownVersion(),
66
+ },
67
+ tools: archTools({
68
+ root,
69
+ docsDir: vendoredDocsDir(),
70
+ probe: spawnProbe({ cwd: root }),
71
+ }),
72
+ }),
73
+ });
package/src/config.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { deepMerge } from "@zerotal/core";
2
+
3
+ /**
4
+ * What `zt arch:install` writes, and under what name.
5
+ *
6
+ * Only the install side is configurable. The MCP server itself never boots an
7
+ * application, so it can read no config — which is the point of it, and the
8
+ * reason there is nothing here about tools or transports.
9
+ */
10
+ export interface ArchConfigShape {
11
+ /**
12
+ * Write `AGENTS.md`, the cross-tool instruction file. Default: `true`.
13
+ *
14
+ * Read natively by Cursor, Copilot, Codex, Gemini CLI, Aider, Windsurf and
15
+ * Zed, so it is where the guidelines belong even in a single-tool project.
16
+ */
17
+ agentsFile: boolean;
18
+ /**
19
+ * Write a `CLAUDE.md` that imports `AGENTS.md`. Default: `true`.
20
+ *
21
+ * A one-line shim rather than a second copy: Claude Code does not read
22
+ * `AGENTS.md` natively, and two files of guidance drift apart.
23
+ */
24
+ claudeFile: boolean;
25
+ /** Write the MCP client configuration. Default: `true`. */
26
+ mcpConfig: boolean;
27
+ /** Path of the MCP client config, relative to the project root. */
28
+ mcpConfigPath: string;
29
+ /**
30
+ * The key this server is registered under in the MCP config. Default: `zerotal`.
31
+ *
32
+ * Worth changing only when a project already has a server by that name.
33
+ */
34
+ serverName: string;
35
+ }
36
+
37
+ const defaults: ArchConfigShape = {
38
+ agentsFile: true,
39
+ claudeFile: true,
40
+ mcpConfig: true,
41
+ mcpConfigPath: ".mcp.json",
42
+ serverName: "zerotal",
43
+ };
44
+
45
+ /**
46
+ * Build an {@link ArchConfigShape} with defaults applied.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * // config/arch.ts
51
+ * import { ArchConfig } from "@zerotal/arch";
52
+ * export default ArchConfig({ claudeFile: false });
53
+ * ```
54
+ */
55
+ export function ArchConfig(options: Partial<ArchConfigShape> = {}): ArchConfigShape {
56
+ return deepMerge(defaults, options);
57
+ }
58
+
59
+ declare module "@zerotal/core" {
60
+ interface ConfigRegistry {
61
+ arch: ArchConfigShape;
62
+ }
63
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { ZerotalError } from "@zerotal/core";
2
+
3
+ /** Base class for all @zerotal/arch errors. */
4
+ export class ArchError extends ZerotalError {
5
+ constructor(message: string, code = "E_ARCH", status = 500, context?: Record<string, unknown>) {
6
+ super(message, code, status, context);
7
+ }
8
+ }
9
+
10
+ /**
11
+ * `arch:install` was run somewhere that is not a project.
12
+ *
13
+ * Thrown rather than warned: writing agent instructions into whatever directory
14
+ * happened to be current is not a recoverable mistake for whoever has to find
15
+ * the stray files later.
16
+ */
17
+ export class NoProjectRootError extends ArchError {
18
+ constructor(dir: string) {
19
+ super(
20
+ `[Zerotal Arch] No package.json at or above ${dir}, so there is no project to install ` +
21
+ `into. Run this from the root of a Zerotal app.`,
22
+ "E_ARCH_NO_PROJECT_ROOT",
23
+ 500,
24
+ { dir },
25
+ );
26
+ }
27
+ }