@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/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `@zerotal/arch` — the agent surface.
3
+ *
4
+ * A coding agent working in a Zerotal app should not have to recall an API, or
5
+ * guess a route name, or say "looks fine to me". The framework already produces
6
+ * the answers mechanically: `api-surface.md` records every export's exact
7
+ * signature, `runDoctor()` returns structured findings with a fix beside each,
8
+ * the router knows its routes and the ORM knows its columns. This package hands
9
+ * all of it over as MCP tools.
10
+ *
11
+ * Install it into a project with `bun zt arch:install`. The server itself is a
12
+ * bin — `node_modules/@zerotal/arch/src/bin/mcp.ts` — and deliberately never
13
+ * boots the application; see that file for the reasoning.
14
+ */
15
+
16
+ // The provider, which registers `arch:install`, `arch:update` and `arch:probe`.
17
+ export { ArchProvider } from "./provider/ArchProvider.ts";
18
+
19
+ // Config factory + its shape.
20
+ export { ArchConfig } from "./config.ts";
21
+ export type { ArchConfigShape } from "./config.ts";
22
+
23
+ // Typed error vocabulary.
24
+ export * from "./errors.ts";
25
+
26
+ // The tools, and the context they are built from. Exported so a project can
27
+ // serve a subset, or add its own alongside them.
28
+ export { archTools, vendoredDocsDir } from "./tools/index.ts";
29
+ export type { ToolContext } from "./tools/index.ts";
30
+ export { findApp, spawnProbe } from "./tools/_probe.ts";
31
+ export type { ProbeResult, ProbeRunner, SpawnProbeOptions } from "./tools/_probe.ts";
32
+
33
+ // The in-app reads, for anyone wanting them without the subprocess.
34
+ export { PROBE_TOPICS, isProbeTopic, probe } from "./probe/topics.ts";
35
+ export type {
36
+ AppInfo,
37
+ DoctorFinding,
38
+ DoctorReport,
39
+ InstalledPackage,
40
+ ProbeTopic,
41
+ RouteEntry,
42
+ RouteReport,
43
+ SchemaColumn,
44
+ SchemaModel,
45
+ SchemaReport,
46
+ } from "./probe/topics.ts";
47
+
48
+ // The install writers, so a project can generate the same files its own way.
49
+ export { detectAgents } from "./install/detect.ts";
50
+ export type { Detected, McpTarget } from "./install/detect.ts";
51
+ export { applyMcpConfig, serverEntry, SERVER_ENTRY_PATH } from "./install/mcpConfig.ts";
52
+ export type { ConfigOutcome } from "./install/mcpConfig.ts";
53
+ export { applyBlock, fence, BLOCK_END, BLOCK_START } from "./install/markers.ts";
54
+ export type { BlockOutcome } from "./install/markers.ts";
55
+ export { agentsPreamble, buildGuidelines, claudeShim } from "./install/guidelines.ts";
56
+ export type { GuidelineOptions } from "./install/guidelines.ts";
@@ -0,0 +1,189 @@
1
+ /**
2
+ * `bun zt arch:install` — register the MCP server and write the agent
3
+ * instructions. Also registered as `arch:update`, because re-running it *is* the
4
+ * update: every write is a merge into a fenced region, so the second run and the
5
+ * fiftieth do the same thing.
6
+ *
7
+ * That idempotence is the contract, not a nicety. This command runs again on
8
+ * every framework upgrade, against files a developer is invited to edit. A run
9
+ * that clobbered a hand-written paragraph would be discovered once and never
10
+ * trusted again — so anything it cannot merge cleanly it reports and leaves
11
+ * alone.
12
+ */
13
+ import { dirname, join } from "node:path";
14
+ import { Command } from "@zerotal/core";
15
+ import type { Application } from "@zerotal/core";
16
+ import { ArchConfig } from "../config.ts";
17
+ import type { ArchConfigShape } from "../config.ts";
18
+ import { NoProjectRootError } from "../errors.ts";
19
+ import { installedPackages } from "../probe/topics.ts";
20
+ import { detectAgents } from "./detect.ts";
21
+ import { agentsPreamble, buildGuidelines, claudeShim } from "./guidelines.ts";
22
+ import { applyBlock } from "./markers.ts";
23
+ import { applyMcpConfig } from "./mcpConfig.ts";
24
+
25
+ /** One file the run touched, and what happened to it. */
26
+ interface Change {
27
+ path: string;
28
+ status: "created" | "updated" | "unchanged" | "conflict";
29
+ detail?: string;
30
+ text?: string;
31
+ }
32
+
33
+ export class ArchInstallCommand extends Command {
34
+ static override commandName = "arch:install";
35
+ static override description =
36
+ "Register the Zerotal MCP server and write agent instructions (alias: arch:update)";
37
+ static override needsApp = true;
38
+ static override args = [];
39
+ static override flags = [
40
+ {
41
+ name: "dry",
42
+ type: "boolean" as const,
43
+ description: "Show what would change without writing anything",
44
+ default: false,
45
+ },
46
+ ];
47
+
48
+ async run(): Promise<void> {
49
+ const dry = this.flags["dry"] === true;
50
+ const root = await findProjectRoot(process.cwd());
51
+ const config = this._config();
52
+
53
+ const detected = await detectAgents(root);
54
+ const packages = (await installedPackages(root)).map((pkg) => pkg.name);
55
+
56
+ const changes: Change[] = [
57
+ ...(config.mcpConfig ? await this._mcpChanges(root, config, detected.targets) : []),
58
+ ...(config.agentsFile ? [await this._agentsChange(root, config, packages)] : []),
59
+ ...(config.claudeFile ? [await this._claudeChange(root)] : []),
60
+ ];
61
+
62
+ this.section(dry ? "arch:install — dry run" : "arch:install");
63
+ if (detected.agents.length > 0) {
64
+ this.dim(` detected: ${detected.agents.join(", ")}`);
65
+ } else {
66
+ this.dim(" no agent tooling detected — writing the portable defaults");
67
+ }
68
+ this.newLine();
69
+
70
+ let wrote = 0;
71
+ let conflicts = 0;
72
+
73
+ for (const change of changes) {
74
+ if (change.status === "conflict") {
75
+ conflicts++;
76
+ this.error(` ✗ ${change.path} — ${change.detail ?? "left untouched"}`);
77
+ continue;
78
+ }
79
+ if (change.status === "unchanged") {
80
+ this.dim(` · ${change.path} — already up to date`);
81
+ continue;
82
+ }
83
+ if (!dry && change.text !== undefined) {
84
+ await Bun.write(join(root, change.path), change.text);
85
+ }
86
+ wrote++;
87
+ this.info(
88
+ ` ${dry ? "would " : ""}${change.status === "created" ? "create" : "update"} ${change.path}`,
89
+ );
90
+ }
91
+
92
+ this.newLine();
93
+ if (conflicts > 0) {
94
+ this.warn(
95
+ `${conflicts} file(s) could not be merged and were left as they are. ` +
96
+ `Fix the markers or the JSON and run this again.`,
97
+ );
98
+ }
99
+ if (dry) {
100
+ this.line(`${wrote} file(s) would change. Run without --dry to apply.`);
101
+ return;
102
+ }
103
+ if (wrote === 0) {
104
+ this.line("Everything is already in place.");
105
+ return;
106
+ }
107
+ this.line("Restart your agent so it picks up the MCP server.");
108
+ }
109
+
110
+ /** The `arch` config namespace, or the defaults when the app declares none. */
111
+ private _config(): ArchConfigShape {
112
+ const app = this.app as Application | undefined;
113
+ try {
114
+ const store = app?.container.makeSync("config") as
115
+ { get(key: string, fallback?: unknown): unknown } | undefined;
116
+ const declared = store?.get("arch", {});
117
+ return ArchConfig((declared ?? {}) as Partial<ArchConfigShape>);
118
+ } catch {
119
+ return ArchConfig();
120
+ }
121
+ }
122
+
123
+ private async _mcpChanges(
124
+ root: string,
125
+ config: ArchConfigShape,
126
+ detected: Awaited<ReturnType<typeof detectAgents>>["targets"],
127
+ ): Promise<Change[]> {
128
+ // The configured path always gets written; the detected ones are extra.
129
+ const targets = detected.some((target) => target.path === config.mcpConfigPath)
130
+ ? detected
131
+ : [
132
+ { path: config.mcpConfigPath, key: "mcpServers" as const, client: "MCP client" },
133
+ ...detected,
134
+ ];
135
+
136
+ const changes: Change[] = [];
137
+ for (const target of targets) {
138
+ const outcome = applyMcpConfig(
139
+ await readIfPresent(join(root, target.path)),
140
+ config.serverName,
141
+ target,
142
+ );
143
+ changes.push(
144
+ outcome.status === "conflict"
145
+ ? { path: target.path, status: "conflict", detail: outcome.reason }
146
+ : { path: target.path, status: outcome.status, text: outcome.text },
147
+ );
148
+ }
149
+ return changes;
150
+ }
151
+
152
+ private async _agentsChange(
153
+ root: string,
154
+ config: ArchConfigShape,
155
+ packages: string[],
156
+ ): Promise<Change> {
157
+ const outcome = applyBlock(
158
+ await readIfPresent(join(root, "AGENTS.md")),
159
+ buildGuidelines({ packages, serverName: config.serverName }),
160
+ agentsPreamble(),
161
+ );
162
+ return outcome.status === "conflict"
163
+ ? { path: "AGENTS.md", status: "conflict", detail: outcome.reason }
164
+ : { path: "AGENTS.md", status: outcome.status, text: outcome.text };
165
+ }
166
+
167
+ private async _claudeChange(root: string): Promise<Change> {
168
+ const outcome = applyBlock(await readIfPresent(join(root, "CLAUDE.md")), claudeShim());
169
+ return outcome.status === "conflict"
170
+ ? { path: "CLAUDE.md", status: "conflict", detail: outcome.reason }
171
+ : { path: "CLAUDE.md", status: outcome.status, text: outcome.text };
172
+ }
173
+ }
174
+
175
+ async function readIfPresent(path: string): Promise<string | undefined> {
176
+ const file = Bun.file(path);
177
+ return (await file.exists()) ? file.text() : undefined;
178
+ }
179
+
180
+ /** The nearest ancestor with a `package.json`. */
181
+ async function findProjectRoot(start: string): Promise<string> {
182
+ let dir = start;
183
+ for (;;) {
184
+ if (await Bun.file(join(dir, "package.json")).exists()) return dir;
185
+ const parent = dirname(dir);
186
+ if (parent === dir) throw new NoProjectRootError(start);
187
+ dir = parent;
188
+ }
189
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Which coding agents this project is already set up for.
3
+ *
4
+ * Detection is by the directories and files an agent leaves behind, not by
5
+ * asking. A project with `.cursor/` gets a Cursor MCP config; one without does
6
+ * not get a directory it never wanted. The one exception is `.mcp.json` at the
7
+ * root, which is written unconditionally: it is the widely-read project-scoped
8
+ * location, and a project with no agent configured yet is exactly the project
9
+ * about to configure one.
10
+ */
11
+ import { join } from "node:path";
12
+
13
+ /** Where an MCP client keeps its server list, and under which key. */
14
+ export interface McpTarget {
15
+ /** Path relative to the project root. */
16
+ path: string;
17
+ /**
18
+ * The object servers are listed under.
19
+ *
20
+ * Most clients use `mcpServers`; VS Code's own file uses `servers`. Writing
21
+ * the wrong one produces a config that parses and does nothing.
22
+ */
23
+ key: "mcpServers" | "servers";
24
+ /** What reads this file, for the report. */
25
+ client: string;
26
+ }
27
+
28
+ export interface Detected {
29
+ /** Agent tooling found in the project, by name. */
30
+ agents: string[];
31
+ /** MCP config files worth writing here. */
32
+ targets: McpTarget[];
33
+ }
34
+
35
+ interface Probe {
36
+ agent: string;
37
+ /** A path whose existence means this agent is in use. */
38
+ marker: string;
39
+ target?: McpTarget;
40
+ }
41
+
42
+ const PROBES: Probe[] = [
43
+ {
44
+ agent: "Claude Code",
45
+ marker: ".claude",
46
+ target: { path: ".mcp.json", key: "mcpServers", client: "Claude Code" },
47
+ },
48
+ {
49
+ agent: "Cursor",
50
+ marker: ".cursor",
51
+ target: { path: ".cursor/mcp.json", key: "mcpServers", client: "Cursor" },
52
+ },
53
+ {
54
+ agent: "VS Code / Copilot",
55
+ marker: ".vscode",
56
+ target: { path: ".vscode/mcp.json", key: "servers", client: "VS Code" },
57
+ },
58
+ { agent: "GitHub Copilot", marker: ".github/copilot-instructions.md" },
59
+ { agent: "Gemini CLI", marker: ".gemini" },
60
+ { agent: "Codex", marker: ".codex" },
61
+ { agent: "Windsurf", marker: ".windsurf" },
62
+ ];
63
+
64
+ /** The config every project gets, whatever it already has. */
65
+ const DEFAULT_TARGET: McpTarget = {
66
+ path: ".mcp.json",
67
+ key: "mcpServers",
68
+ client: "Claude Code and other clients reading .mcp.json",
69
+ };
70
+
71
+ export async function detectAgents(root: string): Promise<Detected> {
72
+ const agents: string[] = [];
73
+ const targets = new Map<string, McpTarget>([[DEFAULT_TARGET.path, DEFAULT_TARGET]]);
74
+
75
+ for (const probe of PROBES) {
76
+ if (!(await exists(join(root, probe.marker)))) continue;
77
+ agents.push(probe.agent);
78
+ if (probe.target) targets.set(probe.target.path, probe.target);
79
+ }
80
+
81
+ return { agents, targets: [...targets.values()] };
82
+ }
83
+
84
+ /**
85
+ * Whether a path exists, directory or file.
86
+ *
87
+ * `Bun.file(dir).exists()` answers `false` for a directory, so the markers here
88
+ * — most of which are directories — need the stat.
89
+ */
90
+ async function exists(path: string): Promise<boolean> {
91
+ try {
92
+ const { stat } = await import("node:fs/promises");
93
+ await stat(path);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * The instructions written into `AGENTS.md`.
3
+ *
4
+ * Composed from what the project actually has: an app without `@zerotal/admin`
5
+ * never reads a line about admin resources. Every prompt this file lands in pays
6
+ * for its whole length, so each block earns its place or is not emitted.
7
+ *
8
+ * The content deliberately points rather than teaches. There is a `search_docs`
9
+ * tool and an `api_surface` tool three lines above; a paraphrase of the routing
10
+ * guide here would be a second copy to drift, and a worse one — the pages it
11
+ * would summarise are the ones those tools return in full.
12
+ */
13
+
14
+ /** A block contributed by one installed package. */
15
+ interface PackageBlock {
16
+ /** The package that triggers it. */
17
+ pkg: string;
18
+ lines: string[];
19
+ }
20
+
21
+ /**
22
+ * Per-package guidance, at the level of "this exists, here is what owns it".
23
+ *
24
+ * Kept to a few lines each on purpose: the point is to stop an agent inventing
25
+ * an API when the framework already has one, not to replace the documentation
26
+ * it can now search.
27
+ */
28
+ const PACKAGE_BLOCKS: PackageBlock[] = [
29
+ {
30
+ pkg: "@zerotal/orm",
31
+ lines: [
32
+ "**Data.** Models are Active Record classes in `app/models/`, declared with `@table` and " +
33
+ "`@column` decorators; `DB` from `@zerotal/orm` is the query builder. Migrations live in " +
34
+ "`database/migrations/` — generate with `bun zt make:migration`, never hand-edit an " +
35
+ "applied one. Call `schema` before writing a query or a factory.",
36
+ ],
37
+ },
38
+ {
39
+ pkg: "@zerotal/auth",
40
+ lines: [
41
+ "**Auth.** `Auth` from `@zerotal/auth` for the current user; policies go in `app/policies/` " +
42
+ "and are discovered automatically. Do not hand-roll password hashing or session handling.",
43
+ ],
44
+ },
45
+ {
46
+ pkg: "@zerotal/validator",
47
+ lines: [
48
+ "**Validation.** `FormRequest` subclasses in `app/requests/`, or `validate()` inline. " +
49
+ "Rules are built with `RuleBuilder`, so a typo is a compile error rather than a runtime one.",
50
+ ],
51
+ },
52
+ {
53
+ pkg: "@zerotal/flow",
54
+ lines: [
55
+ "**Flow components.** Server-rendered interactive components: a class with state fields and " +
56
+ "actions, rendered as JSX. Every component is an island and is fully interactive — there " +
57
+ "is no separate hydration step to write. `bun zt make:flow` scaffolds one.",
58
+ ],
59
+ },
60
+ {
61
+ pkg: "@zerotal/flow-ui",
62
+ lines: [
63
+ "**UI kit.** `@zerotal/flow-ui` ships the component set. Check it before writing a button, " +
64
+ "modal, table or form field from scratch.",
65
+ ],
66
+ },
67
+ {
68
+ pkg: "@zerotal/inertia",
69
+ lines: [
70
+ "**Inertia.** Pages are React/Vue components under `resources/`; controllers return " +
71
+ "`Inertia.render(...)`. Props are serialised — do not pass class instances.",
72
+ ],
73
+ },
74
+ {
75
+ pkg: "@zerotal/admin",
76
+ lines: [
77
+ "**Admin.** Resources in `app/admin/` describe tables, forms and infolists declaratively. " +
78
+ "`bun zt make:admin-resource` scaffolds one; prefer extending a resource over writing a " +
79
+ "bespoke page.",
80
+ ],
81
+ },
82
+ {
83
+ pkg: "@zerotal/queue",
84
+ lines: [
85
+ "**Background work.** Jobs in `app/jobs/`, dispatched with `.dispatch()`. Anything slower " +
86
+ "than a request belongs in one. `bun zt queue:work` runs the worker; `bun zt dev` runs it " +
87
+ "for you.",
88
+ ],
89
+ },
90
+ {
91
+ pkg: "@zerotal/cache",
92
+ lines: ["**Cache.** `Cache` from `@zerotal/cache`. Never cache a request-scoped value."],
93
+ },
94
+ {
95
+ pkg: "@zerotal/scheduler",
96
+ lines: [
97
+ "**Scheduling.** Schedules in `app/schedules/`, discovered at boot. They need " +
98
+ "`SchedulerProvider` registered — `doctor` will say so if it is missing.",
99
+ ],
100
+ },
101
+ {
102
+ pkg: "@zerotal/notifications",
103
+ lines: [
104
+ "**Notifications.** Notification classes in `app/notifications/`, one `via()` per channel.",
105
+ ],
106
+ },
107
+ {
108
+ pkg: "@zerotal/media",
109
+ lines: ["**Media.** `@zerotal/media` owns uploads, conversions and the media library."],
110
+ },
111
+ {
112
+ pkg: "@zerotal/broadcasting",
113
+ lines: [
114
+ "**Realtime.** Channels are declared in the app and authorised server-side. " +
115
+ "`bun zt make:channel` scaffolds one.",
116
+ ],
117
+ },
118
+ {
119
+ pkg: "@zerotal/i18n",
120
+ lines: ["**Translations.** `@zerotal/i18n`; message files live under `resources/lang/`."],
121
+ },
122
+ {
123
+ pkg: "@zerotal/tenancy",
124
+ lines: [
125
+ "**Multi-tenancy.** Tenant resolution is middleware-driven. Anything tenant-scoped must go " +
126
+ "through it rather than reading an id from the request.",
127
+ ],
128
+ },
129
+ {
130
+ pkg: "@zerotal/testing",
131
+ lines: [
132
+ "**Tests.** `createTestApp()` from `@zerotal/testing` boots a real app; factories and fakes " +
133
+ "come from the same package. Tests run on `bun test` via `bun zt test`.",
134
+ ],
135
+ },
136
+ ];
137
+
138
+ export interface GuidelineOptions {
139
+ /** Installed `@zerotal/*` package names, as reported by `app_info`. */
140
+ packages: string[];
141
+ /** The key the MCP server is registered under, so the text names it correctly. */
142
+ serverName: string;
143
+ }
144
+
145
+ /**
146
+ * The preamble written above the managed block when the file is created.
147
+ *
148
+ * Once written it is never touched again — it exists to tell whoever opens the
149
+ * file that the fenced part is generated and the rest is theirs.
150
+ */
151
+ export function agentsPreamble(): string {
152
+ return [
153
+ "# Agent instructions",
154
+ "",
155
+ "Guidance for coding agents working in this repository.",
156
+ "",
157
+ "Everything between the `zerotal:arch` markers below is generated by",
158
+ "`bun zt arch:update` and will be replaced on the next framework upgrade.",
159
+ "Write your own project-specific instructions outside that block — they are",
160
+ "preserved.",
161
+ ].join("\n");
162
+ }
163
+
164
+ /** Build the managed block for `AGENTS.md`. */
165
+ export function buildGuidelines(options: GuidelineOptions): string {
166
+ const installed = new Set(options.packages);
167
+ const sections: string[] = [
168
+ "## Zerotal",
169
+ "",
170
+ "This is a Zerotal app: a Bun-native, full-stack TypeScript framework. Bun executes",
171
+ "`.ts` directly — there is no build step, and what you write is what runs.",
172
+ "",
173
+ toolSection(options.serverName),
174
+ "",
175
+ conventionSection(),
176
+ "",
177
+ commandSection(),
178
+ ];
179
+
180
+ const blocks = PACKAGE_BLOCKS.filter((block) => installed.has(block.pkg));
181
+ if (blocks.length > 0) {
182
+ sections.push(
183
+ "",
184
+ "### What this app has",
185
+ "",
186
+ ...blocks.flatMap((block) => [...block.lines, ""]),
187
+ );
188
+ }
189
+
190
+ sections.push(rulesSection());
191
+
192
+ return sections
193
+ .join("\n")
194
+ .replace(/\n{3,}/g, "\n\n")
195
+ .trim();
196
+ }
197
+
198
+ function toolSection(serverName: string): string {
199
+ return [
200
+ `### Use the \`${serverName}\` MCP tools`,
201
+ "",
202
+ "This project exposes the framework's machine-readable truth as MCP tools. Prefer them",
203
+ "over recalling an API from memory — they describe the versions installed here.",
204
+ "",
205
+ "- `app_info` — versions, providers and package maturity. Worth one call at the start.",
206
+ "- `api_surface` — the exact signature of every export in a package.",
207
+ "- `search_docs` — the framework documentation for this installed version.",
208
+ "- `routes` — the routes actually registered, with their names.",
209
+ "- `schema` — what the models declare: tables, columns, indexes.",
210
+ "- `logs` / `last_error` — what the app actually did.",
211
+ "- `baselines` — the quality ratchets and the commands that check them.",
212
+ "- `doctor` — run it before calling any task done.",
213
+ ].join("\n");
214
+ }
215
+
216
+ function conventionSection(): string {
217
+ return [
218
+ "### Conventions",
219
+ "",
220
+ "Classes are discovered by directory. Put a file in the right place and it registers",
221
+ "itself; no manifest to update.",
222
+ "",
223
+ "```text",
224
+ "app/controllers/ app/models/ app/middleware/ app/policies/",
225
+ "app/jobs/ app/events/ app/listeners/ app/observers/",
226
+ "app/commands/ app/schedules/ app/requests/ app/notifications/",
227
+ "routes/ config/ database/migrations/ tests/",
228
+ "```",
229
+ "",
230
+ "Imports follow one rule: the kernel comes from `zerotal` (and `zerotal/routes`,",
231
+ "`zerotal/http`, …), everything else from its own package.",
232
+ "",
233
+ "```ts",
234
+ 'import type { HttpContext } from "zerotal";',
235
+ 'import { route } from "zerotal/routes";',
236
+ 'import { DB } from "@zerotal/orm";',
237
+ "```",
238
+ ].join("\n");
239
+ }
240
+
241
+ function commandSection(): string {
242
+ return [
243
+ "### Commands",
244
+ "",
245
+ "```bash",
246
+ "bun zt dev # server plus every companion process, in one deck",
247
+ "bun zt list # every command this app has, including package ones",
248
+ "bun zt make:<thing> # scaffold — prefer it over writing boilerplate by hand",
249
+ "bun zt route:list # registered routes",
250
+ "bun zt route:types # regenerate typed route names after changing routes",
251
+ "bun zt test # tests, with the app booted",
252
+ "bun zt doctor # health checks, with a fix beside each finding",
253
+ "```",
254
+ ].join("\n");
255
+ }
256
+
257
+ function rulesSection(): string {
258
+ return [
259
+ "",
260
+ "### Rules",
261
+ "",
262
+ "1. **The types are the guidance.** If a call type-checks it is almost certainly the",
263
+ " intended one. Read the signature with `api_surface` rather than guessing and",
264
+ " correcting.",
265
+ "2. **Scaffold before you write.** `bun zt make:*` produces the shape the framework",
266
+ " expects, including the parts that are easy to forget.",
267
+ "3. **Regenerate route types** with `bun zt route:types` whenever routes change —",
268
+ " `route()` is checked against the generated names.",
269
+ "4. **Finish with `doctor`.** A task with a failing check is not done. Warnings are",
270
+ " worth reading; failures are work.",
271
+ "5. **Do not add dependencies** without being asked. The framework covers most of",
272
+ " what an app needs, and `search_docs` will say where.",
273
+ ].join("\n");
274
+ }
275
+
276
+ /**
277
+ * The `CLAUDE.md` shim.
278
+ *
279
+ * An import line rather than a copy: Claude Code does not read `AGENTS.md`
280
+ * natively, and two files of guidance that start identical do not stay that way.
281
+ */
282
+ export function claudeShim(): string {
283
+ return [
284
+ "@AGENTS.md",
285
+ "",
286
+ "The instructions for this project live in `AGENTS.md` — the cross-tool standard, read",
287
+ "natively by most other agents. The line above imports it. Add anything Claude-specific",
288
+ "below, outside the generated block.",
289
+ ].join("\n");
290
+ }