@nifrajs/cli 4.0.2 → 5.1.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 +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -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 +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/load.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type PluginsField =
|
|
|
40
40
|
* don't).
|
|
41
41
|
*/
|
|
42
42
|
export interface NifraFramework {
|
|
43
|
+
/** Optional coordinated Vite 8 component graph. Tooling stays in nifra.config.ts. */
|
|
44
|
+
readonly componentPipeline?: Omit<
|
|
45
|
+
import("@nifrajs/web/server-components/build").ComponentPipelineOptions,
|
|
46
|
+
"root" | "outDir" | "port" | "plugins"
|
|
47
|
+
>
|
|
48
|
+
readonly serverComponents?: CreateWebAppOptions["serverComponents"]
|
|
43
49
|
/** The render adapter, e.g. `reactAdapter` from `@nifrajs/web-react`. */
|
|
44
50
|
readonly adapter: unknown
|
|
45
51
|
/** The client runtime module providing `mountRouter`, e.g. `"@nifrajs/web-react/client"`. */
|
|
@@ -72,8 +78,11 @@ export interface NifraFramework {
|
|
|
72
78
|
* targets use the socket peer either way. */
|
|
73
79
|
readonly clientIp?: "platform"
|
|
74
80
|
/** `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
|
-
|
|
81
|
+
* (`nifra dev --no-indicator` does it for one); browser errors still reach `nifra errors`.
|
|
82
|
+
* `allowedHosts` lists host names the Bun dev server answers besides loopback ones; it refuses
|
|
83
|
+
* every other `Host` so a DNS-rebound page cannot read it. The Vite pipeline uses Vite's own
|
|
84
|
+
* `server.allowedHosts`. */
|
|
85
|
+
readonly dev?: { readonly indicator?: boolean; readonly allowedHosts?: readonly string[] }
|
|
77
86
|
/** Static files directory. Defaults to `<app>/public`; `false` disables it. */
|
|
78
87
|
readonly publicDir?: string | false
|
|
79
88
|
/** Client-visible environment prefix (default `"PUBLIC_"`; empty disables exposure). */
|
|
@@ -85,9 +94,9 @@ export interface NifraFramework {
|
|
|
85
94
|
readonly cssCodeSplit?: boolean
|
|
86
95
|
/** Framework-owned SSR stylesheet activation. Deferred mode is intended for aggregate CSS. */
|
|
87
96
|
readonly cssLoading?: CssLoadingMode
|
|
88
|
-
/** Path `backend/app.ts`'s backend is served at
|
|
89
|
-
* page file under it can never render, so the app refuses to start and `nifra build`
|
|
90
|
-
* build.
|
|
97
|
+
/** Path `backend/app.ts`'s backend is served at over HTTP, such as `"/api"`; unset or `""`, it is
|
|
98
|
+
* not mounted. A page file under it can never render, so the app refuses to start and `nifra build`
|
|
99
|
+
* refuses to build.
|
|
91
100
|
*
|
|
92
101
|
* The fields from here down reach the generated server entry, so like `use` they MUST be exported
|
|
93
102
|
* from `backend/framework.ts` (re-export them from `nifra.config.ts` when both exist). */
|
|
@@ -106,7 +115,14 @@ export interface NifraFramework {
|
|
|
106
115
|
|
|
107
116
|
/** The {@link NifraFramework} fields forwarded to `createWebApp` and imported by the generated server
|
|
108
117
|
* entry, in one list so every command that builds the app forwards the same set. */
|
|
109
|
-
export const FRAMEWORK_WEB_OPTIONS = [
|
|
118
|
+
export const FRAMEWORK_WEB_OPTIONS = [
|
|
119
|
+
"apiPrefix",
|
|
120
|
+
"apiStrip",
|
|
121
|
+
"mounts",
|
|
122
|
+
"csp",
|
|
123
|
+
"nonce",
|
|
124
|
+
"serverComponents",
|
|
125
|
+
] as const
|
|
110
126
|
export type FrameworkWebOption = (typeof FRAMEWORK_WEB_OPTIONS)[number]
|
|
111
127
|
|
|
112
128
|
/** Refuse a forwarded field of the wrong shape at load, naming the field, rather than letting it fail
|
|
@@ -115,6 +131,24 @@ function assertFrameworkWebOptions(fw: Partial<NifraFramework>, configFile: stri
|
|
|
115
131
|
const wrong = (field: string, expected: string): never => {
|
|
116
132
|
throw new Error(`[nifra] ${configFile}: \`${field}\` must be ${expected}.`)
|
|
117
133
|
}
|
|
134
|
+
if (fw.componentPipeline !== undefined) {
|
|
135
|
+
const pipeline = fw.componentPipeline
|
|
136
|
+
if (
|
|
137
|
+
typeof pipeline !== "object" ||
|
|
138
|
+
pipeline === null ||
|
|
139
|
+
!["react", "vue", "svelte", "solid", "preact"].includes(pipeline.adapter) ||
|
|
140
|
+
typeof pipeline.entries !== "object" ||
|
|
141
|
+
pipeline.entries === null ||
|
|
142
|
+
typeof pipeline.entries.client !== "string" ||
|
|
143
|
+
typeof pipeline.entries.ssr !== "string" ||
|
|
144
|
+
(pipeline.adapter === "react" && typeof pipeline.entries.rsc !== "string") ||
|
|
145
|
+
(pipeline.adapter !== "react" && pipeline.entries.rsc !== undefined)
|
|
146
|
+
)
|
|
147
|
+
wrong(
|
|
148
|
+
"componentPipeline",
|
|
149
|
+
"an adapter plus existing client/ssr entries and a React-only rsc entry",
|
|
150
|
+
)
|
|
151
|
+
}
|
|
118
152
|
if (fw.apiPrefix !== undefined && typeof fw.apiPrefix !== "string") {
|
|
119
153
|
wrong("apiPrefix", 'a path string such as "/api", or "" to turn the backend mount off')
|
|
120
154
|
}
|
|
@@ -143,6 +177,13 @@ function assertFrameworkWebOptions(fw: Partial<NifraFramework>, configFile: stri
|
|
|
143
177
|
const indicator: unknown = Reflect.get(fw.dev, "indicator")
|
|
144
178
|
if (indicator !== undefined && typeof indicator !== "boolean")
|
|
145
179
|
wrong("dev.indicator", "a boolean")
|
|
180
|
+
const allowedHosts: unknown = Reflect.get(fw.dev, "allowedHosts")
|
|
181
|
+
if (
|
|
182
|
+
allowedHosts !== undefined &&
|
|
183
|
+
(!Array.isArray(allowedHosts) ||
|
|
184
|
+
allowedHosts.some((host) => typeof host !== "string" || host.trim() === ""))
|
|
185
|
+
)
|
|
186
|
+
wrong("dev.allowedHosts", 'an array of host names such as ["app.test"]')
|
|
146
187
|
}
|
|
147
188
|
}
|
|
148
189
|
|
|
@@ -198,6 +239,23 @@ export interface LoadedApp {
|
|
|
198
239
|
readonly backend: unknown
|
|
199
240
|
}
|
|
200
241
|
|
|
242
|
+
/** An API-only app as the backend readers see it: `backend/app.ts` and no web config. */
|
|
243
|
+
export interface BackendOnlyApp {
|
|
244
|
+
readonly cwd: string
|
|
245
|
+
readonly backend: unknown
|
|
246
|
+
readonly framework?: undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** What the backend readers (routes, context, hooks, OpenAPI, the app's MCP surface) load. */
|
|
250
|
+
export type IntrospectedApp = LoadedApp | BackendOnlyApp
|
|
251
|
+
|
|
252
|
+
/** How a command reaches the app. The MCP server hands out one whose calls both refuse. */
|
|
253
|
+
export interface AppLoader {
|
|
254
|
+
(outDirName?: string): Promise<LoadedApp>
|
|
255
|
+
/** The whole app with a web config, else `backend/app.ts` alone. */
|
|
256
|
+
readonly backend: () => Promise<IntrospectedApp>
|
|
257
|
+
}
|
|
258
|
+
|
|
201
259
|
export interface LoadAppOptions {
|
|
202
260
|
/** Optional dynamic-import query used by long-lived MCP processes to bust Bun's module cache. */
|
|
203
261
|
readonly importQuery?: string
|
|
@@ -258,22 +316,24 @@ const isAdapter = (v: unknown): boolean => typeof v === "object" && v !== null
|
|
|
258
316
|
const importWithQuery = (path: string, query: string | undefined): Promise<unknown> =>
|
|
259
317
|
import(query === undefined || query === "" ? path : `${path}?${query}`)
|
|
260
318
|
|
|
319
|
+
/** Whether `cwd` has a web config file, the one thing that makes it more than its backend. */
|
|
320
|
+
export const hasAppConfig = (cwd: string): boolean =>
|
|
321
|
+
existsSync(resolve(cwd, CONFIG_FILE)) || existsSync(resolve(cwd, FRAMEWORK_FILE))
|
|
322
|
+
|
|
261
323
|
/**
|
|
262
|
-
* What reads only the backend
|
|
263
|
-
*
|
|
324
|
+
* What reads only the backend: the whole app when it has a web config, else `backend/app.ts`
|
|
325
|
+
* alone, so an API-only app needs no adapter for its routes, context, hooks or OpenAPI document.
|
|
264
326
|
*/
|
|
265
327
|
export async function loadBackendApp(
|
|
266
328
|
cwd: string,
|
|
267
329
|
options: LoadAppOptions = {},
|
|
268
|
-
): Promise<
|
|
269
|
-
if (
|
|
270
|
-
return loadApp(cwd, "dist", options)
|
|
271
|
-
}
|
|
330
|
+
): Promise<IntrospectedApp> {
|
|
331
|
+
if (hasAppConfig(cwd)) return loadApp(cwd, "dist", options)
|
|
272
332
|
assertCurrentLayout(cwd)
|
|
273
333
|
const backendPath = resolve(cwd, BACKEND_APP_FILE)
|
|
274
334
|
if (!existsSync(backendPath)) {
|
|
275
335
|
throw new Error(
|
|
276
|
-
`[nifra] no ${BACKEND_APP_FILE} in ${cwd}:
|
|
336
|
+
`[nifra] no ${CONFIG_FILE}, ${FRAMEWORK_FILE} or ${BACKEND_APP_FILE} in ${cwd}: a web app needs its config, an API-only app its ${BACKEND_APP_FILE}`,
|
|
277
337
|
)
|
|
278
338
|
}
|
|
279
339
|
const backend = (
|
|
@@ -282,9 +342,31 @@ export async function loadBackendApp(
|
|
|
282
342
|
return { cwd, backend }
|
|
283
343
|
}
|
|
284
344
|
|
|
345
|
+
/** The fields the generated server entry imports from backend/framework.ts. */
|
|
346
|
+
const SERVER_OPTIONS = [...FRAMEWORK_WEB_OPTIONS, "use"] as const
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* `config` with each server option it leaves unset taken from backend/framework.ts, so `nifra dev` and
|
|
350
|
+
* the server entry `nifra build` generates read one value without nifra.config.ts re-exporting it.
|
|
351
|
+
*/
|
|
352
|
+
async function withServerOptions(
|
|
353
|
+
config: Partial<NifraFramework>,
|
|
354
|
+
frameworkPath: string,
|
|
355
|
+
importQuery: string | undefined,
|
|
356
|
+
): Promise<Partial<NifraFramework>> {
|
|
357
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a config module, read like nifra.config.ts; every web option taken from it is validated below.
|
|
358
|
+
const framework = (await importWithQuery(frameworkPath, importQuery)) as Partial<NifraFramework>
|
|
359
|
+
const taken: Record<string, unknown> = {}
|
|
360
|
+
for (const name of SERVER_OPTIONS) {
|
|
361
|
+
if (config[name] === undefined && framework[name] !== undefined) taken[name] = framework[name]
|
|
362
|
+
}
|
|
363
|
+
assertFrameworkWebOptions(taken, FRAMEWORK_FILE)
|
|
364
|
+
return { ...config, ...taken }
|
|
365
|
+
}
|
|
366
|
+
|
|
285
367
|
/** Discover + validate the app conventions rooted at `cwd`. Prefers `nifra.config.ts`, falls back
|
|
286
|
-
* to
|
|
287
|
-
*
|
|
368
|
+
* to `backend/framework.ts`; with both, a server option nifra.config.ts leaves unset comes from
|
|
369
|
+
* backend/framework.ts. Throws a clear, actionable error if neither exists or the config is
|
|
288
370
|
* malformed. */
|
|
289
371
|
export async function loadApp(
|
|
290
372
|
cwd: string,
|
|
@@ -303,7 +385,11 @@ export async function loadApp(
|
|
|
303
385
|
" nifra is zero-config but needs one of them exporting at least:\n" +
|
|
304
386
|
" export const adapter = reactAdapter // from @nifrajs/web-react\n" +
|
|
305
387
|
' export const clientModule = "@nifrajs/web-react/client"\n' +
|
|
306
|
-
" (create-nifra scaffolds this for you.)"
|
|
388
|
+
" (create-nifra scaffolds this for you.)" +
|
|
389
|
+
(existsSync(resolve(cwd, BACKEND_APP_FILE))
|
|
390
|
+
? `\n With ${BACKEND_APP_FILE} and no web config this is an API-only app: \`nifra routes\`,\n` +
|
|
391
|
+
" `nifra context`, `nifra openapi` and `nifra mcp` read its backend; pages need the config."
|
|
392
|
+
: ""),
|
|
307
393
|
)
|
|
308
394
|
}
|
|
309
395
|
if (!existsSync(resolve(cwd, "routes"))) {
|
|
@@ -311,7 +397,11 @@ export async function loadApp(
|
|
|
311
397
|
`[nifra] no routes/ directory in ${cwd} - nifra apps are file-routed under routes/.`,
|
|
312
398
|
)
|
|
313
399
|
}
|
|
314
|
-
const
|
|
400
|
+
const loaded = (await importWithQuery(configPath, options.importQuery)) as Partial<NifraFramework>
|
|
401
|
+
const fw =
|
|
402
|
+
configFile === CONFIG_FILE && existsSync(resolve(cwd, FRAMEWORK_FILE))
|
|
403
|
+
? await withServerOptions(loaded, resolve(cwd, FRAMEWORK_FILE), options.importQuery)
|
|
404
|
+
: loaded
|
|
315
405
|
if (!isAdapter(fw.adapter) || typeof fw.clientModule !== "string") {
|
|
316
406
|
throw new Error(
|
|
317
407
|
`[nifra] ${configFile} must export \`adapter\` (a render adapter object) and \`clientModule\` (a string).`,
|
package/src/mcp-context.ts
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import { realpathSync } from "node:fs"
|
|
9
9
|
import { readFile } from "node:fs/promises"
|
|
10
10
|
import { basename, isAbsolute, relative, resolve } from "node:path"
|
|
11
|
-
import type {
|
|
11
|
+
import type { AppLoader } from "./load.ts"
|
|
12
12
|
import { createCachedAppLoader } from "./mcp-exec.ts"
|
|
13
13
|
import { resolveMcpProjectPath } from "./mcp-path.ts"
|
|
14
14
|
import type { McpPrompt, McpResource, McpServerFeatures, McpTool } from "./mcp-protocol.ts"
|
|
15
|
+
import { writeTargetOf } from "./project-path.ts"
|
|
15
16
|
|
|
16
17
|
function openApiFormat(args: Record<string, unknown>): "json" | "yaml" {
|
|
17
18
|
const format = args.format
|
|
@@ -20,11 +21,11 @@ function openApiFormat(args: Record<string, unknown>): "json" | "yaml" {
|
|
|
20
21
|
|
|
21
22
|
export async function openApiHandler(
|
|
22
23
|
args: Record<string, unknown>,
|
|
23
|
-
loadAppCached:
|
|
24
|
+
loadAppCached: AppLoader,
|
|
24
25
|
): Promise<string> {
|
|
25
26
|
const { renderOpenApiWithTypes } = await import("./openapi-tool.ts")
|
|
26
27
|
const pathPrefix = typeof args.path === "string" ? args.path : undefined
|
|
27
|
-
return renderOpenApiWithTypes(await loadAppCached(), openApiFormat(args), pathPrefix)
|
|
28
|
+
return renderOpenApiWithTypes(await loadAppCached.backend(), openApiFormat(args), pathPrefix)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
async function readProjectFile(
|
|
@@ -57,7 +58,7 @@ function promptText(
|
|
|
57
58
|
|
|
58
59
|
export function projectResources(
|
|
59
60
|
cwd: string,
|
|
60
|
-
loadAppCached:
|
|
61
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
61
62
|
): McpResource[] {
|
|
62
63
|
return [
|
|
63
64
|
{
|
|
@@ -68,7 +69,7 @@ export function projectResources(
|
|
|
68
69
|
mimeType: "application/json",
|
|
69
70
|
read: async () => {
|
|
70
71
|
const { routesToJson } = await import("./introspect.ts")
|
|
71
|
-
return { text: JSON.stringify(routesToJson(await loadAppCached()), null, 2) }
|
|
72
|
+
return { text: JSON.stringify(routesToJson(await loadAppCached.backend()), null, 2) }
|
|
72
73
|
},
|
|
73
74
|
},
|
|
74
75
|
{
|
|
@@ -165,7 +166,7 @@ export function projectPrompts(): McpPrompt[] {
|
|
|
165
166
|
|
|
166
167
|
export function projectFeatures(
|
|
167
168
|
cwd: string,
|
|
168
|
-
loadAppCached:
|
|
169
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
169
170
|
): McpServerFeatures {
|
|
170
171
|
return { resources: projectResources(cwd, loadAppCached), prompts: projectPrompts() }
|
|
171
172
|
}
|
|
@@ -186,24 +187,10 @@ export function resolveProjectDir(root: string, dir: string | undefined): string
|
|
|
186
187
|
const lexical = relative(root, target)
|
|
187
188
|
return lexical === "" || (!lexical.startsWith("..") && !isAbsolute(lexical)) ? target : null
|
|
188
189
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const canonicalProbe = realpathSync(probe)
|
|
194
|
-
const rel = relative(canonicalRoot, canonicalProbe)
|
|
195
|
-
if (rel.startsWith("..") || isAbsolute(rel)) return null
|
|
196
|
-
return missing.length === 0 ? canonicalProbe : resolve(canonicalProbe, ...missing.reverse())
|
|
197
|
-
} catch {
|
|
198
|
-
const parent = resolve(probe, "..")
|
|
199
|
-
if (parent === probe) return null
|
|
200
|
-
// `basename`, not a slice past `parent`: when `parent` is the filesystem root its trailing
|
|
201
|
-
// separator is part of it, so `parent.length + 1` eats the first character of the segment and
|
|
202
|
-
// the path gets rebuilt wrong.
|
|
203
|
-
missing.push(basename(probe))
|
|
204
|
-
probe = parent
|
|
205
|
-
}
|
|
206
|
-
}
|
|
190
|
+
const canonical = writeTargetOf(target)
|
|
191
|
+
if (canonical === undefined) return null
|
|
192
|
+
const rel = relative(canonicalRoot, canonical)
|
|
193
|
+
return rel.startsWith("..") || isAbsolute(rel) ? null : canonical
|
|
207
194
|
}
|
|
208
195
|
|
|
209
196
|
/** Consistent error string for a `dir` that escapes the project root. */
|
|
@@ -218,7 +205,7 @@ export function dirError(dir: string | undefined): string {
|
|
|
218
205
|
/** Build the project-scoped tools for `cwd`. */
|
|
219
206
|
export interface CommandMcpToolOptions {
|
|
220
207
|
readonly cwd: string
|
|
221
|
-
readonly loadAppCached?:
|
|
208
|
+
readonly loadAppCached?: AppLoader
|
|
222
209
|
}
|
|
223
210
|
|
|
224
211
|
/** Adapt one executable catalog spec to its MCP descriptor and project-scoped handler. */
|
package/src/mcp-delegate.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* answer still reads as authoritative. So the server settles the version before it serves:
|
|
7
7
|
*
|
|
8
8
|
* 1. The project installs its own `@nifrajs/cli` at a different version: hand the whole session to
|
|
9
|
-
* that CLI (`
|
|
9
|
+
* that CLI (its `bin` script, run by this Bun with `--no-env-file`, stdio inherited) and step aside.
|
|
10
10
|
* 2. Handing off is impossible (no project CLI, a missing bin, or this process IS the hand-off and
|
|
11
11
|
* the versions still disagree): serve, but the version-sensitive tools refuse with the fix
|
|
12
12
|
* instead of answering for the wrong release.
|
|
@@ -31,27 +31,37 @@ export const VERSION_SENSITIVE_TOOLS: ReadonlySet<string> = new Set([
|
|
|
31
31
|
"nifra_contracts",
|
|
32
32
|
])
|
|
33
33
|
|
|
34
|
-
/** The project's own CLI: its version and, when
|
|
34
|
+
/** The project's own CLI: its version and, when it is on disk, the script its `bin` names. */
|
|
35
35
|
export interface ProjectCli {
|
|
36
36
|
readonly version: string
|
|
37
37
|
readonly bin?: string
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const BIN_NAME = process.platform === "win32" ? "nifra.exe" : "nifra"
|
|
41
40
|
const MAX_ANCESTOR_DIRS = 8
|
|
42
41
|
|
|
42
|
+
function binScript(bin: unknown): string | undefined {
|
|
43
|
+
if (typeof bin === "string") return bin
|
|
44
|
+
if (typeof bin !== "object" || bin === null) return undefined
|
|
45
|
+
const named: unknown = Reflect.get(bin, "nifra")
|
|
46
|
+
return typeof named === "string" ? named : undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
/** The nearest `@nifrajs/cli` install at or above `root` (a monorepo app hoists to the repo root). */
|
|
44
50
|
export async function findProjectCli(root: string): Promise<ProjectCli | undefined> {
|
|
45
51
|
let dir = root
|
|
46
52
|
for (let depth = 0; depth < MAX_ANCESTOR_DIRS; depth++) {
|
|
47
|
-
const
|
|
53
|
+
const pkg = join(dir, "node_modules", "@nifrajs", "cli")
|
|
48
54
|
try {
|
|
49
|
-
const meta = JSON.parse(
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const meta = JSON.parse(await readFile(join(pkg, "package.json"), "utf8")) as {
|
|
56
|
+
version?: unknown
|
|
57
|
+
bin?: unknown
|
|
58
|
+
}
|
|
52
59
|
if (typeof meta.version === "string" && meta.version.length > 0) {
|
|
53
|
-
const
|
|
54
|
-
|
|
60
|
+
const script = binScript(meta.bin)
|
|
61
|
+
const bin = script === undefined ? undefined : join(pkg, script)
|
|
62
|
+
return bin !== undefined && existsSync(bin)
|
|
63
|
+
? { version: meta.version, bin }
|
|
64
|
+
: { version: meta.version }
|
|
55
65
|
}
|
|
56
66
|
} catch {
|
|
57
67
|
// Not installed at this level - keep walking up.
|
|
@@ -79,7 +89,9 @@ export async function delegateToProjectCli(options: {
|
|
|
79
89
|
const cli = await findProjectCli(options.root)
|
|
80
90
|
if (cli === undefined || cli.version === options.version || cli.bin === undefined)
|
|
81
91
|
return undefined
|
|
82
|
-
|
|
92
|
+
// Not the `.bin` shim: its `bun` would load the project's `.env`, and the inherited
|
|
93
|
+
// NIFRA_MCP_WITHOUT_ENV_FILES then tells that CLI the values are already gone.
|
|
94
|
+
return handOffSession([process.execPath, "--no-env-file", cli.bin, "mcp", ...options.args], {
|
|
83
95
|
cwd: options.cwd,
|
|
84
96
|
env: { ...process.env, [MCP_DELEGATED_ENV]: "1" },
|
|
85
97
|
announce: `[nifra] mcp: this CLI is ${options.version}; the project installs @nifrajs/cli ${cli.version} - serving this session from ${cli.bin}`,
|
package/src/mcp-exec.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { type Dirent, realpathSync } from "node:fs"
|
|
10
10
|
import { readdir, stat } from "node:fs/promises"
|
|
11
|
-
import { relative, resolve } from "node:path"
|
|
11
|
+
import { isAbsolute, relative, resolve } from "node:path"
|
|
12
12
|
import { fileURLToPath } from "node:url"
|
|
13
13
|
import { BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
14
14
|
import {
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { collectContractProof } from "./contract-proof.ts"
|
|
25
25
|
import { loadDocsCorpus } from "./docs-search.ts"
|
|
26
26
|
import { loadExamplesCorpus } from "./examples.ts"
|
|
27
|
-
import type { LoadAppOptions, LoadedApp } from "./load.ts"
|
|
27
|
+
import type { AppLoader, IntrospectedApp, LoadAppOptions, LoadedApp } from "./load.ts"
|
|
28
28
|
import {
|
|
29
29
|
type CommandMcpToolOptions,
|
|
30
30
|
dirError,
|
|
@@ -36,7 +36,11 @@ import {
|
|
|
36
36
|
CHILD_INPUT_MAX_BYTES,
|
|
37
37
|
CHILD_OUTPUT_MAX_BYTES,
|
|
38
38
|
CHILD_TIMEOUT_MS,
|
|
39
|
+
type EscalatingStop,
|
|
40
|
+
escalatingStop,
|
|
41
|
+
OWN_PROCESS_GROUP,
|
|
39
42
|
readBoundedStream,
|
|
43
|
+
signalProcessGroup,
|
|
40
44
|
timeoutMessage,
|
|
41
45
|
validateLocalPort,
|
|
42
46
|
} from "./mcp-io.ts"
|
|
@@ -46,20 +50,29 @@ import { loadTypesCorpus } from "./types-search.ts"
|
|
|
46
50
|
|
|
47
51
|
const CODEFRAME_SOURCE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|mdx)$/i
|
|
48
52
|
|
|
53
|
+
type GatePaths = Pick<typeof import("node:path"), "isAbsolute" | "relative" | "resolve">
|
|
54
|
+
|
|
49
55
|
/** The files a stack the caller supplies may show a codeframe from: project source, never a dotfile
|
|
50
|
-
* or anything under a dot directory (`.env`, `.git/`, `.nifra/`)
|
|
51
|
-
|
|
56
|
+
* or anything under a dot directory (`.env`, `.git/`, `.nifra/`), and never a file outside the root -
|
|
57
|
+
* which on Windows includes one on another drive, where `relative` answers with an absolute path. */
|
|
58
|
+
export function explainSourceGate(
|
|
59
|
+
root: string,
|
|
60
|
+
paths: GatePaths = { isAbsolute, relative, resolve },
|
|
61
|
+
): (file: string) => boolean {
|
|
52
62
|
// The codeframe reads a native realpath, which expands a Windows short name (`RUNNER~1`); the root
|
|
53
63
|
// has to be spelled the same way or every file looks like it sits outside it.
|
|
54
|
-
let base = resolve(root)
|
|
64
|
+
let base = paths.resolve(root)
|
|
55
65
|
try {
|
|
56
66
|
base = realpathSync.native(base)
|
|
57
67
|
} catch {}
|
|
58
|
-
return (file) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
.
|
|
68
|
+
return (file) => {
|
|
69
|
+
const inside = paths.relative(base, file)
|
|
70
|
+
return (
|
|
71
|
+
CODEFRAME_SOURCE.test(file) &&
|
|
72
|
+
!paths.isAbsolute(inside) &&
|
|
73
|
+
!inside.split(/[\\/]/).some((segment) => segment.startsWith("."))
|
|
74
|
+
)
|
|
75
|
+
}
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
/** Path to a sibling child entry (`mcp-run` / `mcp-render` / `mcp-ws`), resolved next to this module (`.ts` in
|
|
@@ -74,7 +87,16 @@ export function toMcpTool(
|
|
|
74
87
|
const entry = "run" in source ? undefined : source
|
|
75
88
|
const { cwd } = options
|
|
76
89
|
const loadAppCached = options.loadAppCached ?? createCachedAppLoader(cwd)
|
|
77
|
-
const pathFields = [
|
|
90
|
+
const pathFields = [
|
|
91
|
+
"config",
|
|
92
|
+
"out",
|
|
93
|
+
"lockfile",
|
|
94
|
+
"before",
|
|
95
|
+
"after",
|
|
96
|
+
"baseline",
|
|
97
|
+
"file",
|
|
98
|
+
"since",
|
|
99
|
+
] as const
|
|
78
100
|
const catalogEntry = entry ?? toCommandCatalogEntry(spec)
|
|
79
101
|
return {
|
|
80
102
|
name: commandMcpName(catalogEntry.name),
|
|
@@ -143,7 +165,7 @@ export function toMcpTool(
|
|
|
143
165
|
|
|
144
166
|
export function catalogProjectTools(
|
|
145
167
|
cwd: string,
|
|
146
|
-
loadAppCached:
|
|
168
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
147
169
|
): McpTool[] {
|
|
148
170
|
return commandCatalog
|
|
149
171
|
.filter((entry) => entry.transports.includes("mcp"))
|
|
@@ -156,11 +178,11 @@ export function catalogProjectTools(
|
|
|
156
178
|
|
|
157
179
|
export function projectTools(
|
|
158
180
|
cwd: string,
|
|
159
|
-
loadAppCached:
|
|
181
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
160
182
|
): McpTool[] {
|
|
161
183
|
const warmRun = createWarmHandler("mcp-run", cwd, "run")
|
|
162
184
|
const warmRender = createWarmHandler("mcp-render", cwd, "render")
|
|
163
|
-
const catalogTools = catalogProjectTools(cwd,
|
|
185
|
+
const catalogTools = catalogProjectTools(cwd, loadAppCached)
|
|
164
186
|
const legacyTools: McpTool[] = [
|
|
165
187
|
{
|
|
166
188
|
name: "nifra_verify",
|
|
@@ -692,12 +714,14 @@ export async function spawnChild(
|
|
|
692
714
|
stdin: "pipe",
|
|
693
715
|
stdout: "pipe",
|
|
694
716
|
stderr: "pipe",
|
|
717
|
+
detached: OWN_PROCESS_GROUP,
|
|
695
718
|
})
|
|
719
|
+
const stopper = escalatingStop((name) => signalProcessGroup(proc, name))
|
|
696
720
|
let killed = false
|
|
697
721
|
const kill = (): void => {
|
|
698
722
|
if (killed) return
|
|
699
723
|
killed = true
|
|
700
|
-
|
|
724
|
+
stopper.stop()
|
|
701
725
|
}
|
|
702
726
|
const abort = (): void => kill()
|
|
703
727
|
signal?.addEventListener("abort", abort, { once: true })
|
|
@@ -738,6 +762,7 @@ export async function spawnChild(
|
|
|
738
762
|
return text || `${label} failed:\n${err.text.trim() || "(no output)"}`
|
|
739
763
|
} finally {
|
|
740
764
|
clearTimeout(timer)
|
|
765
|
+
stopper.dispose()
|
|
741
766
|
signal?.removeEventListener("abort", abort)
|
|
742
767
|
}
|
|
743
768
|
}
|
|
@@ -791,6 +816,16 @@ type PipeSubprocess = ReturnType<typeof Bun.spawn> & {
|
|
|
791
816
|
readonly stderr: ReadableStream<Uint8Array>
|
|
792
817
|
}
|
|
793
818
|
|
|
819
|
+
const liveWarmWorkers = new Set<WarmWorker>()
|
|
820
|
+
let warmWorkersClosed = false
|
|
821
|
+
|
|
822
|
+
/** Stop every warm worker and start no more, once the MCP client has closed stdin. A live worker
|
|
823
|
+
* holds the process open, so the server would otherwise never exit. */
|
|
824
|
+
export function closeWarmWorkers(): void {
|
|
825
|
+
warmWorkersClosed = true
|
|
826
|
+
for (const worker of liveWarmWorkers) worker.stop()
|
|
827
|
+
}
|
|
828
|
+
|
|
794
829
|
/** A persistent `mcp-run`/`mcp-render` `--worker` subprocess: the backend/web app is loaded ONCE and
|
|
795
830
|
* reused across newline-delimited `{ id, input }` requests, replying `{ id, output }`. The same machinery
|
|
796
831
|
* powers both `nifra_run warm` and `nifra_render warm` - `child` selects which engine, `label` shapes the
|
|
@@ -807,6 +842,7 @@ export class WarmWorker {
|
|
|
807
842
|
readonly cleanup: () => void
|
|
808
843
|
}
|
|
809
844
|
>()
|
|
845
|
+
private readonly stopper: EscalatingStop
|
|
810
846
|
private stdoutBuffer = ""
|
|
811
847
|
private stdoutBufferBytes = 0
|
|
812
848
|
private stderrBuffer = ""
|
|
@@ -827,10 +863,14 @@ export class WarmWorker {
|
|
|
827
863
|
stdout: "pipe",
|
|
828
864
|
stderr: "pipe",
|
|
829
865
|
}) as PipeSubprocess
|
|
866
|
+
this.stopper = escalatingStop((name) => this.proc.kill(name))
|
|
867
|
+
liveWarmWorkers.add(this)
|
|
830
868
|
void this.readStdout()
|
|
831
869
|
void this.readStderr()
|
|
832
870
|
void this.proc.exited.then((code) => {
|
|
833
871
|
this.closed = true
|
|
872
|
+
this.stopper.dispose()
|
|
873
|
+
liveWarmWorkers.delete(this)
|
|
834
874
|
const err = new Error(
|
|
835
875
|
`warm ${this.label} worker exited (${code})${this.stderrBuffer ? `:\n${this.stderrBuffer}` : ""}`,
|
|
836
876
|
)
|
|
@@ -843,7 +883,7 @@ export class WarmWorker {
|
|
|
843
883
|
}
|
|
844
884
|
|
|
845
885
|
stop(): void {
|
|
846
|
-
if (!this.closed) this.
|
|
886
|
+
if (!this.closed) this.stopper.stop()
|
|
847
887
|
}
|
|
848
888
|
|
|
849
889
|
async request(input: unknown, signal?: AbortSignal): Promise<string> {
|
|
@@ -1025,8 +1065,10 @@ export function createWarmHandler(
|
|
|
1025
1065
|
label: string,
|
|
1026
1066
|
): (input: unknown, signal?: AbortSignal) => Promise<string> {
|
|
1027
1067
|
let worker: WarmWorker | undefined
|
|
1068
|
+
const shuttingDown = `${label} cancelled: the MCP server is shutting down.`
|
|
1028
1069
|
return async (input, signal) => {
|
|
1029
1070
|
const fingerprint = await warmRunFingerprint(cwd)
|
|
1071
|
+
if (warmWorkersClosed) return shuttingDown
|
|
1030
1072
|
let current = worker
|
|
1031
1073
|
if (current === undefined || current.fingerprint !== fingerprint) {
|
|
1032
1074
|
current?.stop()
|
|
@@ -1044,6 +1086,7 @@ export function createWarmHandler(
|
|
|
1044
1086
|
const reason = cancellationSuffix(signal)
|
|
1045
1087
|
return `${label} cancelled${reason}.`
|
|
1046
1088
|
}
|
|
1089
|
+
if (warmWorkersClosed) return shuttingDown
|
|
1047
1090
|
return spawnChild(child, cwd, input, label, signal)
|
|
1048
1091
|
}
|
|
1049
1092
|
}
|
|
@@ -1146,7 +1189,7 @@ export async function appFingerprint(cwd: string): Promise<string> {
|
|
|
1146
1189
|
export function createCachedAppLoader(
|
|
1147
1190
|
cwd: string,
|
|
1148
1191
|
options: CachedAppLoaderOptions = {},
|
|
1149
|
-
):
|
|
1192
|
+
): AppLoader {
|
|
1150
1193
|
const loadAppCached =
|
|
1151
1194
|
options.loadApp ??
|
|
1152
1195
|
(async (root: string, outDirName?: string, loadOptions?: LoadAppOptions) => {
|
|
@@ -1162,7 +1205,7 @@ export function createCachedAppLoader(
|
|
|
1162
1205
|
}
|
|
1163
1206
|
| undefined
|
|
1164
1207
|
|
|
1165
|
-
|
|
1208
|
+
const whole = async (outDirName = "dist"): Promise<LoadedApp> => {
|
|
1166
1209
|
const nextFingerprint = await fingerprint(cwd)
|
|
1167
1210
|
if (
|
|
1168
1211
|
cached !== undefined &&
|
|
@@ -1177,4 +1220,11 @@ export function createCachedAppLoader(
|
|
|
1177
1220
|
cached = { outDirName, fingerprint: nextFingerprint, app }
|
|
1178
1221
|
return app
|
|
1179
1222
|
}
|
|
1223
|
+
const backend = async (): Promise<IntrospectedApp> => {
|
|
1224
|
+
const { hasAppConfig, loadBackendApp } = await import("./load.ts")
|
|
1225
|
+
if (hasAppConfig(cwd)) return whole()
|
|
1226
|
+
// The fingerprint names backend/app.ts, so an edit loads a fresh module and no edit reuses it.
|
|
1227
|
+
return loadBackendApp(cwd, { importQuery: cacheToken(await fingerprint(cwd)) })
|
|
1228
|
+
}
|
|
1229
|
+
return Object.assign(whole, { backend })
|
|
1180
1230
|
}
|
package/src/mcp-http.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
|
|
|
29
29
|
export type { TypeEntry } from "./types-search.ts"
|
|
30
30
|
|
|
31
31
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
32
|
-
const VERSION = "
|
|
32
|
+
const VERSION = "5.1.0"
|
|
33
33
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION }
|
|
34
34
|
// Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
|
|
35
35
|
const docsHealth = (tools: McpTool[]): string =>
|