@nifrajs/cli 3.3.0 → 4.0.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.
- package/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +7 -15
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +162 -713
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +58 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +323 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +1 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +145 -54
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +306 -87
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +60 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +298 -33
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts +3 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +56 -16
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diagnostics.d.ts +86 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +85 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +41 -21
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -4
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +93 -74
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +71 -16
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +5 -6
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +3 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +263 -131
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +20 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +113 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-path.d.ts +10 -0
- package/dist/mcp-path.d.ts.map +1 -0
- package/dist/mcp-path.js +51 -0
- package/dist/mcp-path.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-reflect.d.ts.map +1 -1
- package/dist/mcp-reflect.js +2 -2
- package/dist/mcp-reflect.js.map +1 -1
- package/dist/mcp-render.d.ts.map +1 -1
- package/dist/mcp-render.js +64 -45
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +124 -25
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +159 -55
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +18 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +165 -82
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +982 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +28 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +12 -2
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +34 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts +21 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +823 -0
- package/dist/review.js.map +1 -0
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts +3 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +829 -34
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +154 -90
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +8 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts +3 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +33 -16
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +4123 -548
- package/docs/types.json +14391 -10060
- package/package.json +19 -11
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +199 -797
- package/src/check-scan.ts +394 -63
- package/src/check.ts +146 -58
- package/src/cli.ts +347 -92
- package/src/command-catalog.ts +407 -45
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +66 -15
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diagnostics.ts +179 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +44 -31
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +93 -82
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +144 -32
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +5 -7
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +301 -161
- package/src/mcp-http.ts +22 -5
- package/src/mcp-io.ts +130 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-path.ts +50 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-reflect.ts +3 -2
- package/src/mcp-render.ts +72 -42
- package/src/mcp-root.ts +130 -24
- package/src/mcp-run.ts +190 -56
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +193 -91
- package/src/migrate-layout.ts +1121 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +42 -8
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +988 -0
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +947 -35
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +175 -98
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +27 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +40 -16
package/src/load.ts
CHANGED
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zero-config app discovery. The CLI reads a few conventions from the project root:
|
|
3
3
|
* - `routes/` - the file-based routes (per `@nifrajs/web/fs`).
|
|
4
|
-
* - `nifra.config.ts` - the CLI's framework wiring (see {@link NifraFramework});
|
|
5
|
-
* used as a fallback. One of the two is required.
|
|
6
|
-
* - `backend.ts` - exports `backend` (a `@nifrajs/core` server, the contract). Optional.
|
|
4
|
+
* - `nifra.config.ts` - the CLI's framework wiring (see {@link NifraFramework});
|
|
5
|
+
* `backend/framework.ts` is used as a fallback. One of the two is required.
|
|
6
|
+
* - `backend/app.ts` - exports `backend` (a `@nifrajs/core` server, the contract). Optional.
|
|
7
7
|
*
|
|
8
|
-
* Why two filenames: a multi-target app's edge entry (`_worker.ts`) imports `framework.ts`
|
|
9
|
-
* render adapter, so `framework.ts` must stay edge-bundlable - it can't reference
|
|
10
|
-
* SFC compiler (Bun would pull them into the `target:"browser"` worker and fail
|
|
11
|
-
* `nifra.config.ts` holds that build/dev tooling instead: it's imported ONLY
|
|
12
|
-
* Bun), so its Vite plugin + SFC-compiler imports (`@vitejs/plugin-vue`,
|
|
13
|
-
* reach the edge bundle. A simple single-target app with no edge
|
|
8
|
+
* Why two filenames: a multi-target app's edge entry (`_worker.ts`) imports `backend/framework.ts`
|
|
9
|
+
* for the render adapter, so `backend/framework.ts` must stay edge-bundlable - it can't reference
|
|
10
|
+
* Vite plugins or the SFC compiler (Bun would pull them into the `target:"browser"` worker and fail
|
|
11
|
+
* on `child_process`). `nifra.config.ts` holds that build/dev tooling instead: it's imported ONLY
|
|
12
|
+
* by this CLI (which runs on Bun), so its Vite plugin + SFC-compiler imports (`@vitejs/plugin-vue`,
|
|
13
|
+
* `@nifrajs/web-vue/plugin`) never reach the edge bundle. A simple single-target app with no edge
|
|
14
|
+
* build can put it all in `backend/framework.ts`.
|
|
14
15
|
*/
|
|
15
16
|
import { existsSync } from "node:fs"
|
|
16
17
|
import { isAbsolute, resolve } from "node:path"
|
|
18
|
+
import type { CreateWebAppOptions, CssLoadingMode } from "@nifrajs/web"
|
|
19
|
+
import type { SecretExemption } from "@nifrajs/web/build"
|
|
20
|
+
import { assertCurrentLayout, BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
17
21
|
import { checkPipelineSeparation } from "./pipeline-guard.ts"
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
|
-
* A plugin list - either an array, or a **thunk** that returns one (optionally async). Both forms
|
|
21
|
-
* accepted; {@link resolvePlugins} normalizes them. The thunk is just a convenience for
|
|
22
|
-
* plugin's construction - it does NOT make a plugin edge-safe (Bun.build follows a
|
|
23
|
-
* even from an unused export, so a Vite/compiler import in an edge-bundled file
|
|
24
|
-
* safety comes from WHERE the config lives: keep these fields in
|
|
25
|
-
* the edge-imported `framework.ts`. See the module
|
|
24
|
+
* A plugin list - either an array, or a **thunk** that returns one (optionally async). Both forms
|
|
25
|
+
* are accepted; {@link resolvePlugins} normalizes them. The thunk is just a convenience for
|
|
26
|
+
* deferring a plugin's construction - it does NOT make a plugin edge-safe (Bun.build follows a
|
|
27
|
+
* dynamic `import()` even from an unused export, so a Vite/compiler import in an edge-bundled file
|
|
28
|
+
* fails regardless). Edge safety comes from WHERE the config lives: keep these fields in
|
|
29
|
+
* `nifra.config.ts` (CLI-only), not in the edge-imported `backend/framework.ts`. See the module
|
|
30
|
+
* header.
|
|
26
31
|
*/
|
|
27
32
|
export type PluginsField =
|
|
28
33
|
| readonly unknown[]
|
|
29
34
|
| (() => readonly unknown[] | Promise<readonly unknown[]>)
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
|
-
* The CLI's framework wiring - exported from `nifra.config.ts` (or `framework.ts` as a
|
|
33
|
-
* `create-nifra` generates it. Only `adapter` + `clientModule` are required; the
|
|
34
|
-
* fields are framework-specific extras (Vue/Svelte/Solid need them; React/Preact
|
|
37
|
+
* The CLI's framework wiring - exported from `nifra.config.ts` (or `backend/framework.ts` as a
|
|
38
|
+
* fallback). `create-nifra` generates it. Only `adapter` + `clientModule` are required; the
|
|
39
|
+
* plugin/condition fields are framework-specific extras (Vue/Svelte/Solid need them; React/Preact
|
|
40
|
+
* don't).
|
|
35
41
|
*/
|
|
36
42
|
export interface NifraFramework {
|
|
37
43
|
/** The render adapter, e.g. `reactAdapter` from `@nifrajs/web-react`. */
|
|
@@ -41,7 +47,7 @@ export interface NifraFramework {
|
|
|
41
47
|
/** App-level middleware applied to the web app before its page routes are declared -
|
|
42
48
|
* e.g. `(app) => app.use(securityHeaders())`. Runs ahead of the `api`/`mounts` hooks.
|
|
43
49
|
*
|
|
44
|
-
* MUST be exported from `framework.ts`, not `nifra.config.ts`: `nifra build` emits a
|
|
50
|
+
* MUST be exported from `backend/framework.ts`, not `nifra.config.ts`: `nifra build` emits a
|
|
45
51
|
* server entry that imports it, and that entry is edge-bundled. See the module header. */
|
|
46
52
|
readonly use?: (app: never) => void
|
|
47
53
|
/** Vite plugins for `nifra dev`'s HMR, e.g. `[react()]` / `() => import("@vitejs/plugin-vue")…`. */
|
|
@@ -56,10 +62,88 @@ export interface NifraFramework {
|
|
|
56
62
|
readonly conditions?: readonly string[]
|
|
57
63
|
/** Compile-time `define` replacements, e.g. Vue's `__VUE_*` flags. */
|
|
58
64
|
readonly define?: Readonly<Record<string, string>>
|
|
65
|
+
/** The deploy target `nifra build` emits without `--target` (default `"bun"`); `nifra target <t>`
|
|
66
|
+
* rewrites it. */
|
|
67
|
+
readonly target?: string
|
|
68
|
+
/** `"platform"` makes a `cloudflare` / `vercel` build read `c.clientIp` from the header that
|
|
69
|
+
* platform's edge overwrites (`cf-connecting-ip` / `x-real-ip`). Left out, an edge build has no
|
|
70
|
+
* caller address, so a per-caller `rateLimit` refuses every request. Declare it only while that edge
|
|
71
|
+
* is the sole way in: served any other way the header is whatever the client sent. Self-hosting
|
|
72
|
+
* targets use the socket peer either way. */
|
|
73
|
+
readonly clientIp?: "platform"
|
|
74
|
+
/** `nifra dev` settings. `indicator: false` turns the in-page issues badge off for every run
|
|
75
|
+
* (`nifra dev --no-indicator` does it for one); browser errors still reach `nifra errors`. */
|
|
76
|
+
readonly dev?: { readonly indicator?: boolean }
|
|
59
77
|
/** Static files directory. Defaults to `<app>/public`; `false` disables it. */
|
|
60
78
|
readonly publicDir?: string | false
|
|
61
79
|
/** Client-visible environment prefix (default `"PUBLIC_"`; empty disables exposure). */
|
|
62
80
|
readonly publicEnvPrefix?: string
|
|
81
|
+
/** Reviewed false positives of `nifra build`'s secret scan, each `{ rule, file, reason }` (or
|
|
82
|
+
* `{ rule: "private-env-value", env, reason }`). There is no exemption by value. */
|
|
83
|
+
readonly secretExemptions?: readonly SecretExemption[]
|
|
84
|
+
/** Vite production CSS output policy. `false` emits one aggregate stylesheet for the whole client. */
|
|
85
|
+
readonly cssCodeSplit?: boolean
|
|
86
|
+
/** Framework-owned SSR stylesheet activation. Deferred mode is intended for aggregate CSS. */
|
|
87
|
+
readonly cssLoading?: CssLoadingMode
|
|
88
|
+
/** Path `backend/app.ts`'s backend is served at (default `"/api"`; `""` turns the mount off). A
|
|
89
|
+
* page file under it can never render, so the app refuses to start and `nifra build` refuses to
|
|
90
|
+
* build.
|
|
91
|
+
*
|
|
92
|
+
* The fields from here down reach the generated server entry, so like `use` they MUST be exported
|
|
93
|
+
* from `backend/framework.ts` (re-export them from `nifra.config.ts` when both exist). */
|
|
94
|
+
readonly apiPrefix?: string
|
|
95
|
+
/** Strip {@link apiPrefix} before the backend sees the path, for a backend that declares its routes
|
|
96
|
+
* without it. Default `false`. */
|
|
97
|
+
readonly apiStrip?: boolean
|
|
98
|
+
/** Apps mounted ahead of page routing - an auth handler, a webhook receiver, a backend served at an
|
|
99
|
+
* extra path. See `createWebApp`'s `mounts`. */
|
|
100
|
+
readonly mounts?: CreateWebAppOptions["mounts"]
|
|
101
|
+
/** A hash-based Content-Security-Policy from `createCspPolicy`. Exclusive with {@link nonce}. */
|
|
102
|
+
readonly csp?: CreateWebAppOptions["csp"]
|
|
103
|
+
/** A per-request CSP nonce resolver. Exclusive with {@link csp}. */
|
|
104
|
+
readonly nonce?: CreateWebAppOptions["nonce"]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The {@link NifraFramework} fields forwarded to `createWebApp` and imported by the generated server
|
|
108
|
+
* entry, in one list so every command that builds the app forwards the same set. */
|
|
109
|
+
export const FRAMEWORK_WEB_OPTIONS = ["apiPrefix", "apiStrip", "mounts", "csp", "nonce"] as const
|
|
110
|
+
export type FrameworkWebOption = (typeof FRAMEWORK_WEB_OPTIONS)[number]
|
|
111
|
+
|
|
112
|
+
/** Refuse a forwarded field of the wrong shape at load, naming the field, rather than letting it fail
|
|
113
|
+
* later inside `createWebApp` or the generated server entry. */
|
|
114
|
+
function assertFrameworkWebOptions(fw: Partial<NifraFramework>, configFile: string): void {
|
|
115
|
+
const wrong = (field: string, expected: string): never => {
|
|
116
|
+
throw new Error(`[nifra] ${configFile}: \`${field}\` must be ${expected}.`)
|
|
117
|
+
}
|
|
118
|
+
if (fw.apiPrefix !== undefined && typeof fw.apiPrefix !== "string") {
|
|
119
|
+
wrong("apiPrefix", 'a path string such as "/api", or "" to turn the backend mount off')
|
|
120
|
+
}
|
|
121
|
+
if (fw.apiStrip !== undefined && typeof fw.apiStrip !== "boolean") wrong("apiStrip", "a boolean")
|
|
122
|
+
if (fw.mounts !== undefined && !Array.isArray(fw.mounts)) {
|
|
123
|
+
wrong("mounts", "an array of { path, app } entries")
|
|
124
|
+
}
|
|
125
|
+
if (fw.csp !== undefined && (typeof fw.csp !== "object" || fw.csp === null)) {
|
|
126
|
+
wrong("csp", "the policy createCspPolicy() returns")
|
|
127
|
+
}
|
|
128
|
+
if (fw.nonce !== undefined && typeof fw.nonce !== "function") {
|
|
129
|
+
wrong("nonce", "a nonce resolver function, such as createNonceResolver() returns")
|
|
130
|
+
}
|
|
131
|
+
if (fw.secretExemptions !== undefined && !Array.isArray(fw.secretExemptions)) {
|
|
132
|
+
wrong("secretExemptions", "an array of { rule, file, reason } entries")
|
|
133
|
+
}
|
|
134
|
+
if (fw.target !== undefined && typeof fw.target !== "string") {
|
|
135
|
+
wrong("target", 'a deploy target name such as "bun"')
|
|
136
|
+
}
|
|
137
|
+
if (fw.clientIp !== undefined && fw.clientIp !== "platform") {
|
|
138
|
+
wrong("clientIp", '"platform" (trust the edge platform\'s client-address header), or left out')
|
|
139
|
+
}
|
|
140
|
+
if (fw.dev !== undefined) {
|
|
141
|
+
if (typeof fw.dev !== "object" || fw.dev === null)
|
|
142
|
+
wrong("dev", "an object such as { indicator: false }")
|
|
143
|
+
const indicator: unknown = Reflect.get(fw.dev, "indicator")
|
|
144
|
+
if (indicator !== undefined && typeof indicator !== "boolean")
|
|
145
|
+
wrong("dev.indicator", "a boolean")
|
|
146
|
+
}
|
|
63
147
|
}
|
|
64
148
|
|
|
65
149
|
/**
|
|
@@ -99,8 +183,9 @@ export interface ResolvedPlugins {
|
|
|
99
183
|
|
|
100
184
|
export interface LoadedApp {
|
|
101
185
|
readonly cwd: string
|
|
102
|
-
/** Absolute path of the config this app was loaded from - `nifra.config.ts`, or
|
|
103
|
-
* fallback. `nifra dev`'s Bun pipeline re-imports it from a
|
|
186
|
+
/** Absolute path of the config this app was loaded from - `nifra.config.ts`, or
|
|
187
|
+
* `backend/framework.ts` as the fallback. `nifra dev`'s Bun pipeline re-imports it from a
|
|
188
|
+
* GENERATED module so the app's own
|
|
104
189
|
* `clientPlugins` reach Bun's dev-server bundler, which takes plugins only as module paths. */
|
|
105
190
|
readonly configPath: string
|
|
106
191
|
readonly routesDir: string
|
|
@@ -109,7 +194,7 @@ export interface LoadedApp {
|
|
|
109
194
|
readonly framework: NifraFramework
|
|
110
195
|
/** Plugin thunks resolved exactly once during app loading and reused by every command phase. */
|
|
111
196
|
readonly resolvedPlugins: ResolvedPlugins
|
|
112
|
-
/** The `backend` export from `backend.ts`, or `undefined` if there's no `backend.ts`. */
|
|
197
|
+
/** The `backend` export from `backend/app.ts`, or `undefined` if there's no `backend/app.ts`. */
|
|
113
198
|
readonly backend: unknown
|
|
114
199
|
}
|
|
115
200
|
|
|
@@ -121,7 +206,8 @@ export interface LoadAppOptions {
|
|
|
121
206
|
/**
|
|
122
207
|
* Root-level monorepo config - exported from `nifra.config.ts` at the workspace root.
|
|
123
208
|
* Each key in `apps` is the short name used to namespace MCP tools (`nifra_<name>_context` etc.);
|
|
124
|
-
* each value is a path relative to the root that contains its own `nifra.config.ts` /
|
|
209
|
+
* each value is a path relative to the root that contains its own `nifra.config.ts` /
|
|
210
|
+
* `backend/framework.ts`.
|
|
125
211
|
*
|
|
126
212
|
* Example:
|
|
127
213
|
* ```ts
|
|
@@ -172,23 +258,48 @@ const isAdapter = (v: unknown): boolean => typeof v === "object" && v !== null
|
|
|
172
258
|
const importWithQuery = (path: string, query: string | undefined): Promise<unknown> =>
|
|
173
259
|
import(query === undefined || query === "" ? path : `${path}?${query}`)
|
|
174
260
|
|
|
175
|
-
/**
|
|
176
|
-
*
|
|
261
|
+
/**
|
|
262
|
+
* What reads only the backend (`nifra openapi`): the whole app when it has a web config, else
|
|
263
|
+
* `backend/app.ts` alone, so an API-only app needs no adapter to describe its routes.
|
|
264
|
+
*/
|
|
265
|
+
export async function loadBackendApp(
|
|
266
|
+
cwd: string,
|
|
267
|
+
options: LoadAppOptions = {},
|
|
268
|
+
): Promise<Pick<LoadedApp, "cwd" | "backend">> {
|
|
269
|
+
if (existsSync(resolve(cwd, CONFIG_FILE)) || existsSync(resolve(cwd, FRAMEWORK_FILE))) {
|
|
270
|
+
return loadApp(cwd, "dist", options)
|
|
271
|
+
}
|
|
272
|
+
assertCurrentLayout(cwd)
|
|
273
|
+
const backendPath = resolve(cwd, BACKEND_APP_FILE)
|
|
274
|
+
if (!existsSync(backendPath)) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`[nifra] no ${BACKEND_APP_FILE} in ${cwd}: the OpenAPI document describes its routes`,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
const backend = (
|
|
280
|
+
(await importWithQuery(backendPath, options.importQuery)) as { backend?: unknown }
|
|
281
|
+
).backend
|
|
282
|
+
return { cwd, backend }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Discover + validate the app conventions rooted at `cwd`. Prefers `nifra.config.ts`, falls back
|
|
286
|
+
* to
|
|
287
|
+
* `backend/framework.ts`. Throws a clear, actionable error if neither exists or the config is
|
|
288
|
+
* malformed. */
|
|
177
289
|
export async function loadApp(
|
|
178
290
|
cwd: string,
|
|
179
291
|
outDirName = "dist",
|
|
180
292
|
options: LoadAppOptions = {},
|
|
181
293
|
): Promise<LoadedApp> {
|
|
182
294
|
// nifra.config.ts is the CLI's config (it may import Vite plugins / the SFC compiler, which a
|
|
183
|
-
// multi-target app keeps OUT of the edge-imported framework.ts). framework.ts is
|
|
184
|
-
// simple single-target app that has no edge build.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
: "framework.ts"
|
|
295
|
+
// multi-target app keeps OUT of the edge-imported backend/framework.ts). backend/framework.ts is
|
|
296
|
+
// the fallback for a simple single-target app that has no edge build.
|
|
297
|
+
assertCurrentLayout(cwd)
|
|
298
|
+
const configFile = existsSync(resolve(cwd, CONFIG_FILE)) ? CONFIG_FILE : FRAMEWORK_FILE
|
|
188
299
|
const configPath = resolve(cwd, configFile)
|
|
189
300
|
if (!existsSync(configPath)) {
|
|
190
301
|
throw new Error(
|
|
191
|
-
`[nifra] no
|
|
302
|
+
`[nifra] no ${CONFIG_FILE} or ${FRAMEWORK_FILE} found in ${cwd}.\n` +
|
|
192
303
|
" nifra is zero-config but needs one of them exporting at least:\n" +
|
|
193
304
|
" export const adapter = reactAdapter // from @nifrajs/web-react\n" +
|
|
194
305
|
' export const clientModule = "@nifrajs/web-react/client"\n' +
|
|
@@ -206,6 +317,7 @@ export async function loadApp(
|
|
|
206
317
|
`[nifra] ${configFile} must export \`adapter\` (a render adapter object) and \`clientModule\` (a string).`,
|
|
207
318
|
)
|
|
208
319
|
}
|
|
320
|
+
assertFrameworkWebOptions(fw, configFile)
|
|
209
321
|
// A `clientModule` given as a RELATIVE path (a local client entry, e.g. `./src/client.tsx`) is
|
|
210
322
|
// resolved to absolute here. It is embedded verbatim as an import specifier in the generated client
|
|
211
323
|
// entry, and `nifra dev` and `nifra build` write that entry into DIFFERENT directories - so a relative
|
|
@@ -246,7 +358,7 @@ export async function loadApp(
|
|
|
246
358
|
assertPipelineSeparation(resolvedPlugins, configFile)
|
|
247
359
|
|
|
248
360
|
let backend: unknown
|
|
249
|
-
const backendPath = resolve(cwd,
|
|
361
|
+
const backendPath = resolve(cwd, BACKEND_APP_FILE)
|
|
250
362
|
if (existsSync(backendPath)) {
|
|
251
363
|
backend = ((await importWithQuery(backendPath, options.importQuery)) as { backend?: unknown })
|
|
252
364
|
.backend
|
package/src/manifest-tool.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
serializeNifraManifestSignature,
|
|
13
13
|
signNifraManifest,
|
|
14
14
|
} from "@nifrajs/core/manifest"
|
|
15
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
15
16
|
import { collectProjectVerification } from "./verification.ts"
|
|
16
17
|
|
|
17
18
|
export const DEFAULT_MANIFEST_FILE = "nifra.manifest.json"
|
|
@@ -23,8 +24,8 @@ export function formatManifestDiff(diff: NifraManifestDiff): string {
|
|
|
23
24
|
.sort(
|
|
24
25
|
(a, b) =>
|
|
25
26
|
weight[a.severity] - weight[b.severity] ||
|
|
26
|
-
a.path
|
|
27
|
-
a.method
|
|
27
|
+
codeUnitOrder(a.path, b.path) ||
|
|
28
|
+
codeUnitOrder(a.method, b.method),
|
|
28
29
|
)
|
|
29
30
|
.map(
|
|
30
31
|
(change) =>
|
package/src/mcp-app-tool.ts
CHANGED
|
@@ -54,6 +54,8 @@ export function examplesAppTool(loadExamples: () => Promise<Example[] | undefine
|
|
|
54
54
|
annotations: {
|
|
55
55
|
title: "Browse the nifra example gallery",
|
|
56
56
|
readOnlyHint: true,
|
|
57
|
+
destructiveHint: false,
|
|
58
|
+
idempotentHint: true,
|
|
57
59
|
openWorldHint: false,
|
|
58
60
|
},
|
|
59
61
|
description:
|
package/src/mcp-context.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { realpathSync } from "node:fs"
|
|
9
9
|
import { readFile } from "node:fs/promises"
|
|
10
|
-
import { basename, isAbsolute, relative, resolve
|
|
10
|
+
import { basename, isAbsolute, relative, resolve } from "node:path"
|
|
11
11
|
import type { LoadedApp } from "./load.ts"
|
|
12
12
|
import { createCachedAppLoader } from "./mcp-exec.ts"
|
|
13
|
+
import { resolveMcpProjectPath } from "./mcp-path.ts"
|
|
13
14
|
import type { McpPrompt, McpResource, McpServerFeatures, McpTool } from "./mcp-protocol.ts"
|
|
14
15
|
|
|
15
16
|
function openApiFormat(args: Record<string, unknown>): "json" | "yaml" {
|
|
@@ -31,11 +32,8 @@ async function readProjectFile(
|
|
|
31
32
|
relativeFile: string,
|
|
32
33
|
maxChars: number,
|
|
33
34
|
): Promise<string> {
|
|
34
|
-
const target =
|
|
35
|
-
|
|
36
|
-
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
37
|
-
throw new Error(`refusing to read outside project root: ${relativeFile}`)
|
|
38
|
-
}
|
|
35
|
+
const target = resolveMcpProjectPath(cwd, relativeFile)
|
|
36
|
+
if (target === null) throw new Error(`refusing to read outside project root: ${relativeFile}`)
|
|
39
37
|
try {
|
|
40
38
|
const text = await readFile(target, "utf8")
|
|
41
39
|
return text.length <= maxChars
|
|
@@ -76,7 +74,7 @@ export function projectResources(
|
|
|
76
74
|
{
|
|
77
75
|
uri: "nifra://openapi",
|
|
78
76
|
name: "OpenAPI 3.1",
|
|
79
|
-
description: "OpenAPI document generated from backend.ts using @nifrajs/schema.",
|
|
77
|
+
description: "OpenAPI document generated from backend/app.ts using @nifrajs/schema.",
|
|
80
78
|
mimeType: "application/json",
|
|
81
79
|
read: async () => ({ text: await openApiHandler({ format: "json" }, loadAppCached) }),
|
|
82
80
|
},
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One nifra per MCP session: the version the project installs.
|
|
3
|
+
*
|
|
4
|
+
* A client that spawns a globally installed `nifra mcp` against a project pinned to a different
|
|
5
|
+
* release gets types, checks, docs and contracts for a surface the project does not have - and every
|
|
6
|
+
* answer still reads as authoritative. So the server settles the version before it serves:
|
|
7
|
+
*
|
|
8
|
+
* 1. The project installs its own `@nifrajs/cli` at a different version: hand the whole session to
|
|
9
|
+
* that CLI (`node_modules/.bin/nifra mcp`, stdio inherited) and step aside.
|
|
10
|
+
* 2. Handing off is impossible (no project CLI, a missing bin, or this process IS the hand-off and
|
|
11
|
+
* the versions still disagree): serve, but the version-sensitive tools refuse with the fix
|
|
12
|
+
* instead of answering for the wrong release.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from "node:fs"
|
|
16
|
+
import { readFile } from "node:fs/promises"
|
|
17
|
+
import { dirname, join } from "node:path"
|
|
18
|
+
import type { McpTool, McpToolResult } from "./mcp-protocol.ts"
|
|
19
|
+
import type { ToolingDrift } from "./mcp-root.ts"
|
|
20
|
+
|
|
21
|
+
/** Set on the hand-off child so it never hands off again, whatever it finds. */
|
|
22
|
+
export const MCP_DELEGATED_ENV = "NIFRA_MCP_DELEGATED"
|
|
23
|
+
|
|
24
|
+
/** The tools whose answer is a property of the nifra release: they refuse under a version split. */
|
|
25
|
+
export const VERSION_SENSITIVE_TOOLS: ReadonlySet<string> = new Set([
|
|
26
|
+
"nifra_check",
|
|
27
|
+
"nifra_types",
|
|
28
|
+
"nifra_docs",
|
|
29
|
+
"nifra_example",
|
|
30
|
+
"nifra_assure",
|
|
31
|
+
"nifra_contracts",
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
/** The project's own CLI: its version and, when the install linked one, its bin. */
|
|
35
|
+
export interface ProjectCli {
|
|
36
|
+
readonly version: string
|
|
37
|
+
readonly bin?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BIN_NAME = process.platform === "win32" ? "nifra.exe" : "nifra"
|
|
41
|
+
const MAX_ANCESTOR_DIRS = 8
|
|
42
|
+
|
|
43
|
+
/** The nearest `@nifrajs/cli` install at or above `root` (a monorepo app hoists to the repo root). */
|
|
44
|
+
export async function findProjectCli(root: string): Promise<ProjectCli | undefined> {
|
|
45
|
+
let dir = root
|
|
46
|
+
for (let depth = 0; depth < MAX_ANCESTOR_DIRS; depth++) {
|
|
47
|
+
const modules = join(dir, "node_modules")
|
|
48
|
+
try {
|
|
49
|
+
const meta = JSON.parse(
|
|
50
|
+
await readFile(join(modules, "@nifrajs", "cli", "package.json"), "utf8"),
|
|
51
|
+
) as { version?: unknown }
|
|
52
|
+
if (typeof meta.version === "string" && meta.version.length > 0) {
|
|
53
|
+
const bin = join(modules, ".bin", BIN_NAME)
|
|
54
|
+
return existsSync(bin) ? { version: meta.version, bin } : { version: meta.version }
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Not installed at this level - keep walking up.
|
|
58
|
+
}
|
|
59
|
+
const parent = dirname(dir)
|
|
60
|
+
if (parent === dir) return undefined
|
|
61
|
+
dir = parent
|
|
62
|
+
}
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Hand this stdio session to the project's own CLI when it installs a different version. Resolves to
|
|
68
|
+
* the child's exit code once the session ends, or `undefined` when this process should serve itself
|
|
69
|
+
* (versions agree, no project CLI or bin, already a hand-off, or the spawn failed). Must run before
|
|
70
|
+
* anything reads stdin: the child inherits the descriptor and owns the whole conversation.
|
|
71
|
+
*/
|
|
72
|
+
export async function delegateToProjectCli(options: {
|
|
73
|
+
readonly cwd: string
|
|
74
|
+
readonly root: string
|
|
75
|
+
readonly version: string
|
|
76
|
+
readonly args: readonly string[]
|
|
77
|
+
}): Promise<number | undefined> {
|
|
78
|
+
if (process.env[MCP_DELEGATED_ENV] === "1") return undefined
|
|
79
|
+
const cli = await findProjectCli(options.root)
|
|
80
|
+
if (cli === undefined || cli.version === options.version || cli.bin === undefined)
|
|
81
|
+
return undefined
|
|
82
|
+
return handOffSession([cli.bin, "mcp", ...options.args], {
|
|
83
|
+
cwd: options.cwd,
|
|
84
|
+
env: { ...process.env, [MCP_DELEGATED_ENV]: "1" },
|
|
85
|
+
announce: `[nifra] mcp: this CLI is ${options.version}; the project installs @nifrajs/cli ${cli.version} - serving this session from ${cli.bin}`,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hand this stdio session to `command`: the child inherits stdin, stdout and stderr and owns the
|
|
91
|
+
* whole conversation, and this process forwards signals and waits. Resolves to the child's exit code,
|
|
92
|
+
* or `undefined` when it could not start. Must run before anything reads stdin.
|
|
93
|
+
*/
|
|
94
|
+
export async function handOffSession(
|
|
95
|
+
command: readonly string[],
|
|
96
|
+
options: {
|
|
97
|
+
readonly cwd: string
|
|
98
|
+
readonly env: Record<string, string | undefined>
|
|
99
|
+
/** Written to stderr once the child runs. */
|
|
100
|
+
readonly announce?: string
|
|
101
|
+
},
|
|
102
|
+
): Promise<number | undefined> {
|
|
103
|
+
let child: ReturnType<typeof Bun.spawn>
|
|
104
|
+
try {
|
|
105
|
+
child = Bun.spawn([...command], {
|
|
106
|
+
cwd: options.cwd,
|
|
107
|
+
env: options.env,
|
|
108
|
+
stdin: "inherit",
|
|
109
|
+
stdout: "inherit",
|
|
110
|
+
stderr: "inherit",
|
|
111
|
+
})
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
// stderr only: stdout is the JSON-RPC channel, and it now belongs to the child.
|
|
116
|
+
if (options.announce !== undefined) process.stderr.write(`${options.announce}\n`)
|
|
117
|
+
const forward = (signal: NodeJS.Signals) => () => child.kill(signal)
|
|
118
|
+
const onTerm = forward("SIGTERM")
|
|
119
|
+
const onInt = forward("SIGINT")
|
|
120
|
+
process.on("SIGTERM", onTerm)
|
|
121
|
+
process.on("SIGINT", onInt)
|
|
122
|
+
try {
|
|
123
|
+
return await child.exited
|
|
124
|
+
} finally {
|
|
125
|
+
process.off("SIGTERM", onTerm)
|
|
126
|
+
process.off("SIGINT", onInt)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const refusalFix = (drift: ToolingDrift): string =>
|
|
131
|
+
drift.package === "@nifrajs/cli"
|
|
132
|
+
? "Fix: point the MCP client at the project's own CLI (`./node_modules/.bin/nifra mcp`, reinstalling if that file is missing), or restart this server from the project directory so it hands the session over."
|
|
133
|
+
: `Fix: add the matching CLI to the project (\`bun add -d @nifrajs/cli@${drift.project}\`) and restart the MCP server - it then hands the session to the project's CLI - or point the client at a nifra ${drift.project} install.`
|
|
134
|
+
|
|
135
|
+
/** The in-band refusal a version-sensitive tool returns under a version split. */
|
|
136
|
+
export const versionRefusal = (tool: string, drift: ToolingDrift): McpToolResult => ({
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text:
|
|
141
|
+
`${tool} refused: this MCP server runs nifra CLI ${drift.cli}, but the project installs ${drift.package} ${drift.project}, ` +
|
|
142
|
+
`so its answer would describe a different nifra than your code builds with. ${refusalFix(drift)}`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
isError: true,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Swap each version-sensitive tool's handler for the refusal while `drift` holds. Apply it before
|
|
150
|
+
* namespacing, so a monorepo's `nifra_<app>_check` refuses too. No drift: the tools pass through.
|
|
151
|
+
*/
|
|
152
|
+
export function refuseVersionSensitive(
|
|
153
|
+
tools: McpTool[],
|
|
154
|
+
drift: ToolingDrift | undefined,
|
|
155
|
+
): McpTool[] {
|
|
156
|
+
if (drift === undefined) return tools
|
|
157
|
+
return tools.map((tool) =>
|
|
158
|
+
VERSION_SENSITIVE_TOOLS.has(tool.name)
|
|
159
|
+
? { ...tool, handler: async () => versionRefusal(tool.name, drift) }
|
|
160
|
+
: tool,
|
|
161
|
+
)
|
|
162
|
+
}
|
package/src/mcp-docs-tools.ts
CHANGED
|
@@ -28,7 +28,13 @@ export function docsTools(
|
|
|
28
28
|
// The annotations state that for MCP hosts (and directory reviews check for them).
|
|
29
29
|
{
|
|
30
30
|
name: "nifra_docs",
|
|
31
|
-
annotations: {
|
|
31
|
+
annotations: {
|
|
32
|
+
title: "Search nifra docs",
|
|
33
|
+
readOnlyHint: true,
|
|
34
|
+
destructiveHint: false,
|
|
35
|
+
idempotentHint: true,
|
|
36
|
+
openWorldHint: false,
|
|
37
|
+
},
|
|
32
38
|
description:
|
|
33
39
|
"Search nifra's framework documentation and get back ONLY the matching sections - auth, uploads, ISR, WebSockets, loaders, deployment, anything. Call with no query for the cheap section index; pass query (e.g. \"isr revalidate\") for the top matching sections. Use this instead of reading llms-full.txt (~150 KB) whole. For the EXACT TypeScript of a type/interface/function, call nifra_types instead (don't read .d.ts files).",
|
|
34
40
|
inputSchema: {
|
|
@@ -57,10 +63,12 @@ export function docsTools(
|
|
|
57
63
|
annotations: {
|
|
58
64
|
title: "Get a verified nifra example",
|
|
59
65
|
readOnlyHint: true,
|
|
66
|
+
destructiveHint: false,
|
|
67
|
+
idempotentHint: true,
|
|
60
68
|
openWorldHint: false,
|
|
61
69
|
},
|
|
62
70
|
description:
|
|
63
|
-
'Get a
|
|
71
|
+
'Get a verified, copy-pasteable Nifra code example for a task - auth route, file upload, ISR page, loader/action, typed client, SSE, deployment, and more. Every snippet is typechecked against the installed Nifra version, so it compiles as-is. Returns code as text for direct use. Call with no query for the grouped index; pass query (e.g. "protected route", "upload", "isr revalidate") for matching snippets.',
|
|
64
72
|
inputSchema: {
|
|
65
73
|
type: "object",
|
|
66
74
|
properties: {
|
|
@@ -84,9 +92,15 @@ export function docsTools(
|
|
|
84
92
|
},
|
|
85
93
|
{
|
|
86
94
|
name: "nifra_types",
|
|
87
|
-
annotations: {
|
|
95
|
+
annotations: {
|
|
96
|
+
title: "Look up nifra API types",
|
|
97
|
+
readOnlyHint: true,
|
|
98
|
+
destructiveHint: false,
|
|
99
|
+
idempotentHint: true,
|
|
100
|
+
openWorldHint: false,
|
|
101
|
+
},
|
|
88
102
|
description:
|
|
89
|
-
'Get the
|
|
103
|
+
'Get the exact TypeScript declaration of any exported @nifrajs/* symbol - interface, type, class, function, or const. Each signature is generated from the package\'s built .d.ts and returns the complete declaration rather than prose. Pass `name` for an exact symbol (e.g. "RateLimitStore", "RouteSchema", "Context", "rateLimit"); pass `query` to search by keyword; omit both for the per-package index of names. A `name` lookup is always the complete declaration; a `query` returns a one-line summary plus the signature, collapsing an oversized body - pass `full: true` to override.',
|
|
90
104
|
inputSchema: {
|
|
91
105
|
type: "object",
|
|
92
106
|
properties: {
|
|
@@ -132,6 +146,8 @@ export function docsTools(
|
|
|
132
146
|
annotations: {
|
|
133
147
|
title: "Guided nifra learning path",
|
|
134
148
|
readOnlyHint: true,
|
|
149
|
+
destructiveHint: false,
|
|
150
|
+
idempotentHint: true,
|
|
135
151
|
openWorldHint: false,
|
|
136
152
|
},
|
|
137
153
|
description:
|
|
@@ -153,10 +169,12 @@ export function docsTools(
|
|
|
153
169
|
annotations: {
|
|
154
170
|
title: "Frontend footgun guidance",
|
|
155
171
|
readOnlyHint: true,
|
|
172
|
+
destructiveHint: false,
|
|
173
|
+
idempotentHint: true,
|
|
156
174
|
openWorldHint: false,
|
|
157
175
|
},
|
|
158
176
|
description:
|
|
159
|
-
'Diagnose a
|
|
177
|
+
'Diagnose a client-side Nifra problem by symptom across every adapter (React, Preact, Solid, Vue, Svelte, and vanilla). Returns the likely cause, a concrete fix, and how to verify it. Covers the Nifra server/client seam, hydration mismatches, duplicated framework runtimes, loader-data typing, and adapter-specific reactivity guidance. Call with no args for the index; pass `symptom` (e.g. "hydration mismatch", "value stopped updating") and/or `adapter` to filter.',
|
|
160
178
|
inputSchema: {
|
|
161
179
|
type: "object",
|
|
162
180
|
properties: {
|