create-nifra 2.1.0 → 2.3.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/README.md +4 -4
  2. package/dist/agent-files.d.ts +14 -14
  3. package/dist/agent-files.js +29 -29
  4. package/dist/agents.d.ts +4 -4
  5. package/dist/agents.js +63 -63
  6. package/dist/auth.d.ts +2 -2
  7. package/dist/auth.js +8 -8
  8. package/dist/cli.d.ts +6 -6
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +56 -35
  11. package/dist/cli.js.map +1 -1
  12. package/dist/db.d.ts +6 -6
  13. package/dist/db.d.ts.map +1 -1
  14. package/dist/db.js +138 -41
  15. package/dist/db.js.map +1 -1
  16. package/dist/scaffold/features.d.ts +41 -0
  17. package/dist/scaffold/features.d.ts.map +1 -0
  18. package/dist/scaffold/features.js +48 -0
  19. package/dist/scaffold/features.js.map +1 -0
  20. package/dist/scaffold/frameworks.d.ts +73 -0
  21. package/dist/scaffold/frameworks.d.ts.map +1 -0
  22. package/dist/scaffold/frameworks.js +100 -0
  23. package/dist/scaffold/frameworks.js.map +1 -0
  24. package/dist/scaffold/site-build.d.ts +60 -0
  25. package/dist/scaffold/site-build.d.ts.map +1 -0
  26. package/dist/scaffold/site-build.js +198 -0
  27. package/dist/scaffold/site-build.js.map +1 -0
  28. package/dist/scaffold/site-files.d.ts +16 -0
  29. package/dist/scaffold/site-files.d.ts.map +1 -0
  30. package/dist/scaffold/site-files.js +112 -0
  31. package/dist/scaffold/site-files.js.map +1 -0
  32. package/dist/scaffold/site.d.ts +33 -0
  33. package/dist/scaffold/site.d.ts.map +1 -0
  34. package/dist/scaffold/site.js +99 -0
  35. package/dist/scaffold/site.js.map +1 -0
  36. package/package.json +3 -2
  37. package/src/agent-files.ts +149 -0
  38. package/template/README.md +3 -3
  39. package/template/nifra.assurance.ts +80 -0
  40. package/template/package.json +5 -3
  41. package/template/src/app.ts +10 -11
  42. package/template/src/routes.ts +13 -0
  43. package/template-isr/README.md +1 -1
  44. package/template-isr/backend.ts +11 -15
  45. package/template-isr/nifra.assurance.ts +81 -0
  46. package/template-isr/package.json +8 -7
  47. package/template-isr/page.ts +12 -0
  48. package/template-isr/routes/_404.tsx +1 -1
  49. package/template-isr/routes/_layout.tsx +1 -1
  50. package/template-isr/routes/index.tsx +2 -2
  51. package/template-isr/server.ts +1 -1
  52. package/template-isr/worker.ts +2 -2
  53. package/template-isr/wrangler.toml +2 -2
  54. package/template-site/Dockerfile +1 -1
  55. package/template-site/README.md +6 -6
  56. package/template-site/backend.ts +11 -13
  57. package/template-site/counter.ts +16 -0
  58. package/template-site/nifra.assurance.ts +83 -0
  59. package/template-site/nifra.config.ts +1 -1
  60. package/template-site/routes/index.tsx +1 -1
  61. package/template-site/server-node.ts +1 -1
  62. package/template-site/server-vercel.ts +1 -1
  63. package/template-site-preact/README.md +7 -7
  64. package/template-site-preact/nifra.config.ts +1 -1
  65. package/template-site-preact/routes/index.tsx +1 -1
  66. package/template-site-solid/README.md +7 -7
  67. package/template-site-solid/nifra.config.ts +2 -2
  68. package/template-site-solid/routes/index.tsx +1 -1
  69. package/template-site-svelte/README.md +7 -7
  70. package/template-site-svelte/nifra.config.ts +2 -2
  71. package/template-site-svelte/routes/_layout.svelte +1 -1
  72. package/template-site-svelte/routes/index.svelte +1 -1
  73. package/template-site-vue/README.md +7 -7
  74. package/template-site-vue/nifra.config.ts +2 -2
  75. package/template-site-vue/routes/_layout.vue +2 -2
  76. package/template-site-vue/routes/index.vue +3 -3
  77. package/template-site/build-bun.ts +0 -27
  78. package/template-site/build-deno.ts +0 -25
  79. package/template-site/build-node.ts +0 -26
  80. package/template-site/build-vercel.ts +0 -45
  81. package/template-site/build.ts +0 -29
  82. package/template-site/framework.ts +0 -6
  83. package/template-site/package.json +0 -38
  84. package/template-site/tsconfig.json +0 -37
  85. package/template-site-preact/.dockerignore +0 -14
  86. package/template-site-preact/Dockerfile +0 -20
  87. package/template-site-preact/_worker.ts +0 -17
  88. package/template-site-preact/backend.ts +0 -15
  89. package/template-site-preact/build-bun.ts +0 -27
  90. package/template-site-preact/build-deno.ts +0 -25
  91. package/template-site-preact/build-node.ts +0 -26
  92. package/template-site-preact/build-vercel.ts +0 -45
  93. package/template-site-preact/build.ts +0 -29
  94. package/template-site-preact/deno.json +0 -7
  95. package/template-site-preact/framework.ts +0 -6
  96. package/template-site-preact/gitignore +0 -16
  97. package/template-site-preact/package.json +0 -35
  98. package/template-site-preact/server-bun.ts +0 -35
  99. package/template-site-preact/server-deno.ts +0 -38
  100. package/template-site-preact/server-node.ts +0 -40
  101. package/template-site-preact/server-vercel.ts +0 -17
  102. package/template-site-preact/tsconfig.json +0 -38
  103. package/template-site-preact/wrangler.toml +0 -4
  104. package/template-site-solid/.dockerignore +0 -14
  105. package/template-site-solid/Dockerfile +0 -20
  106. package/template-site-solid/_worker.ts +0 -17
  107. package/template-site-solid/backend.ts +0 -15
  108. package/template-site-solid/build-bun.ts +0 -33
  109. package/template-site-solid/build-deno.ts +0 -32
  110. package/template-site-solid/build-node.ts +0 -33
  111. package/template-site-solid/build-vercel.ts +0 -47
  112. package/template-site-solid/build.ts +0 -36
  113. package/template-site-solid/deno.json +0 -7
  114. package/template-site-solid/framework.ts +0 -6
  115. package/template-site-solid/gitignore +0 -16
  116. package/template-site-solid/package.json +0 -35
  117. package/template-site-solid/server-bun.ts +0 -35
  118. package/template-site-solid/server-deno.ts +0 -38
  119. package/template-site-solid/server-node.ts +0 -40
  120. package/template-site-solid/server-vercel.ts +0 -17
  121. package/template-site-solid/tsconfig.json +0 -38
  122. package/template-site-solid/wrangler.toml +0 -4
  123. package/template-site-svelte/.dockerignore +0 -14
  124. package/template-site-svelte/Dockerfile +0 -20
  125. package/template-site-svelte/_worker.ts +0 -17
  126. package/template-site-svelte/backend.ts +0 -15
  127. package/template-site-svelte/build-bun.ts +0 -32
  128. package/template-site-svelte/build-deno.ts +0 -31
  129. package/template-site-svelte/build-node.ts +0 -32
  130. package/template-site-svelte/build-vercel.ts +0 -46
  131. package/template-site-svelte/build.ts +0 -35
  132. package/template-site-svelte/deno.json +0 -7
  133. package/template-site-svelte/framework.ts +0 -6
  134. package/template-site-svelte/gitignore +0 -16
  135. package/template-site-svelte/package.json +0 -35
  136. package/template-site-svelte/server-bun.ts +0 -35
  137. package/template-site-svelte/server-deno.ts +0 -38
  138. package/template-site-svelte/server-node.ts +0 -40
  139. package/template-site-svelte/server-vercel.ts +0 -17
  140. package/template-site-svelte/tsconfig.json +0 -37
  141. package/template-site-svelte/wrangler.toml +0 -4
  142. package/template-site-vue/.dockerignore +0 -14
  143. package/template-site-vue/Dockerfile +0 -20
  144. package/template-site-vue/_worker.ts +0 -17
  145. package/template-site-vue/backend.ts +0 -15
  146. package/template-site-vue/build-bun.ts +0 -37
  147. package/template-site-vue/build-deno.ts +0 -36
  148. package/template-site-vue/build-node.ts +0 -37
  149. package/template-site-vue/build-vercel.ts +0 -54
  150. package/template-site-vue/build.ts +0 -40
  151. package/template-site-vue/deno.json +0 -7
  152. package/template-site-vue/framework.ts +0 -6
  153. package/template-site-vue/gitignore +0 -16
  154. package/template-site-vue/package.json +0 -36
  155. package/template-site-vue/server-bun.ts +0 -35
  156. package/template-site-vue/server-deno.ts +0 -38
  157. package/template-site-vue/server-node.ts +0 -40
  158. package/template-site-vue/server-vercel.ts +0 -17
  159. package/template-site-vue/tsconfig.json +0 -37
  160. package/template-site-vue/wrangler.toml +0 -4
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Compose a site scaffold: shared base, generated files, framework overlay.
3
+ *
4
+ * A site is 26 files. Thirteen are the same whatever you render with - the Dockerfile, every server
5
+ * entry, the worker, the assurance config - eight are mechanical enough to emit from a model, and five
6
+ * are genuinely the framework's own. Keeping five copies of all 26 is what let `.vercel` reach four
7
+ * tsconfigs and not the fifth, and a Vercel comment reach one build entry and not the other four.
8
+ *
9
+ * The five that stay literal stay literal on purpose. `nifra.config.ts` explains why Solid wants a
10
+ * `solid` resolve condition and what `@preact/preset-vite` is; the routes are the app a user reads
11
+ * first. That prose belongs in a file you can open, not in a TypeScript string.
12
+ */
13
+ import { cp, mkdir, writeFile } from "node:fs/promises";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { frameworkSpec } from "./frameworks.js";
17
+ import { BUILD_TARGETS, renderBuildFile } from "./site-build.js";
18
+ import { renderFrameworkModule, renderPackageJson, renderTsconfig } from "./site-files.js";
19
+ /**
20
+ * Files identical in every site scaffold, taken from the base directory.
21
+ *
22
+ * Listed rather than globbed: a glob would silently absorb a file someone adds to the base for one
23
+ * framework's benefit, which is the failure this is meant to end. Adding a shared file means saying so
24
+ * here, and `site-composition.test.ts` fails if this list and the directory disagree.
25
+ */
26
+ export const SHARED_SITE_FILES = [
27
+ ".dockerignore",
28
+ "Dockerfile",
29
+ "_worker.ts",
30
+ "backend.ts",
31
+ "counter.ts",
32
+ "deno.json",
33
+ "gitignore",
34
+ "nifra.assurance.ts",
35
+ "server-bun.ts",
36
+ "server-deno.ts",
37
+ "server-node.ts",
38
+ "server-vercel.ts",
39
+ "wrangler.toml",
40
+ ];
41
+ /** Files each framework supplies itself, relative to its overlay directory. */
42
+ export const FRAMEWORK_SITE_FILES = ["README.md", "nifra.config.ts"];
43
+ /** Route files, whose extension is the framework's own. */
44
+ export const ROUTE_BASENAMES = ["_404", "_layout", "index"];
45
+ /** Extension the framework's route files carry. */
46
+ export function routeExtension(framework) {
47
+ if (framework.id === "svelte")
48
+ return "svelte";
49
+ if (framework.id === "vue")
50
+ return "vue";
51
+ return "tsx";
52
+ }
53
+ const here = dirname(fileURLToPath(import.meta.url));
54
+ /** The base directory: the shared files plus React's own, which is also the React overlay. */
55
+ export const SITE_BASE_DIR = join(here, "..", "..", "template-site");
56
+ export function overlayDir(framework) {
57
+ return framework.id === "react"
58
+ ? SITE_BASE_DIR
59
+ : join(here, "..", "..", `template-site-${framework.id}`);
60
+ }
61
+ /** Every file a site scaffold emits, and the text it holds. Generated files only. */
62
+ export function generatedSiteFiles(framework) {
63
+ const files = new Map();
64
+ for (const target of BUILD_TARGETS)
65
+ files.set(target.file, renderBuildFile(target, framework));
66
+ files.set("framework.ts", renderFrameworkModule(framework));
67
+ files.set("package.json", renderPackageJson(framework));
68
+ files.set("tsconfig.json", renderTsconfig(framework));
69
+ return files;
70
+ }
71
+ /**
72
+ * Write a complete site scaffold into `target`.
73
+ *
74
+ * Refusing an occupied destination is the default and is preserved deliberately: `cp`'s
75
+ * `errorOnExist` did that before this was generated, and losing it would turn a mistyped path into
76
+ * silent data loss in someone's working directory. `--force` is what `bun create nifra .` needs.
77
+ */
78
+ export async function materializeSite(target, id, options = {}) {
79
+ const framework = frameworkSpec(id);
80
+ const overlay = overlayDir(framework);
81
+ const force = options.force === true;
82
+ const copy = (from, to) => cp(from, to, force ? { force: true } : { errorOnExist: true, force: false });
83
+ // `wx` fails when the file exists, which is the write-side equivalent of `errorOnExist`.
84
+ const emit = (to, contents) => writeFile(to, contents, force ? {} : { flag: "wx" });
85
+ await mkdir(join(target, "routes"), { recursive: true });
86
+ for (const file of SHARED_SITE_FILES)
87
+ await copy(join(SITE_BASE_DIR, file), join(target, file));
88
+ for (const [file, contents] of generatedSiteFiles(framework)) {
89
+ await emit(join(target, file), contents);
90
+ }
91
+ for (const file of FRAMEWORK_SITE_FILES)
92
+ await copy(join(overlay, file), join(target, file));
93
+ const extension = routeExtension(framework);
94
+ for (const base of ROUTE_BASENAMES) {
95
+ const name = join("routes", `${base}.${extension}`);
96
+ await copy(join(overlay, name), join(target, name));
97
+ }
98
+ }
99
+ //# sourceMappingURL=site.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site.js","sourceRoot":"","sources":["../../src/scaffold/site.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-nifra",
3
- "version": "2.1.0",
4
- "description": "Scaffold a new nifra app — `bun create nifra <dir>`.",
3
+ "version": "2.3.0",
4
+ "description": "Scaffold a new nifra app - `bun create nifra <dir>`.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "files": [
31
31
  "dist",
32
+ "src/agent-files.ts",
32
33
  "template",
33
34
  "template-site",
34
35
  "template-site-vue",
@@ -0,0 +1,149 @@
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:
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
9
+ *
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.
14
+ *
15
+ * 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.
23
+ */
24
+
25
+ import { readFileSync } from "node:fs"
26
+
27
+ /** The MCP launch command, shared by `.mcp.json` and `.cursor/mcp.json`. See the module header for why
28
+ * the package is named explicitly rather than relying on the bare `nifra` bin. */
29
+ export const MCP_SERVER_COMMAND = "bunx" as const
30
+
31
+ /**
32
+ * The `@nifrajs/cli` version the launch command pins to - DERIVED at load time from this package's own
33
+ * `version`, never hardcoded. `fixed` changeset versioning ([["@nifrajs/*", "create-nifra", "nifra"]] in
34
+ * `.changeset/config.json`) bumps `create-nifra` and `@nifrajs/cli` in lockstep, so `create-nifra@x`
35
+ * always means `@nifrajs/cli@x`. Deriving it here means a release bump has nothing to forget to update -
36
+ * there is no stale literal to drift (the 1.0.0 cut shipped a beta pin exactly this way). `check:publish`
37
+ * still asserts it equals `@nifrajs/cli`'s version as a belt-and-suspenders guard on the `fixed` link.
38
+ *
39
+ * Resolves correctly under BOTH export conditions: the `bun` export runs `src/agent-files.ts`
40
+ * (`../package.json` → the package root) and the tsc `default` export runs `dist/agent-files.js`
41
+ * (`../package.json` → the same root, since `dist/` sits beside the manifest). npm always ships a
42
+ * package's own `package.json`, so the read can't miss at install time.
43
+ *
44
+ * Why pin at all instead of `bunx @nifrajs/cli mcp`: `bunx` keys its cache on the exact version spec.
45
+ * An UNPINNED spec resolves to the `latest` tag once, then `bunx` reuses that cached copy on every
46
+ * later spawn WITHOUT re-checking the registry - so an editor that once launched an older `@nifrajs/cli`
47
+ * keeps respawning the stale binary even after a newer one is published (the MCP server silently runs
48
+ * old code). Pinning the exact version makes the version part of the cache key, so each release fetches
49
+ * fresh and a stale cache can never shadow it. The cost: an already-scaffolded app's `.mcp.json` freezes
50
+ * at its scaffold-time version until `nifra init-agents` is re-run - an acceptable, deterministic trade.
51
+ */
52
+ export const MCP_CLI_VERSION: string = (
53
+ JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
54
+ version: string
55
+ }
56
+ ).version
57
+ export const MCP_SERVER_ARGS = [`@nifrajs/cli@${MCP_CLI_VERSION}`, "mcp"] as const
58
+
59
+ /** The server entry registered under the `nifra` key in both Claude Code's and Cursor's MCP config. */
60
+ export interface McpServerConfig {
61
+ readonly command: string
62
+ readonly args: readonly string[]
63
+ }
64
+
65
+ /** Claude Code / Cursor MCP config shape: a map of server name → launch config. */
66
+ export interface McpConfig {
67
+ readonly mcpServers: Readonly<Record<string, McpServerConfig>>
68
+ }
69
+
70
+ /** The one canonical MCP config object both registries serialize - the anti-drift seam. */
71
+ export const MCP_CONFIG: McpConfig = {
72
+ mcpServers: {
73
+ nifra: { command: MCP_SERVER_COMMAND, args: [...MCP_SERVER_ARGS] },
74
+ },
75
+ }
76
+
77
+ /** Serialize the canonical MCP config as the JSON written to `.mcp.json` and `.cursor/mcp.json`.
78
+ * Trailing newline so the file is POSIX-clean and diffs don't flag a missing EOL. */
79
+ export function mcpJson(): string {
80
+ return `${JSON.stringify(MCP_CONFIG, null, 2)}\n`
81
+ }
82
+
83
+ /**
84
+ * `CLAUDE.md` - Claude Code reads this automatically. It is deliberately NOT a copy of `AGENTS.md`:
85
+ * a short preamble that (1) tells Claude this project ships a nifra MCP, registered in `.mcp.json`, and
86
+ * to PREFER it, and (2) pulls in the full cookbook with Claude Code's `@file` import directive on its
87
+ * own line - `@AGENTS.md` - so the conventions live in exactly one place and can't drift between the
88
+ * two files. Keep this short; the depth is in `AGENTS.md`.
89
+ */
90
+ export function claudeMd(): string {
91
+ return `# CLAUDE.md
92
+
93
+ This project ships a **nifra MCP server**, registered for Claude Code in \`.mcp.json\` (it launches with
94
+ \`${MCP_SERVER_COMMAND} ${MCP_SERVER_ARGS.join(" ")}\`). **Prefer the MCP tools** over writing nifra from memory - they are
95
+ typechecked against *this* project and this installed version, so they beat training-data recall:
96
+
97
+ - \`nifra_docs\` / \`nifra_example\` - exact signatures + verified, compiling snippets. Reach for these
98
+ before hand-writing a route, a loader, or a client call; a remembered API is often stale or wrong.
99
+ - \`nifra_context\` - this project's route index + conventions, so changes fit the existing surface.
100
+ - \`nifra_types\` - the EXACT TypeScript of any \`@nifrajs/*\` symbol (e.g. \`nifra_types({ name: "RateLimitStore" })\`).
101
+ Call it for a precise type; **never read \`node_modules/@nifrajs/**/*.d.ts\`** - the tool is the authoritative, complete source.
102
+ - **\`nifra_check\` is the done-gate.** Before declaring any change complete, run \`nifra_check\` (or
103
+ \`nifra check --json\` in a terminal). It typechecks the frontend↔backend contract and flags drift
104
+ (hand-rolled \`fetch()\` to this app's own API, server-only imports in \`routes/\`). A failing check
105
+ means the work isn't done - fix it, don't ship around it.
106
+
107
+ The full nifra cookbook for this app - backend rules, the typed never-throwing client, file routing, and
108
+ the gotchas - lives in \`AGENTS.md\`, imported here so it stays the single source of truth:
109
+
110
+ @AGENTS.md
111
+ `
112
+ }
113
+
114
+ /**
115
+ * The "## MCP server" section appended to a scaffolded (or retrofitted) `AGENTS.md`, so non-Claude
116
+ * agents (Cursor, and anything that reads `AGENTS.md`) also learn the MCP exists and what to prefer.
117
+ * Mirrors the CLAUDE.md preamble's guidance without the Claude-specific `@import`.
118
+ */
119
+ export function agentsMcpSection(): string {
120
+ return `## MCP server
121
+
122
+ This project ships a **nifra MCP server** - launch it with \`${MCP_SERVER_COMMAND} ${MCP_SERVER_ARGS.join(" ")}\`. It's registered for
123
+ Claude Code in \`.mcp.json\` and for Cursor in \`.cursor/mcp.json\`; other agents can point their MCP client
124
+ at that command. **Prefer its tools** over writing nifra from memory - they're typechecked against this
125
+ project + the installed version, so they beat training-data recall:
126
+
127
+ - \`nifra_docs\` / \`nifra_example\` - exact signatures + verified, compiling snippets.
128
+ - \`nifra_context\` / \`nifra_routes\` - this project's route index + per-route schemas.
129
+ - \`nifra_types\` - the EXACT TypeScript of any \`@nifrajs/*\` symbol (interface/type/function). Call it for a
130
+ precise type; **never read \`@nifrajs\` \`.d.ts\` files** - this is the authoritative, complete source.
131
+ - \`nifra_check\` - the done-gate (typecheck + drift lints). Run it before calling work complete; a
132
+ failing check means it isn't done. (\`nifra check --json\` in a terminal does the same.)`
133
+ }
134
+
135
+ /** Identifies a generated agent-discovery file: where it goes (relative to the project root) and how to
136
+ * produce its content. `merge` is for files that augment an existing one (AGENTS.md) rather than own it. */
137
+ export interface AgentFileSpec {
138
+ /** Path relative to the project root (POSIX-style; the caller joins onto the cwd). */
139
+ readonly path: string
140
+ /** A human label for the "wrote/skipped" report. */
141
+ readonly label: string
142
+ }
143
+
144
+ /** The standalone files this module fully owns (whole-file generators). AGENTS.md is handled separately
145
+ * because create-nifra builds it from `agents.ts` and the retrofit command appends a section to it. */
146
+ export const MCP_JSON_PATH = ".mcp.json"
147
+ export const CURSOR_MCP_JSON_PATH = ".cursor/mcp.json"
148
+ export const CLAUDE_MD_PATH = "CLAUDE.md"
149
+ export const AGENTS_MD_PATH = "AGENTS.md"
@@ -9,9 +9,9 @@ bun test # run the tests
9
9
  bun run typecheck # type-check
10
10
  ```
11
11
 
12
- - `src/app.ts` — your routes (exported without `listen`, so tests drive it via `app.fetch`).
13
- - `src/index.ts` — boots the server.
14
- - `src/app.test.ts` — an example test.
12
+ - `src/app.ts` - your routes (exported without `listen`, so tests drive it via `app.fetch`).
13
+ - `src/index.ts` - boots the server.
14
+ - `src/app.test.ts` - an example test.
15
15
 
16
16
  Add a typed client from `typeof app`:
17
17
 
@@ -0,0 +1,80 @@
1
+ /**
2
+ * What this app's routes are required to PROVE, checked by `nifra assure` (and level L1 of
3
+ * `nifra levels`). Evidence is collected from the routes themselves - a `body` schema records
4
+ * `body-bounded`, an auth middleware records `authenticated` - so this file states the requirement
5
+ * and the framework reports whether the route actually carries it.
6
+ *
7
+ * The starter rules below are deliberately small and true of THIS app. Grow them as the app grows:
8
+ * once there is auth, add `NIFRA_ASSURANCE.AUTHENTICATED` to `mutation`, and every route that does
9
+ * not carry it fails the check rather than shipping.
10
+ *
11
+ * Rules are first-match-wins, so order matters: put narrow exemptions above the general rule.
12
+ */
13
+ import { defineAssuranceConfig, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
14
+ import { app } from "./src/app.ts"
15
+
16
+ export default defineAssuranceConfig({
17
+ source: app,
18
+ // What each effect IS, so the policy below can be written about a CLASS of effect rather than a list
19
+ // of token names: `{ access: "write", zone: "domain" }` covers `payments.charge` the day someone adds
20
+ // it, where a rule naming `db.write` would not.
21
+ capabilities: {
22
+ definitions: [
23
+ { id: "db.read", zone: "domain", access: "read" },
24
+ { id: "db.write", zone: "domain", access: "write" },
25
+ ],
26
+ provenance: {
27
+ // Reaching one of these modules means holding the capabilities beside it, and `nifra check` fails
28
+ // any route that can reach further than it declared - so wiring a database into a handler and
29
+ // forgetting to say so is a failing check rather than something review has to catch.
30
+ //
31
+ // The first two are the seam `--db` scaffolds, split by access on purpose: reach is computed per
32
+ // MODULE, and a GET route may not declare a domain write, so a module that can reach both has GET
33
+ // routes with no legal declaration. Keeping reads and writes import-disjoint - at the seam and at
34
+ // the route modules - is what keeps every route's declaration equal to its reach. The drivers
35
+ // below are the backstop for code that goes around the seam; they grant both, because an import
36
+ // cannot tell a read from a write.
37
+ imports: [
38
+ { specifier: "./read.ts", capabilities: ["db.read"] },
39
+ { specifier: "./write.ts", capabilities: ["db.write"] },
40
+ { specifier: "bun:sqlite", capabilities: ["db.read", "db.write"] },
41
+ { specifier: "node:sqlite", capabilities: ["db.read", "db.write"] },
42
+ { specifier: "pg", capabilities: ["db.read", "db.write"] },
43
+ { specifier: "postgres", capabilities: ["db.read", "db.write"] },
44
+ { specifier: "mysql2", capabilities: ["db.read", "db.write"] },
45
+ { specifier: "kysely", capabilities: ["db.read", "db.write"] },
46
+ { specifier: "drizzle-orm", capabilities: ["db.read", "db.write"] },
47
+ { specifier: "drizzle-orm/*", capabilities: ["db.read", "db.write"] },
48
+ { specifier: "@libsql/*", capabilities: ["db.read", "db.write"] },
49
+ { specifier: "@prisma/client", capabilities: ["db.read", "db.write"] },
50
+ ],
51
+ // Add a driver here to force every query through the seam, and the check will name any route that
52
+ // reaches around it: `{ specifier: "pg", reason: "query through db/read.ts or db/write.ts" }`.
53
+ forbiddenImports: [],
54
+ },
55
+ },
56
+ policy: {
57
+ rules: [
58
+ // Anything that writes business state must prove who asked. Matched on what the capability IS
59
+ // rather than on its name or its path, so a token introduced later - `payments.charge`,
60
+ // `orders.write` - is covered the day it is declared instead of the day someone remembers to
61
+ // widen this rule. A server function is a public POST endpoint like any other, so this is what
62
+ // stops one shipping unauthenticated.
63
+ {
64
+ name: "authenticated-write",
65
+ match: { access: "write", zone: "domain" },
66
+ require: [NIFRA_ASSURANCE.AUTHENTICATED],
67
+ },
68
+ // Anything that changes state must validate its input at the boundary. Adding a POST without a
69
+ // `body` schema fails this - which is the point: the check is what notices, not review.
70
+ {
71
+ name: "mutation",
72
+ match: { methods: ["POST", "PUT", "PATCH", "DELETE"] },
73
+ require: [NIFRA_ASSURANCE.BODY_BOUNDED],
74
+ },
75
+ // Reads carry no requirement yet. When some become private, split this into a public rule and
76
+ // an authenticated one rather than weakening the whole class.
77
+ { name: "read", match: { methods: ["GET"] }, require: [] },
78
+ ],
79
+ },
80
+ })
@@ -6,15 +6,17 @@
6
6
  "scripts": {
7
7
  "dev": "bun --watch run src/index.ts",
8
8
  "start": "bun run src/index.ts",
9
+ "check": "nifra check && nifra assure",
9
10
  "test": "bun test",
10
11
  "typecheck": "tsc --noEmit"
11
12
  },
12
13
  "dependencies": {
13
- "@nifrajs/client": "^2.1.0",
14
- "@nifrajs/core": "^2.1.0",
15
- "@nifrajs/schema": "^2.1.0"
14
+ "@nifrajs/client": "^2.3.0",
15
+ "@nifrajs/core": "^2.3.0",
16
+ "@nifrajs/schema": "^2.3.0"
16
17
  },
17
18
  "devDependencies": {
19
+ "@nifrajs/cli": "^2.3.0",
18
20
  "@types/bun": "latest",
19
21
  "typescript": "^6.0.3"
20
22
  }
@@ -1,17 +1,16 @@
1
1
  import { server } from "@nifrajs/core/server"
2
- import { t } from "@nifrajs/schema"
2
+ import { routes } from "./routes.ts"
3
3
 
4
4
  /** Your app. Exported (without `listen`) so tests can drive it via the in-process test client. */
5
5
 
6
- // Contract-first: the schema validates the body at the boundary, types `c.body` in the
7
- // handler, types the response, and flows into the typed client (`testClient` in app.test.ts,
8
- // `client<App>(baseUrl)` in a frontend) - no codegen step.
9
- const EchoInput = t.object({ message: t.string({ minLength: 1 }) })
10
- const EchoReply = t.object({ echoed: t.string() })
11
-
12
- export const app = server()
13
- .get("/", () => ({ hello: "nifra" }))
14
- .get("/users/:id", (c) => ({ id: c.params.id }))
15
- .post("/echo", { body: EchoInput, response: EchoReply }, (c) => ({ echoed: c.body.message }))
6
+ // Composition only: this module merges route modules and registers none of its own.
7
+ //
8
+ // That is not a style preference. `nifra check` works out what a route can reach from the module that
9
+ // REGISTERS it, following that module's imports - so a file that registers routes AND imports a
10
+ // database gives every route in it database reach, and a GET route may not declare a domain write at
11
+ // all. Keeping the root pure means each route's reach is its own module's, which is what lets the
12
+ // capability declarations in `nifra.assurance.ts` stay true as the app grows. Add a feature as a
13
+ // module, merge it here.
14
+ export const app = server().merge(routes)
16
15
 
17
16
  export type App = typeof app
@@ -0,0 +1,13 @@
1
+ import { server } from "@nifrajs/core/server"
2
+ import { t } from "@nifrajs/schema"
3
+
4
+ // Contract-first: the schema validates the body at the boundary, types `c.body` in the
5
+ // handler, types the response, and flows into the typed client (`testClient` in app.test.ts,
6
+ // `client<App>(baseUrl)` in a frontend) - no codegen step.
7
+ const EchoInput = t.object({ message: t.string({ minLength: 1 }) })
8
+ const EchoReply = t.object({ echoed: t.string() })
9
+
10
+ export const routes = server()
11
+ .get("/", () => ({ hello: "nifra" }))
12
+ .get("/users/:id", (c) => ({ id: c.params.id }))
13
+ .post("/echo", { body: EchoInput, response: EchoReply }, (c) => ({ echoed: c.body.message }))
@@ -1,6 +1,6 @@
1
1
  # nifra + ISR
2
2
 
3
- A nifra + React app with **Incremental Static Regeneration** — pages are cached and served
3
+ A nifra + React app with **Incremental Static Regeneration** - pages are cached and served
4
4
  stale-while-revalidate, backed by **Workers KV** in production (so the cache and on-demand purges hold
5
5
  across instances) and an in-memory store for local dev.
6
6
 
@@ -1,17 +1,13 @@
1
1
  import { server } from "@nifrajs/core/server"
2
- import { t } from "@nifrajs/schema"
2
+ import { page } from "./page"
3
3
 
4
- // Your backend contract — page loaders/actions call it in-process during SSR (no network). This demo
5
- // counts server renders so the ISR cache is observable: a cache HIT serves stored bytes (the loader
6
- // doesn't run, so the number holds), while a MISS or background REGENERATION runs it (it bumps).
7
- // Replace it with real data (KV/D1/Postgres/…); on the edge, reach bindings via `c.env`.
8
- let renders = 0
9
-
10
- export const backend = server().get(
11
- "/page",
12
- { response: t.object({ renders: t.number() }) },
13
- () => {
14
- renders += 1
15
- return { renders }
16
- },
17
- )
4
+ // Your backend contract - page loaders/actions call it in-process during SSR (no network).
5
+ // Composition only: this module merges route modules and registers none of its own.
6
+ //
7
+ // That is not a style preference. `nifra check` works out what a route can reach from the module that
8
+ // REGISTERS it, following that module's imports - so a file that registers routes AND imports a
9
+ // database gives every route in it database reach, and a GET route may not declare a domain write at
10
+ // all. Keeping this file pure means each route's reach is its own module's, which is what lets the
11
+ // capability declarations in `nifra.assurance.ts` stay true as the app grows. Add a feature as a
12
+ // module, merge it here.
13
+ export const backend = server().merge(page)
@@ -0,0 +1,81 @@
1
+ /**
2
+ * What this app's routes are required to PROVE, checked by `nifra assure` (and level L1 of
3
+ * `nifra levels`). Evidence is collected from the routes themselves - a `body` schema records
4
+ * `body-bounded`, an auth middleware records `authenticated` - so this file states the requirement
5
+ * and the framework reports whether the route actually carries it.
6
+ *
7
+ * Page routes under `routes/` are not classified here; this is the backend contract those pages call.
8
+ *
9
+ * Rules are first-match-wins, so order matters: narrow exemptions go above the general rule, and each
10
+ * one is named and justified. An exemption you have to write down is one a reviewer can argue with;
11
+ * an exemption you get by leaving the rule off is one nobody ever sees.
12
+ */
13
+ import { defineAssuranceConfig, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
14
+ import { backend } from "./backend"
15
+
16
+ export default defineAssuranceConfig({
17
+ source: backend,
18
+ // What each effect IS, so the policy below can be written about a CLASS of effect rather than a list
19
+ // of token names: `{ access: "write", zone: "domain" }` covers `payments.charge` the day someone adds
20
+ // it, where a rule naming `db.write` would not.
21
+ capabilities: {
22
+ definitions: [
23
+ { id: "db.read", zone: "domain", access: "read" },
24
+ { id: "db.write", zone: "domain", access: "write" },
25
+ ],
26
+ provenance: {
27
+ // Reaching one of these modules means holding the capabilities beside it, and `nifra check` fails
28
+ // any route that can reach further than it declared - so wiring a database into a handler and
29
+ // forgetting to say so is a failing check rather than something review has to catch.
30
+ //
31
+ // The first two are the seam `--db` scaffolds, split by access on purpose: reach is computed per
32
+ // MODULE, and a GET route may not declare a domain write, so a module that can reach both has GET
33
+ // routes with no legal declaration. Keeping reads and writes import-disjoint - at the seam and at
34
+ // the route modules - is what keeps every route's declaration equal to its reach. The drivers
35
+ // below are the backstop for code that goes around the seam; they grant both, because an import
36
+ // cannot tell a read from a write.
37
+ imports: [
38
+ { specifier: "./read.ts", capabilities: ["db.read"] },
39
+ { specifier: "./write.ts", capabilities: ["db.write"] },
40
+ { specifier: "bun:sqlite", capabilities: ["db.read", "db.write"] },
41
+ { specifier: "node:sqlite", capabilities: ["db.read", "db.write"] },
42
+ { specifier: "pg", capabilities: ["db.read", "db.write"] },
43
+ { specifier: "postgres", capabilities: ["db.read", "db.write"] },
44
+ { specifier: "mysql2", capabilities: ["db.read", "db.write"] },
45
+ { specifier: "kysely", capabilities: ["db.read", "db.write"] },
46
+ { specifier: "drizzle-orm", capabilities: ["db.read", "db.write"] },
47
+ { specifier: "drizzle-orm/*", capabilities: ["db.read", "db.write"] },
48
+ { specifier: "@libsql/*", capabilities: ["db.read", "db.write"] },
49
+ { specifier: "@prisma/client", capabilities: ["db.read", "db.write"] },
50
+ ],
51
+ // Add a driver here to force every query through the seam, and the check will name any route that
52
+ // reaches around it: `{ specifier: "pg", reason: "query through db/read.ts or db/write.ts" }`.
53
+ forbiddenImports: [],
54
+ },
55
+ },
56
+ policy: {
57
+ rules: [
58
+ // Anything that writes business state must prove who asked. Matched on what the capability IS
59
+ // rather than on its name or its path, so a token introduced later - `payments.charge`,
60
+ // `orders.write` - is covered the day it is declared instead of the day someone remembers to
61
+ // widen this rule. A server function is a public POST endpoint like any other, so this is what
62
+ // stops one shipping unauthenticated.
63
+ {
64
+ name: "authenticated-write",
65
+ match: { access: "write", zone: "domain" },
66
+ require: [NIFRA_ASSURANCE.AUTHENTICATED],
67
+ },
68
+ // Anything that changes state must validate its input at the boundary. This demo is read-only,
69
+ // so the rule classifies nothing yet - which is the point: the first POST added to `page.ts`
70
+ // fails the check unless it bounds its body.
71
+ {
72
+ name: "mutation",
73
+ match: { methods: ["POST", "PUT", "PATCH", "DELETE"] },
74
+ require: [NIFRA_ASSURANCE.BODY_BOUNDED],
75
+ },
76
+ // Reads carry no requirement yet. When some become private, split this into a public rule and
77
+ // an authenticated one rather than weakening the whole class.
78
+ { name: "read", match: { methods: ["GET"] }, require: [] },
79
+ ],
80
+ },
81
+ })
@@ -6,19 +6,20 @@
6
6
  "scripts": {
7
7
  "build": "bun run build.ts",
8
8
  "dev": "bunx wrangler dev",
9
- "dev:bun": "bun run server.ts"
9
+ "dev:bun": "bun run server.ts",
10
+ "check": "nifra check && nifra assure"
10
11
  },
11
12
  "dependencies": {
12
- "@nifrajs/client": "^2.1.0",
13
- "@nifrajs/core": "^2.1.0",
14
- "@nifrajs/schema": "^2.1.0",
15
- "@nifrajs/web": "^2.1.0",
16
- "@nifrajs/web-react": "^2.1.0",
13
+ "@nifrajs/client": "^2.3.0",
14
+ "@nifrajs/core": "^2.3.0",
15
+ "@nifrajs/schema": "^2.3.0",
16
+ "@nifrajs/web": "^2.3.0",
17
+ "@nifrajs/web-react": "^2.3.0",
17
18
  "react": "^19.0.0",
18
19
  "react-dom": "^19.0.0"
19
20
  },
20
21
  "devDependencies": {
21
- "@nifrajs/cli": "^2.1.0",
22
+ "@nifrajs/cli": "^2.3.0",
22
23
  "@types/react": "^19.0.0",
23
24
  "@types/react-dom": "^19.0.0",
24
25
  "typescript": "^6.0.3"
@@ -0,0 +1,12 @@
1
+ import { server } from "@nifrajs/core/server"
2
+ import { t } from "@nifrajs/schema"
3
+
4
+ // This demo counts server renders so the ISR cache is observable: a cache HIT serves stored bytes (the
5
+ // loader doesn't run, so the number holds), while a MISS or background REGENERATION runs it (it bumps).
6
+ // Replace it with real data (KV/D1/Postgres/…); on the edge, reach bindings via `c.env`.
7
+ let renders = 0
8
+
9
+ export const page = server().get("/page", { response: t.object({ renders: t.number() }) }, () => {
10
+ renders += 1
11
+ return { renders }
12
+ })
@@ -1,7 +1,7 @@
1
1
  export default function NotFound() {
2
2
  return (
3
3
  <section>
4
- <h2>404 — not found</h2>
4
+ <h2>404 - not found</h2>
5
5
  <p>
6
6
  <a href="/">← home</a>
7
7
  </p>
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from "react"
2
2
 
3
- /** Root layout — rendered inside the document shell `renderPage` emits. */
3
+ /** Root layout - rendered inside the document shell `renderPage` emits. */
4
4
  export default function Layout(props: { children?: ReactNode }) {
5
5
  return (
6
6
  <main
@@ -7,7 +7,7 @@ export const meta = {
7
7
  }
8
8
 
9
9
  // ISR: the `withISR` wrapper (worker.ts / server.ts) caches this page and serves it
10
- // stale-while-revalidate. `revalidate` is the freshness window in SECONDS — nifra emits it as the
10
+ // stale-while-revalidate. `revalidate` is the freshness window in SECONDS - nifra emits it as the
11
11
  // `x-nifra-isr-revalidate` header, which the wrapper reads to set this page's TTL. Loader/meta are
12
12
  // pure annotated exports, so they tree-shake out of the client bundle.
13
13
  export const revalidate = 10
@@ -24,7 +24,7 @@ export default function Home(props: { data: LoaderData<typeof loader> }) {
24
24
  server renders: <b>{props.data.renders}</b>
25
25
  </p>
26
26
  <p>
27
- Reload within {revalidate}s and this holds — you're served the cached page (response header{" "}
27
+ Reload within {revalidate}s and this holds - you're served the cached page (response header{" "}
28
28
  <code>x-nifra-isr: hit</code>). After the window, the next request gets the stale page
29
29
  instantly (<code>stale</code>) while a fresh copy regenerates behind it. Edit{" "}
30
30
  <code>routes/index.tsx</code> to begin.