create-nifra 3.5.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +3 -3
  2. package/dist/agent-files.d.ts +41 -26
  3. package/dist/agent-files.d.ts.map +1 -1
  4. package/dist/agent-files.js +80 -47
  5. package/dist/agent-files.js.map +1 -1
  6. package/dist/agents.d.ts +3 -3
  7. package/dist/agents.d.ts.map +1 -1
  8. package/dist/agents.js +65 -69
  9. package/dist/agents.js.map +1 -1
  10. package/dist/auth.d.ts +5 -4
  11. package/dist/auth.d.ts.map +1 -1
  12. package/dist/auth.js +27 -14
  13. package/dist/auth.js.map +1 -1
  14. package/dist/cli.d.ts +23 -25
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +141 -130
  17. package/dist/cli.js.map +1 -1
  18. package/dist/db.d.ts.map +1 -1
  19. package/dist/db.js +49 -48
  20. package/dist/db.js.map +1 -1
  21. package/dist/scaffold/frameworks.d.ts +13 -38
  22. package/dist/scaffold/frameworks.d.ts.map +1 -1
  23. package/dist/scaffold/frameworks.js +9 -32
  24. package/dist/scaffold/frameworks.js.map +1 -1
  25. package/dist/scaffold/route-types.d.ts +9 -0
  26. package/dist/scaffold/route-types.d.ts.map +1 -0
  27. package/dist/scaffold/route-types.js +61 -0
  28. package/dist/scaffold/route-types.js.map +1 -0
  29. package/dist/scaffold/site-files.d.ts +14 -8
  30. package/dist/scaffold/site-files.d.ts.map +1 -1
  31. package/dist/scaffold/site-files.js +29 -60
  32. package/dist/scaffold/site-files.js.map +1 -1
  33. package/dist/scaffold/site.d.ts +8 -5
  34. package/dist/scaffold/site.d.ts.map +1 -1
  35. package/dist/scaffold/site.js +47 -34
  36. package/dist/scaffold/site.js.map +1 -1
  37. package/dist/scaffold/targets.d.ts +39 -0
  38. package/dist/scaffold/targets.d.ts.map +1 -0
  39. package/dist/scaffold/targets.js +116 -0
  40. package/dist/scaffold/targets.js.map +1 -0
  41. package/package.json +4 -1
  42. package/src/agent-files.ts +89 -47
  43. package/template/README.md +12 -6
  44. package/template/{src → backend}/app.test.ts +2 -2
  45. package/template/{src → backend}/app.ts +8 -3
  46. package/template/{src → backend}/index.ts +2 -2
  47. package/template/{src → backend}/routes.ts +1 -1
  48. package/template/nifra.assurance.ts +16 -14
  49. package/template/package.json +7 -7
  50. package/template/tsconfig.json +1 -1
  51. package/template-batteries/README.md +10 -6
  52. package/template-batteries/{src → backend}/app.test.ts +23 -23
  53. package/template-batteries/{src → backend}/app.ts +8 -3
  54. package/template-batteries/{src → backend}/index.ts +2 -2
  55. package/template-batteries/{src → backend}/notes.ts +1 -1
  56. package/template-batteries/nifra.assurance.ts +16 -14
  57. package/template-batteries/package.json +10 -9
  58. package/template-batteries/tsconfig.json +1 -1
  59. package/template-isr/README.md +3 -0
  60. package/template-isr/{backend.ts → backend/app.ts} +2 -0
  61. package/template-isr/{server.ts → backend/dev-server.ts} +4 -4
  62. package/template-isr/{worker.ts → backend/worker.ts} +4 -2
  63. package/template-isr/build.ts +3 -1
  64. package/template-isr/gitignore +7 -1
  65. package/template-isr/nifra.assurance.ts +15 -13
  66. package/template-isr/package.json +10 -9
  67. package/template-isr/routes/index.backend.ts +14 -0
  68. package/template-isr/routes/index.tsx +5 -17
  69. package/template-isr/tsconfig.json +5 -4
  70. package/template-isr/wrangler.toml +6 -1
  71. package/template-site/README.md +24 -23
  72. package/template-site/{backend.ts → backend/app.ts} +6 -4
  73. package/template-site/gitignore +5 -10
  74. package/template-site/nifra.assurance.ts +15 -13
  75. package/template-site/nifra.config.ts +4 -4
  76. package/template-site/routes/index.backend.ts +18 -0
  77. package/template-site/routes/index.tsx +2 -15
  78. package/template-site-preact/README.md +21 -20
  79. package/template-site-preact/nifra.config.ts +4 -4
  80. package/template-site-preact/routes/_layout.tsx +3 -1
  81. package/template-site-preact/routes/index.tsx +2 -15
  82. package/template-site-solid/README.md +21 -19
  83. package/template-site-solid/nifra.config.ts +5 -4
  84. package/template-site-solid/routes/index.tsx +2 -15
  85. package/template-site-svelte/README.md +21 -19
  86. package/template-site-svelte/nifra.config.ts +3 -2
  87. package/template-site-svelte/routes/index.svelte +3 -16
  88. package/template-site-vue/README.md +21 -20
  89. package/template-site-vue/nifra.config.ts +4 -3
  90. package/template-site-vue/routes/_layout.vue +21 -25
  91. package/template-site-vue/routes/index.vue +12 -18
  92. package/dist/scaffold/site-build.d.ts +0 -60
  93. package/dist/scaffold/site-build.d.ts.map +0 -1
  94. package/dist/scaffold/site-build.js +0 -198
  95. package/dist/scaffold/site-build.js.map +0 -1
  96. package/template-site/.dockerignore +0 -14
  97. package/template-site/Dockerfile +0 -25
  98. package/template-site/_worker.ts +0 -17
  99. package/template-site/deno.json +0 -7
  100. package/template-site/server-bun.ts +0 -35
  101. package/template-site/server-deno.ts +0 -38
  102. package/template-site/server-node.ts +0 -40
  103. package/template-site/server-vercel.ts +0 -17
  104. package/template-site/wrangler.toml +0 -4
  105. /package/template-isr/{page.ts → backend/page.ts} +0 -0
  106. /package/template-site/{counter.ts → backend/counter.ts} +0 -0
package/README.md CHANGED
@@ -11,11 +11,11 @@ bun create nifra my-app
11
11
 
12
12
  `--template <name>` (default `api`):
13
13
 
14
- - **`api`** - a typed nifra server (`src/app.ts` + `src/index.ts`) with an example test and
14
+ - **`api`** - a typed nifra server (`backend/app.ts` + `backend/index.ts`) with an example test and
15
15
  `dev`/`start`/`test`/`typecheck` scripts.
16
16
  - **`site`** - the full-stack template: a nifra + React SSR site (file-routed frontend + typed
17
- `backend.ts`), one source deployable to Cloudflare Pages, Node, Deno, or Vercel Edge. Pick the
18
- frontend with `--framework react|preact|vue|solid|svelte`.
17
+ `backend/app.ts`) for one deploy target, `--target bun|node|deno|cloudflare|vercel` (default `bun`).
18
+ Pick the frontend with `--framework react|preact|vue|solid|svelte`.
19
19
  - **`isr`** - a nifra + React app with **Incremental Static Regeneration** on Cloudflare Workers + KV
20
20
  (pages cached + served stale-while-revalidate; on-demand purge endpoint).
21
21
  - **`batteries`** - the `api` starter plus the batteries a real API needs: background jobs
@@ -1,25 +1,24 @@
1
1
  /**
2
- * The canonical generators for the agent-discovery files an app ships so a coding agent auto-discovers
3
- * the project's nifra MCP server and prefers it over writing nifra from memory:
2
+ * The canonical generators for the agent files an app ships. `AGENTS.md` is the single source of agent
3
+ * guidance; every other agent's file is a thin pointer to it, so no two copies can drift:
4
4
  *
5
- * .mcp.json - Claude Code's project MCP registry ({ mcpServers: { nifra: { command, args } } })
6
- * .cursor/mcp.json - Cursor's MCP registry (same server config)
7
- * CLAUDE.md - Claude's preamble (use the MCP first; gate on `nifra check`) + `@AGENTS.md` import
8
- * AGENTS.md "MCP" - a section for non-Claude agents pointing at the same server
5
+ * AGENTS.md - the guidance (read natively by Codex, Cursor, Copilot and others)
6
+ * CLAUDE.md - Claude Code: `@AGENTS.md` import
7
+ * GEMINI.md - Gemini CLI: `@./AGENTS.md` import
8
+ * .cursor/rules/nifra.mdc - Cursor: an always-applied rule pointing at AGENTS.md
9
+ * .github/copilot-instructions.md - GitHub Copilot: points at AGENTS.md
10
+ * .mcp.json, .cursor/mcp.json - the nifra MCP server, registered for Claude Code and Cursor
9
11
  *
10
- * Single source of truth: both `create-nifra` (scaffold time) and `@nifrajs/cli`'s `nifra init-agents`
11
- * (retrofit an existing app) import these, so the four files can never drift apart. This module is
12
- * dependency-free (pure string + object generation) on purpose - `create-nifra` ships with no runtime
13
- * deps, and `@nifrajs/cli` imports it as a workspace dependency.
12
+ * Both `create-nifra` (scaffold time) and `@nifrajs/cli`'s `nifra init-agents` (retrofit an existing app)
13
+ * import these, so the files can never drift apart. This module is dependency-free (pure string + object
14
+ * generation) on purpose - `create-nifra` ships with no runtime deps, and `@nifrajs/cli` imports it as a
15
+ * workspace dependency.
14
16
  *
15
17
  * Why `bunx @nifrajs/cli mcp` (not `bunx nifra mcp`): the `nifra` binary is provided by the
16
- * `@nifrajs/cli` package. A scaffolded `site` app carries `@nifrajs/cli` as a devDependency, so either
17
- * spelling would resolve `node_modules/.bin/nifra` locally - BUT the `api`/`isr` templates do NOT carry
18
- * `@nifrajs/cli`, and the bare npm package literally named `nifra` (this monorepo's `@nifrajs/web` shim)
19
- * exposes NO `nifra` bin. So `bunx nifra mcp` would fetch the wrong package and fail there. Naming the
20
- * bin-owning package, `bunx @nifrajs/cli mcp`, resolves the locally-installed bin when present and
21
- * otherwise fetches the package that actually provides the `nifra` command - robust across every
22
- * template and for any existing app.
18
+ * `@nifrajs/cli` package, and the bare npm package literally named `nifra` (this monorepo's
19
+ * `@nifrajs/web` shim) exposes NO `nifra` bin, so `bunx nifra mcp` would fetch the wrong package in an
20
+ * app that does not carry `@nifrajs/cli`. Naming the bin-owning package resolves the locally-installed
21
+ * bin when present and otherwise fetches the package that actually provides the `nifra` command.
23
22
  */
24
23
  /** The MCP launch command, shared by `.mcp.json` and `.cursor/mcp.json`. See the module header for why
25
24
  * the package is named explicitly rather than relying on the bare `nifra` bin. */
@@ -43,7 +42,8 @@ export declare const MCP_SERVER_COMMAND: "bunx";
43
42
  * keeps respawning the stale binary even after a newer one is published (the MCP server silently runs
44
43
  * old code). Pinning the exact version makes the version part of the cache key, so each release fetches
45
44
  * fresh and a stale cache can never shadow it. The cost: an already-scaffolded app's `.mcp.json` freezes
46
- * at its scaffold-time version until `nifra init-agents` is re-run - an acceptable, deterministic trade.
45
+ * at its scaffold-time version until `nifra init-agents --sync-mcp` re-pins it to the nifra the project
46
+ * installs - an acceptable, deterministic trade.
47
47
  */
48
48
  export declare const MCP_CLI_VERSION: string;
49
49
  export declare const MCP_SERVER_ARGS: readonly [`@nifrajs/cli@${string}`, "mcp"];
@@ -61,20 +61,27 @@ export declare const MCP_CONFIG: McpConfig;
61
61
  /** Serialize the canonical MCP config as the JSON written to `.mcp.json` and `.cursor/mcp.json`.
62
62
  * Trailing newline so the file is POSIX-clean and diffs don't flag a missing EOL. */
63
63
  export declare function mcpJson(): string;
64
- /**
65
- * `CLAUDE.md` - Claude Code reads this automatically. It is deliberately NOT a copy of `AGENTS.md`:
66
- * a short preamble that (1) tells Claude this project ships a nifra MCP, registered in `.mcp.json`, and
67
- * to PREFER it, and (2) pulls in the full cookbook with Claude Code's `@file` import directive on its
68
- * own line - `@AGENTS.md` - so the conventions live in exactly one place and can't drift between the
69
- * two files. Keep this short; the depth is in `AGENTS.md`.
70
- */
64
+ /** `CLAUDE.md`: Claude Code expands the `@AGENTS.md` import in place. */
71
65
  export declare function claudeMd(): string;
66
+ /** `GEMINI.md`: Gemini CLI expands `@./AGENTS.md` imports in its context files. */
67
+ export declare function geminiMd(): string;
68
+ /** `.cursor/rules/nifra.mdc`: an always-applied Cursor rule; `@AGENTS.md` attaches the file. */
69
+ export declare function cursorRule(): string;
70
+ /** `.github/copilot-instructions.md`: Copilot has no import syntax, so this names the file. */
71
+ export declare function copilotInstructions(): string;
72
72
  /**
73
73
  * The "## MCP server" section appended to a scaffolded (or retrofitted) `AGENTS.md`, so non-Claude
74
74
  * agents (Cursor, and anything that reads `AGENTS.md`) also learn the MCP exists and what to prefer.
75
75
  * Mirrors the CLAUDE.md preamble's guidance without the Claude-specific `@import`.
76
76
  */
77
77
  export declare function agentsMcpSection(): string;
78
+ /**
79
+ * The "## Project structure" section of a web app's `AGENTS.md`: the zones the build enforces. Shared so
80
+ * a scaffolded app and one `nifra init-agents` retrofits teach the same rules.
81
+ */
82
+ export declare function agentsStructureSection(): string;
83
+ /** The heading `nifra init-agents` looks for before appending {@link agentsStructureSection}. */
84
+ export declare const STRUCTURE_HEADING = "## Project structure";
78
85
  /** Identifies a generated agent-discovery file: where it goes (relative to the project root) and how to
79
86
  * produce its content. `merge` is for files that augment an existing one (AGENTS.md) rather than own it. */
80
87
  export interface AgentFileSpec {
@@ -84,9 +91,17 @@ export interface AgentFileSpec {
84
91
  readonly label: string;
85
92
  }
86
93
  /** The standalone files this module fully owns (whole-file generators). AGENTS.md is handled separately
87
- * because create-nifra builds it from `agents.ts` and the retrofit command appends a section to it. */
94
+ * because create-nifra builds it from `agents.ts` and the retrofit command appends sections to it. */
88
95
  export declare const MCP_JSON_PATH = ".mcp.json";
89
96
  export declare const CURSOR_MCP_JSON_PATH = ".cursor/mcp.json";
90
97
  export declare const CLAUDE_MD_PATH = "CLAUDE.md";
98
+ export declare const GEMINI_MD_PATH = "GEMINI.md";
99
+ export declare const CURSOR_RULE_PATH = ".cursor/rules/nifra.mdc";
100
+ export declare const COPILOT_INSTRUCTIONS_PATH = ".github/copilot-instructions.md";
91
101
  export declare const AGENTS_MD_PATH = "AGENTS.md";
102
+ /** Every agent's pointer to `AGENTS.md`, in the order they are written and reported. */
103
+ export declare const AGENT_POINTERS: readonly {
104
+ readonly path: string;
105
+ readonly content: () => string;
106
+ }[];
92
107
  //# sourceMappingURL=agent-files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-files.d.ts","sourceRoot":"","sources":["../src/agent-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH;kFACkF;AAClF,eAAO,MAAM,kBAAkB,EAAG,MAAe,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,eAAe,EAAE,MAIrB,CAAA;AACT,eAAO,MAAM,eAAe,4CAAsD,CAAA;AAElF,uGAAuG;AACvG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CACjC;AAED,mFAAmF;AACnF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAA;CAC/D;AAED,2FAA2F;AAC3F,eAAO,MAAM,UAAU,EAAE,SAIxB,CAAA;AAED;qFACqF;AACrF,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,IAAI,MAAM,CAsBjC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAczC;AAED;4GAC4G;AAC5G,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED;uGACuG;AACvG,eAAO,MAAM,aAAa,cAAc,CAAA;AACxC,eAAO,MAAM,oBAAoB,qBAAqB,CAAA;AACtD,eAAO,MAAM,cAAc,cAAc,CAAA;AACzC,eAAO,MAAM,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"agent-files.d.ts","sourceRoot":"","sources":["../src/agent-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH;kFACkF;AAClF,eAAO,MAAM,kBAAkB,EAAG,MAAe,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,eAAe,EAAE,MAIrB,CAAA;AACT,eAAO,MAAM,eAAe,4CAAsD,CAAA;AAElF,uGAAuG;AACvG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CACjC;AAED,mFAAmF;AACnF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAA;CAC/D;AAED,2FAA2F;AAC3F,eAAO,MAAM,UAAU,EAAE,SAIxB,CAAA;AAED;qFACqF;AACrF,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAKD,yEAAyE;AACzE,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,gGAAgG;AAChG,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,+FAA+F;AAC/F,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAczC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA6B/C;AAED,iGAAiG;AACjG,eAAO,MAAM,iBAAiB,yBAAyB,CAAA;AAEvD;4GAC4G;AAC5G,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED;sGACsG;AACtG,eAAO,MAAM,aAAa,cAAc,CAAA;AACxC,eAAO,MAAM,oBAAoB,qBAAqB,CAAA;AACtD,eAAO,MAAM,cAAc,cAAc,CAAA;AACzC,eAAO,MAAM,cAAc,cAAc,CAAA;AACzC,eAAO,MAAM,gBAAgB,4BAA4B,CAAA;AACzD,eAAO,MAAM,yBAAyB,oCAAoC,CAAA;AAC1E,eAAO,MAAM,cAAc,cAAc,CAAA;AAEzC,wFAAwF;AACxF,eAAO,MAAM,cAAc,EAAE,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAA;CAAE,EAM5F,CAAA"}
@@ -1,25 +1,24 @@
1
1
  /**
2
- * The canonical generators for the agent-discovery files an app ships so a coding agent auto-discovers
3
- * the project's nifra MCP server and prefers it over writing nifra from memory:
2
+ * The canonical generators for the agent files an app ships. `AGENTS.md` is the single source of agent
3
+ * guidance; every other agent's file is a thin pointer to it, so no two copies can drift:
4
4
  *
5
- * .mcp.json - Claude Code's project MCP registry ({ mcpServers: { nifra: { command, args } } })
6
- * .cursor/mcp.json - Cursor's MCP registry (same server config)
7
- * CLAUDE.md - Claude's preamble (use the MCP first; gate on `nifra check`) + `@AGENTS.md` import
8
- * AGENTS.md "MCP" - a section for non-Claude agents pointing at the same server
5
+ * AGENTS.md - the guidance (read natively by Codex, Cursor, Copilot and others)
6
+ * CLAUDE.md - Claude Code: `@AGENTS.md` import
7
+ * GEMINI.md - Gemini CLI: `@./AGENTS.md` import
8
+ * .cursor/rules/nifra.mdc - Cursor: an always-applied rule pointing at AGENTS.md
9
+ * .github/copilot-instructions.md - GitHub Copilot: points at AGENTS.md
10
+ * .mcp.json, .cursor/mcp.json - the nifra MCP server, registered for Claude Code and Cursor
9
11
  *
10
- * Single source of truth: both `create-nifra` (scaffold time) and `@nifrajs/cli`'s `nifra init-agents`
11
- * (retrofit an existing app) import these, so the four files can never drift apart. This module is
12
- * dependency-free (pure string + object generation) on purpose - `create-nifra` ships with no runtime
13
- * deps, and `@nifrajs/cli` imports it as a workspace dependency.
12
+ * Both `create-nifra` (scaffold time) and `@nifrajs/cli`'s `nifra init-agents` (retrofit an existing app)
13
+ * import these, so the files can never drift apart. This module is dependency-free (pure string + object
14
+ * generation) on purpose - `create-nifra` ships with no runtime deps, and `@nifrajs/cli` imports it as a
15
+ * workspace dependency.
14
16
  *
15
17
  * Why `bunx @nifrajs/cli mcp` (not `bunx nifra mcp`): the `nifra` binary is provided by the
16
- * `@nifrajs/cli` package. A scaffolded `site` app carries `@nifrajs/cli` as a devDependency, so either
17
- * spelling would resolve `node_modules/.bin/nifra` locally - BUT the `api`/`isr` templates do NOT carry
18
- * `@nifrajs/cli`, and the bare npm package literally named `nifra` (this monorepo's `@nifrajs/web` shim)
19
- * exposes NO `nifra` bin. So `bunx nifra mcp` would fetch the wrong package and fail there. Naming the
20
- * bin-owning package, `bunx @nifrajs/cli mcp`, resolves the locally-installed bin when present and
21
- * otherwise fetches the package that actually provides the `nifra` command - robust across every
22
- * template and for any existing app.
18
+ * `@nifrajs/cli` package, and the bare npm package literally named `nifra` (this monorepo's
19
+ * `@nifrajs/web` shim) exposes NO `nifra` bin, so `bunx nifra mcp` would fetch the wrong package in an
20
+ * app that does not carry `@nifrajs/cli`. Naming the bin-owning package resolves the locally-installed
21
+ * bin when present and otherwise fetches the package that actually provides the `nifra` command.
23
22
  */
24
23
  import { readFileSync } from "node:fs";
25
24
  /** The MCP launch command, shared by `.mcp.json` and `.cursor/mcp.json`. See the module header for why
@@ -44,7 +43,8 @@ export const MCP_SERVER_COMMAND = "bunx";
44
43
  * keeps respawning the stale binary even after a newer one is published (the MCP server silently runs
45
44
  * old code). Pinning the exact version makes the version part of the cache key, so each release fetches
46
45
  * fresh and a stale cache can never shadow it. The cost: an already-scaffolded app's `.mcp.json` freezes
47
- * at its scaffold-time version until `nifra init-agents` is re-run - an acceptable, deterministic trade.
46
+ * at its scaffold-time version until `nifra init-agents --sync-mcp` re-pins it to the nifra the project
47
+ * installs - an acceptable, deterministic trade.
48
48
  */
49
49
  export const MCP_CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
50
50
  export const MCP_SERVER_ARGS = [`@nifrajs/cli@${MCP_CLI_VERSION}`, "mcp"];
@@ -59,35 +59,22 @@ export const MCP_CONFIG = {
59
59
  export function mcpJson() {
60
60
  return `${JSON.stringify(MCP_CONFIG, null, 2)}\n`;
61
61
  }
62
- /**
63
- * `CLAUDE.md` - Claude Code reads this automatically. It is deliberately NOT a copy of `AGENTS.md`:
64
- * a short preamble that (1) tells Claude this project ships a nifra MCP, registered in `.mcp.json`, and
65
- * to PREFER it, and (2) pulls in the full cookbook with Claude Code's `@file` import directive on its
66
- * own line - `@AGENTS.md` - so the conventions live in exactly one place and can't drift between the
67
- * two files. Keep this short; the depth is in `AGENTS.md`.
68
- */
62
+ const POINTER_INTRO = "This project's agent guidance - commands, the project structure, the rules, and the nifra MCP server - lives in AGENTS.md, the single source every agent reads.";
63
+ /** `CLAUDE.md`: Claude Code expands the `@AGENTS.md` import in place. */
69
64
  export function claudeMd() {
70
- return `# CLAUDE.md
71
-
72
- This project ships a **nifra MCP server**, registered for Claude Code in \`.mcp.json\` (it launches with
73
- \`${MCP_SERVER_COMMAND} ${MCP_SERVER_ARGS.join(" ")}\`). **Prefer the MCP tools** over writing nifra from memory - they are
74
- typechecked against *this* project and this installed version, so they beat training-data recall:
75
-
76
- - \`nifra_docs\` / \`nifra_example\` - exact signatures + verified, compiling snippets. Reach for these
77
- before hand-writing a route, a loader, or a client call; a remembered API is often stale or wrong.
78
- - \`nifra_context\` - this project's route index + conventions, so changes fit the existing surface.
79
- - \`nifra_types\` - the EXACT TypeScript of any \`@nifrajs/*\` symbol (e.g. \`nifra_types({ name: "RateLimitStore" })\`).
80
- Call it for a precise type; **never read \`node_modules/@nifrajs/**/*.d.ts\`** - the tool is the authoritative, complete source.
81
- - **\`nifra_check\` is the done-gate.** Before declaring any change complete, run \`nifra_check\` (or
82
- \`nifra check --json\` in a terminal). It typechecks the frontend↔backend contract and flags drift
83
- (hand-rolled \`fetch()\` to this app's own API, server-only imports in \`routes/\`). A failing check
84
- means the work isn't done - fix it, don't ship around it.
85
-
86
- The full nifra cookbook for this app - backend rules, the typed never-throwing client, file routing, and
87
- the gotchas - lives in \`AGENTS.md\`, imported here so it stays the single source of truth:
88
-
89
- @AGENTS.md
90
- `;
65
+ return `# CLAUDE.md\n\n${POINTER_INTRO}\n\n@AGENTS.md\n`;
66
+ }
67
+ /** `GEMINI.md`: Gemini CLI expands `@./AGENTS.md` imports in its context files. */
68
+ export function geminiMd() {
69
+ return `# GEMINI.md\n\n${POINTER_INTRO}\n\n@./AGENTS.md\n`;
70
+ }
71
+ /** `.cursor/rules/nifra.mdc`: an always-applied Cursor rule; `@AGENTS.md` attaches the file. */
72
+ export function cursorRule() {
73
+ return `---\ndescription: nifra project guidance\nalwaysApply: true\n---\n\n${POINTER_INTRO} Read it before changing code:\n\n@AGENTS.md\n`;
74
+ }
75
+ /** `.github/copilot-instructions.md`: Copilot has no import syntax, so this names the file. */
76
+ export function copilotInstructions() {
77
+ return `${POINTER_INTRO} Read AGENTS.md at the repository root before changing code, and follow it.\n`;
91
78
  }
92
79
  /**
93
80
  * The "## MCP server" section appended to a scaffolded (or retrofitted) `AGENTS.md`, so non-Claude
@@ -109,10 +96,56 @@ project + the installed version, so they beat training-data recall:
109
96
  - \`nifra_check\` - the done-gate (typecheck + drift lints). Run it before calling work complete; a
110
97
  failing check means it isn't done. (\`nifra check --json\` in a terminal does the same.)`;
111
98
  }
99
+ /**
100
+ * The "## Project structure" section of a web app's `AGENTS.md`: the zones the build enforces. Shared so
101
+ * a scaffolded app and one `nifra init-agents` retrofits teach the same rules.
102
+ */
103
+ export function agentsStructureSection() {
104
+ return `${STRUCTURE_HEADING}
105
+
106
+ Every file a build loads belongs to one side, and the build refuses an import that crosses the wrong
107
+ way - it fails naming the import chain rather than shipping server code to the browser.
108
+
109
+ | Where | Holds | Reaches the browser |
110
+ |---|---|---|
111
+ | \`routes/x.tsx\` (\`.svelte\`, \`.vue\`, \`.mdx\`) | a page: the component, \`meta\` | yes |
112
+ | \`routes/x.backend.ts\` | that page's \`loader\`, \`action\`, \`loaderOutput\`, \`actionOutput\`, \`middleware\` | never |
113
+ | \`frontend/\` | components, hooks, browser-only code | yes |
114
+ | \`backend/\` | \`app.ts\` (the API), \`framework.ts\`, the database, auth, secrets | never |
115
+ | \`shared/\` | schemas, types and pure helpers both sides import | yes |
116
+ | \`public/\` | static files served as they are | yes |
117
+
118
+ A file outside those folders joins a side with a \`.frontend.ts\`, \`.backend.ts\` or \`.shared.ts\` suffix.
119
+
120
+ - **A route is two files.** \`routes/blog/[slug].tsx\` renders; \`routes/blog/[slug].backend.ts\` loads.
121
+ A server-only export (\`loader\`, \`action\`, ...) in the \`.tsx\` file is a build error.
122
+ - **Frontend code never imports backend code** - not \`backend/\`, not a \`.backend.ts\` file, not a
123
+ database driver or \`node:\` built-in. Data reaches a page only as its loader's return value.
124
+ - **Every loader and action declares what it sends**: \`export const loaderOutput = t.object({ ... })\`
125
+ (and \`actionOutput\`). Only what the schema declares reaches the browser, and a loader without one
126
+ is refused.
127
+ - **Type a route with its generated \`./+types/<name>\`**: \`import type { Route } from "./+types/[slug]"\`
128
+ gives \`Route.LoaderArgs\` (with the typed \`api\`) in the backend half and \`Route.ComponentProps\` in
129
+ the page. \`nifra dev\`, \`nifra build\`, \`nifra check\` and \`nifra types\` write them.
130
+ - **Secrets stay in \`backend/\`.** A value only the server may see is read there (\`process.env\`), never
131
+ in a frontend file; a build that finds what looks like a credential in browser output fails.`;
132
+ }
133
+ /** The heading `nifra init-agents` looks for before appending {@link agentsStructureSection}. */
134
+ export const STRUCTURE_HEADING = "## Project structure";
112
135
  /** The standalone files this module fully owns (whole-file generators). AGENTS.md is handled separately
113
- * because create-nifra builds it from `agents.ts` and the retrofit command appends a section to it. */
136
+ * because create-nifra builds it from `agents.ts` and the retrofit command appends sections to it. */
114
137
  export const MCP_JSON_PATH = ".mcp.json";
115
138
  export const CURSOR_MCP_JSON_PATH = ".cursor/mcp.json";
116
139
  export const CLAUDE_MD_PATH = "CLAUDE.md";
140
+ export const GEMINI_MD_PATH = "GEMINI.md";
141
+ export const CURSOR_RULE_PATH = ".cursor/rules/nifra.mdc";
142
+ export const COPILOT_INSTRUCTIONS_PATH = ".github/copilot-instructions.md";
117
143
  export const AGENTS_MD_PATH = "AGENTS.md";
144
+ /** Every agent's pointer to `AGENTS.md`, in the order they are written and reported. */
145
+ export const AGENT_POINTERS = [
146
+ { path: CLAUDE_MD_PATH, content: claudeMd },
147
+ { path: GEMINI_MD_PATH, content: geminiMd },
148
+ { path: CURSOR_RULE_PATH, content: cursorRule },
149
+ { path: COPILOT_INSTRUCTIONS_PATH, content: copilotInstructions },
150
+ ];
118
151
  //# sourceMappingURL=agent-files.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-files.js","sourceRoot":"","sources":["../src/agent-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAEtC;kFACkF;AAClF,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAe,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,eAAe,GAC1B,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAG7E,CAAC,OAAO,CAAA;AACT,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,gBAAgB,eAAe,EAAE,EAAE,KAAK,CAAU,CAAA;AAalF,2FAA2F;AAC3F,MAAM,CAAC,MAAM,UAAU,GAAc;IACnC,UAAU,EAAE;QACV,KAAK,EAAE,EAAE,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,GAAG,eAAe,CAAC,EAAE;KACnE;CACF,CAAA;AAED;qFACqF;AACrF,MAAM,UAAU,OAAO;IACrB,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ;IACtB,OAAO;;;IAGL,kBAAkB,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;CAiBlD,CAAA;AACD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;+DAEsD,kBAAkB,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC;;;;;;;;;;2FAUnB,CAAA;AAC3F,CAAC;AAWD;uGACuG;AACvG,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAA;AACxC,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAA;AACtD,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAA;AACzC,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAA"}
1
+ {"version":3,"file":"agent-files.js","sourceRoot":"","sources":["../src/agent-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAEtC;kFACkF;AAClF,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAe,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,eAAe,GAC1B,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAG7E,CAAC,OAAO,CAAA;AACT,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,gBAAgB,eAAe,EAAE,EAAE,KAAK,CAAU,CAAA;AAalF,2FAA2F;AAC3F,MAAM,CAAC,MAAM,UAAU,GAAc;IACnC,UAAU,EAAE;QACV,KAAK,EAAE,EAAE,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,GAAG,eAAe,CAAC,EAAE;KACnE;CACF,CAAA;AAED;qFACqF;AACrF,MAAM,UAAU,OAAO;IACrB,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED,MAAM,aAAa,GACjB,iKAAiK,CAAA;AAEnK,yEAAyE;AACzE,MAAM,UAAU,QAAQ;IACtB,OAAO,kBAAkB,aAAa,kBAAkB,CAAA;AAC1D,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,QAAQ;IACtB,OAAO,kBAAkB,aAAa,oBAAoB,CAAA;AAC5D,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,UAAU;IACxB,OAAO,uEAAuE,aAAa,gDAAgD,CAAA;AAC7I,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,mBAAmB;IACjC,OAAO,GAAG,aAAa,+EAA+E,CAAA;AACxG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;+DAEsD,kBAAkB,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC;;;;;;;;;;2FAUnB,CAAA;AAC3F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,GAAG,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;+FA2BkE,CAAA;AAC/F,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAA;AAWvD;sGACsG;AACtG,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAA;AACxC,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAA;AACtD,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAA;AACzC,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAA;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,yBAAyB,CAAA;AACzD,MAAM,CAAC,MAAM,yBAAyB,GAAG,iCAAiC,CAAA;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAA;AAEzC,wFAAwF;AACxF,MAAM,CAAC,MAAM,cAAc,GACzB;IACE,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE;IAC3C,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE;IAC3C,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE;IAC/C,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,mBAAmB,EAAE;CAClE,CAAA"}
package/dist/agents.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Generate the `AGENTS.md` a scaffolded app ships with - the conventions file read by coding agents
3
3
  * (Claude Code, Cursor, …). It encodes nifra's non-obvious rules so an agent writes correct code from
4
- * the first prompt: validate at the boundary, the never-throwing client, `app.fetch` as the universal
5
- * entry, and (for full-stack apps) the one gotcha that bites everyone - never top-level-import
6
- * server-only code into a route module. Tailored per template so the paths + commands are real.
4
+ * the first prompt: validate at the boundary, the never-throwing client, `backend.fetch` as the universal
5
+ * entry, and (for full-stack apps) the frontend/backend zones the build enforces. Tailored per template so
6
+ * the paths + commands are real.
7
7
  */
8
8
  import type { AuthChoice } from "./auth.js";
9
9
  import type { Framework, TemplateName } from "./cli.js";
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,EAAc,KAAK,QAAQ,EAAiB,MAAM,SAAS,CAAA;AAkRlE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAA;IACtB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAA;CAC3B;AAED,+DAA+D;AAC/D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAuBtD"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,EAAc,KAAK,QAAQ,EAAiB,MAAM,SAAS,CAAA;AA8QlE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAA;IACtB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAA;CAC3B;AAED,+DAA+D;AAC/D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAuBtD"}
package/dist/agents.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Generate the `AGENTS.md` a scaffolded app ships with - the conventions file read by coding agents
3
3
  * (Claude Code, Cursor, …). It encodes nifra's non-obvious rules so an agent writes correct code from
4
- * the first prompt: validate at the boundary, the never-throwing client, `app.fetch` as the universal
5
- * entry, and (for full-stack apps) the one gotcha that bites everyone - never top-level-import
6
- * server-only code into a route module. Tailored per template so the paths + commands are real.
4
+ * the first prompt: validate at the boundary, the never-throwing client, `backend.fetch` as the universal
5
+ * entry, and (for full-stack apps) the frontend/backend zones the build enforces. Tailored per template so
6
+ * the paths + commands are real.
7
7
  */
8
- import { agentsMcpSection } from "./agent-files.js";
8
+ import { agentsMcpSection, agentsStructureSection } from "./agent-files.js";
9
9
  import { DB_PRESETS } from "./db.js";
10
10
  const FRAMEWORK_LABEL = {
11
11
  react: "React",
@@ -21,7 +21,7 @@ const API_RULES = `## The backend: \`server()\`
21
21
  import { server } from "@nifrajs/core/server"
22
22
  import { t } from "@nifrajs/schema"
23
23
 
24
- export const app = server()
24
+ export const backend = server()
25
25
  .get("/users/:id", { response: t.object({ id: t.string() }) }, (c) => ({ id: c.params.id }))
26
26
  .post(
27
27
  "/users",
@@ -33,19 +33,21 @@ export const app = server()
33
33
  },
34
34
  )
35
35
 
36
- export type App = typeof app
36
+ export type Backend = typeof backend
37
37
  \`\`\`
38
38
 
39
39
  Rules an agent must follow:
40
40
 
41
- - **Validate every input at the boundary.** The route schema slots are \`{ body, query }\` (plus
42
- \`response\`, below) - use \`t\` from \`@nifrajs/schema\` (installed) or any Standard Schema (zod, valibot).
43
- Read the typed, already-validated \`c.body\` / \`c.query\` (invalid input was rejected with a 422 before the
44
- handler ran). **Never** hand-parse \`await c.req.json()\` and poke at properties - that's the bug class the
45
- schema exists to remove.
46
- - **Path params are NOT a schema slot.** \`:id\` etc. are inferred from the path literal as \`string\` and
47
- read via \`c.params.id\`; there is no \`params\` (or \`headers\`) key in the route schema. Validate a param's
48
- shape inside the handler (a length/format check), not via \`{ params: ... }\` - that's a type error.
41
+ - **Validate every input at the boundary.** The route schema slots are \`{ body, query, params, headers,
42
+ cookies }\` (plus \`response\`, below) - use \`t\` from \`@nifrajs/schema\` (installed) or any Standard Schema
43
+ (zod, valibot). Read the typed, already-validated \`c.body\` / \`c.query\` / \`c.params\` / \`c.headers\` /
44
+ \`c.cookies\` (invalid input was rejected with a 422 before the handler ran). **Never** hand-parse
45
+ \`await c.req.json()\` and poke at properties - that's the bug class the schema exists to remove.
46
+ - **Path params are strings until a \`params\` schema says otherwise.** \`:id\` is inferred from the path
47
+ literal as a \`string\` on \`c.params.id\`. Declare \`{ params: t.object({ id: t.string({ format: "uuid" }) }) }\`
48
+ to reject a malformed id with a 422 before the handler runs, or \`t.query({ id: t.integer() })\` to coerce a
49
+ numeric one, instead of a format check inside the handler. The schema covers every path param. Header
50
+ names in a \`headers\` schema are lower-case.
49
51
  - **Lock the output shape with \`response\` (no drift).** Add \`{ response: t.object({...}) }\` to a route:
50
52
  the handler's return is type-checked against it, and the typed client sees exactly that shape. One
51
53
  contract, both sides - the frontend physically can't drift from the backend's output.
@@ -60,8 +62,9 @@ Rules an agent must follow:
60
62
  - **Raw \`Response\` is for redirects / streams / files** (\`Response.redirect(url, 302)\`, a file body, an
61
63
  SSE stream). The typed client infers \`res.data: never\` on such a route and \`nifra check\` emits a
62
64
  non-blocking \`response-route\` warning - both are EXPECTED there, not defects.
63
- - **\`app.fetch(request)\` is the whole app** - \`(Request) => Response | Promise<Response>\`. Tests drive it
64
- directly (\`await app.fetch(new Request("http://x/users/1"))\`); no server needs to be running.
65
+ - **\`backend.fetch(request)\` is the whole app** - \`(Request) => Response | Promise<Response>\`. Tests
66
+ drive it directly (\`await backend.fetch(new Request("http://x/users/1"))\`); no server needs to be
67
+ running.
65
68
  - **Don't reach for a heavy ORM/HTTP layer.** Routing, validation, cookies, and the typed client are
66
69
  built in. Parameterize DB queries; use \`timingSafeEqual\` (or WebCrypto) for secret comparison.
67
70
  - **Cross-cutting concerns - use \`@nifrajs/middleware\`, don't hand-roll.** Rate limiting (\`429\` +
@@ -73,30 +76,30 @@ Rules an agent must follow:
73
76
 
74
77
  \`\`\`ts
75
78
  import { client } from "@nifrajs/client"
76
- import type { app } from "./app" // type-only import - server code never ships to the client
79
+ import type { Backend } from "./backend/app" // type-only import - server code never ships
77
80
 
78
- const api = client<typeof app>("https://api.example.com")
81
+ const api = client<Backend>("https://api.example.com")
79
82
  const res = await api.users({ id: "42" }).get()
80
83
  if (res.ok) res.data // typed from the route's return type - zero codegen
81
84
  else res.error // errors are returned, never thrown
82
85
  \`\`\`
83
86
 
84
87
  **Never hand-roll \`fetch\` + ad-hoc response types for this app's own API** - that's exactly how a screen
85
- drifts from the backend. \`client<typeof app>\` derives both the request inputs and \`res.data\` from the
88
+ drifts from the backend. \`client<Backend>\` derives both the request inputs and \`res.data\` from the
86
89
  route types, so \`tsc\` (and \`nifra build\`) catch any mismatch the moment a route changes. When building a
87
90
  page that calls the API, reach for the client first.
88
91
  `;
89
- /** File-routing + the server-only gotcha - only for full-stack (site/isr) templates. */
92
+ /** File routing, loaders and the zones - only for full-stack (site/isr) templates. */
90
93
  function webRules(framework) {
91
94
  const label = FRAMEWORK_LABEL[framework];
92
95
  return `## The frontend: file routing + loaders (${label})
93
96
 
94
97
  - Routes live in \`routes/\`. \`index.tsx\` → \`/\`, \`[id].tsx\` → \`/:id\`, \`_layout.tsx\` wraps a subtree,
95
98
  \`_404.tsx\` / \`_error.tsx\` are the fallbacks.
96
- - A route file may export: \`default\` (the page component), \`loader\` (server-only data), \`action\`
97
- (server-only mutation), and \`meta\` (head tags). The \`loader\` returns data typed straight into the page.
99
+ - A page's \`.tsx\` file exports \`default\` (the component) and \`meta\` (head tags). Its \`.backend.ts\` file
100
+ exports \`loader\` (data) and \`action\` (mutations), each with its output schema.
98
101
  - **The \`server()\` backend is IN-PROCESS, not a public HTTP surface.** Loaders/actions call it through
99
- \`ctx.api\` (the typed client - no network) during SSR; there is no \`GET /your-route\` endpoint on the page
102
+ \`api\` (the typed client - no network) during SSR; there is no \`GET /your-route\` endpoint on the page
100
103
  server to curl. Build features through loaders/actions + the typed client, not direct HTTP calls.
101
104
  - Swap UI frameworks by changing one import (\`@nifrajs/web-${framework}\` → another adapter); your route,
102
105
  loader, and action code do not change.
@@ -105,20 +108,14 @@ function webRules(framework) {
105
108
  and navigate without a full reload. Throwing in an action or returning in a loader is a silent bug
106
109
  that produces a false "operation failed" error on the client.
107
110
 
108
- ## ⚠️ The one rule that bites everyone: never import server-only code at a route's top level
109
-
110
- A route module's \`loader\`/\`action\` run **only on the server**, but the module is **also bundled for the
111
- browser** (for the component) - and the loader is *not* stripped. So a top-level
112
- \`import db from "./db"\` (or anything touching \`process.env\`, secrets, \`node:\` APIs) ships server code to
113
- the client and crashes hydration. Reach server resources through \`ctx.api\` / \`ctx.env\` (injected via
114
- \`createWebApp\`) inside the loader instead - never as a top-level import in a \`routes/\` file.
111
+ ${agentsStructureSection()}
115
112
  `;
116
113
  }
117
114
  /** Per-ORM body of the DB rules: the query idiom, schema location, and migrate story all differ. */
118
115
  function dbRulesBody(orm) {
119
116
  if (orm === "prisma") {
120
- return `A Prisma data layer is wired: \`prisma/schema.prisma\` (a starter \`Note\` model) + \`db/index.ts\` (a
121
- singleton \`PrismaClient\`).
117
+ return `A Prisma data layer is wired: \`prisma/schema.prisma\` (a starter \`Note\` model) +
118
+ \`backend/db/index.ts\` (a singleton \`PrismaClient\`).
122
119
 
123
120
  - **Wire it into the backend once**, then read \`c.db\` in handlers:
124
121
  \`\`\`ts
@@ -137,9 +134,9 @@ singleton \`PrismaClient\`).
137
134
  applies the migration and regenerates the client). \`bun run db:studio\` opens a DB browser.`;
138
135
  }
139
136
  if (orm === "kysely") {
140
- return `A Kysely typed query builder is wired in \`db/\`: \`db/schema.ts\` (the DB-shape interface - keep it in
141
- sync with your migrations) + \`db/index.ts\` (the typed client). You own the migrations (\`db/migrations/\`,
142
- run by \`db/migrate.ts\`).
137
+ return `A Kysely typed query builder is wired in \`backend/db/\`: \`backend/db/schema.ts\` (the DB-shape
138
+ interface - keep it in sync with your migrations) + \`backend/db/index.ts\` (the typed client). You own the
139
+ migrations (\`backend/db/migrations/\`, run by \`backend/db/migrate.ts\`).
143
140
 
144
141
  - **Wire it into the backend once**, then read \`c.db\` in handlers:
145
142
  \`\`\`ts
@@ -154,11 +151,11 @@ run by \`db/migrate.ts\`).
154
151
  .post("/notes", { body: t.object({ title: t.string({ minLength: 1 }) }) }, async (c) =>
155
152
  c.db.insertInto("notes").values({ title: c.body.title }).returningAll().executeTakeFirstOrThrow())
156
153
  \`\`\`
157
- - **Migrations:** add a file to \`db/migrations/\` (copy the \`0001_create_notes.ts\` shape), update the
158
- \`db/schema.ts\` interface to match, then \`bun run db:migrate\`.`;
154
+ - **Migrations:** add a file to \`backend/db/migrations/\` (copy the \`0001_create_notes.ts\` shape),
155
+ update the \`backend/db/schema.ts\` interface to match, then \`bun run db:migrate\`.`;
159
156
  }
160
- return `A Drizzle data layer is wired in \`db/\`: \`db/schema.ts\` (a starter \`notes\` table) + \`db/index.ts\` (the
161
- typed client).
157
+ return `A Drizzle data layer is wired in \`backend/db/\`: \`backend/db/schema.ts\` (a starter \`notes\`
158
+ table) + \`backend/db/index.ts\` (the typed client).
162
159
 
163
160
  - **Wire it into the backend once**, then read \`c.db\` in handlers:
164
161
  \`\`\`ts
@@ -173,8 +170,8 @@ typed client).
173
170
  (await c.db.insert(notes).values({ title: c.body.title }).returning())[0],
174
171
  )
175
172
  \`\`\`
176
- - **Migrations:** edit \`db/schema.ts\`, then \`bun run db:generate\` (writes SQL to \`db/migrations\`) +
177
- \`bun run db:migrate\` (applies it). \`bun run db:studio\` opens a DB browser.`;
173
+ - **Migrations:** edit \`backend/db/schema.ts\`, then \`bun run db:generate\` (writes SQL to
174
+ \`backend/db/migrations\`) + \`bun run db:migrate\` (applies it). \`bun run db:studio\` opens a DB browser.`;
178
175
  }
179
176
  /** DB rules - only when scaffolded with `--db`. Teaches the wired data layer + the `c.db` seam. */
180
177
  function dbRules(db) {
@@ -184,15 +181,15 @@ function dbRules(db) {
184
181
  ${dbRulesBody(p.orm)} ${p.note}
185
182
 
186
183
  - **Query is fully typed** - types flow from the schema/model; \`c.db\` is typed end to end.
187
- - **Never top-level-import \`db\` into a \`routes/\` page file** - it's server-only; reach it via \`c.db\` on
188
- the backend (or \`ctx.api\` from a loader). A top-level import ships it to the browser and breaks the build.`;
184
+ - **\`backend/db/\` is backend code.** Reach it via \`c.db\` on the backend, or through \`api\` from a
185
+ route's \`.backend.ts\` loader. A frontend file that imports it fails the build.`;
189
186
  }
190
187
  /** Auth rules - only when scaffolded with `--auth`. Teaches the better-auth mount + the typed guards. */
191
188
  function authRules(_auth) {
192
189
  return `## Authentication (better-auth)
193
190
 
194
- better-auth is configured in \`auth.ts\` (email/password + sessions, backed by your scaffolded database via
195
- its ORM adapter). nifra mounts it and gives you typed session guards via \`@nifrajs/better-auth\`.
191
+ better-auth is configured in \`backend/auth.ts\` (email/password + sessions, backed by your scaffolded
192
+ database via its ORM adapter). nifra mounts it and gives you typed session guards via \`@nifrajs/better-auth\`.
196
193
 
197
194
  - **Mount it once** on the backend - it serves every auth endpoint under \`/api/auth/*\`:
198
195
  \`\`\`ts
@@ -201,9 +198,10 @@ its ORM adapter). nifra mounts it and gives you typed session guards via \`@nifr
201
198
  import { auth } from "./auth"
202
199
  export const app = server().use(betterAuth(auth))
203
200
  \`\`\`
204
- - **Generate the auth tables** once, and after changing the config: \`bunx @better-auth/cli@latest generate\`
205
- writes them into your schema (\`db/schema.ts\` for Drizzle, \`prisma/schema.prisma\` for Prisma); then
206
- \`bun run db:migrate\`.
201
+ - **Generate the auth tables** once, and after changing the config:
202
+ \`bunx @better-auth/cli@latest generate --config backend/auth.ts\` writes their schema (Prisma: into
203
+ \`prisma/schema.prisma\`; Drizzle: a \`schema.ts\` at the root unless \`--output\` names a file under
204
+ \`backend/db/\`); then \`bun run db:migrate\`.
207
205
  - **Read / require a session** with the typed guards (pass the raw \`Request\` - \`c.req\` in a handler, or a
208
206
  loader's \`request\`):
209
207
  \`\`\`ts
@@ -214,7 +212,8 @@ its ORM adapter). nifra mounts it and gives you typed session guards via \`@nifr
214
212
  return { id: user.id }
215
213
  })
216
214
  \`\`\`
217
- - \`auth\` is **server-only** - never import it into a \`routes/\` page, and keep \`BETTER_AUTH_SECRET\` in \`.env\`.`;
215
+ - \`backend/auth.ts\` is **backend code** - a frontend file cannot import it - and \`BETTER_AUTH_SECRET\`
216
+ stays in \`.env\`.`;
218
217
  }
219
218
  const SECURITY_RULES = `## Production defaults (don't ship the happy path only)
220
219
 
@@ -233,28 +232,25 @@ const SECURITY_RULES = `## Production defaults (don't ship the happy path only)
233
232
  const COMMANDS = {
234
233
  api: `- \`bun install\` - install dependencies
235
234
  - \`bun run dev\` - run the API (watch mode)
236
- - \`bun test\` - drive \`app.fetch\` directly; no server required
235
+ - \`bun test\` - drive \`backend.fetch\` directly; no server required
237
236
  - \`nifra check\` - typecheck + typed-client lint (run before you call work done)`,
238
237
  site: `- \`bun install\` - install dependencies
239
- - \`nifra dev\` - true-HMR dev server
240
- - \`nifra build\` - complete Bun deploy (server + content-hashed client assets)
241
- - \`nifra start\` - run the generated Bun server - pairs with \`nifra build\`
242
- - \`nifra check\` - typecheck + typed-client lint (run before you call work done)
243
-
244
- For a local production run use \`nifra build && nifra start\` (or \`bun run build:bun && bun run start\`). NOTE:
245
- the bare \`bun run build\` script targets **Cloudflare Pages for DEPLOY** (emits \`dist/\`, a different layout)
246
- - it does NOT pair with \`nifra start\`/\`bun run start\` (which serve \`dist-bun/\`). Don't mix the two.`,
238
+ - \`bun run dev\` (\`nifra dev\`) - true-HMR dev server
239
+ - \`bun run build\` (\`nifra build\`) - the deploy directory for the app's \`target\` in \`nifra.config.ts\`; it
240
+ generates the server entry, so the app has no per-runtime entry or build script
241
+ - \`bun run start\` - run the built app locally (on targets with a local runner)
242
+ - \`nifra target <t>\` - switch where the app deploys: bun | node | deno | cloudflare | vercel
243
+ - \`nifra check\` - typecheck + typed-client lint (run before you call work done)`,
247
244
  isr: `- \`bun install\` - install dependencies
248
- - \`nifra dev\` - true-HMR dev server
249
- - \`nifra build\` - complete Bun deploy (server + content-hashed client assets)
250
- - \`nifra start\` - run the generated Bun server (SSR + ISR cache) - pairs with \`nifra build\`
251
- - \`nifra check\` - typecheck + typed-client lint (run before you call work done)
252
-
253
- For a local production run use \`nifra build && nifra start\`. NOTE: the bare \`bun run build\` script targets
254
- **Cloudflare Pages for DEPLOY** (\`dist/\`) and does NOT pair with \`nifra start\` - don't mix the two.`,
245
+ - \`bun run build\` - \`build.ts\`: the client bundle into \`public/assets\`, the worker
246
+ (\`backend/worker.ts\`) into \`dist-server/worker.js\`
247
+ - \`bun run dev\` - \`wrangler dev\` on real workerd with a local KV (run \`bun run build\` first)
248
+ - \`bun run dev:bun\` - the same app on a local Bun server (\`backend/dev-server.ts\`, in-memory cache)
249
+ - \`bun run build && bunx wrangler deploy\` - deploy (create the KV namespace first; see README)
250
+ - \`nifra check\` - typecheck + typed-client lint (run before you call work done)`,
255
251
  batteries: `- \`bun install\` - install dependencies
256
- - \`bun run dev\` - run the API (watch mode); \`src/index.ts\` also starts the job worker (\`queue.start()\`)
257
- - \`bun test\` - drives \`app.fetch\` directly (pagination, jobs via \`queue.drain()\`, cache, storage); no server required
252
+ - \`bun run dev\` - run the API (watch mode); \`backend/index.ts\` also starts the job worker (\`queue.start()\`)
253
+ - \`bun test\` - drives \`backend.fetch\` directly (pagination, jobs via \`queue.drain()\`, cache, storage); no server required
258
254
  - \`bun run typecheck\` - run before you call work done
259
255
 
260
256
  Wired: cursor pagination (\`t.pageQuery\`/\`t.paginated\`/\`paginate\`), background jobs (\`@nifrajs/jobs\`),
@@ -267,8 +263,8 @@ const DONE = `## Definition of done - run \`nifra check\`
267
263
  Before considering any change complete, run **\`nifra check\`**. It (1) typechecks - the frontend↔backend
268
264
  contract is compiler-enforced, so the typed client deriving \`res.data\` from your routes turns any
269
265
  mismatch into an error; (2) flags any hand-rolled \`fetch()\` to this app's own API (which bypasses that
270
- check); and (3) flags a server-only import (a DB driver, \`node:\`/\`bun:\`, \`./db\`) at the top level of a
271
- \`routes/\` page - that ships server code to the browser. Use \`nifra check --json\` for machine-readable
266
+ check); and (3) enforces the frontend/backend zones - a frontend file reaching backend code, or a loader
267
+ or action without an output schema, fails it. Use \`nifra check --json\` for machine-readable
272
268
  diagnostics. A failing \`nifra check\` means the work isn't done - fix it, don't ship around it.`;
273
269
  /** Build the full `AGENTS.md` for a freshly scaffolded app. */
274
270
  export function agentsMd(opts) {