@nifrajs/cli 3.5.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 +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- 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 +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +57 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +270 -26
- 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.map +1 -1
- package/dist/contracts.js +52 -13
- 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/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 +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- 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 +67 -17
- 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.js +1 -1
- 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 +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -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-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-render.js +2 -2
- 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 +110 -23
- 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 +82 -9
- 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 +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- 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 +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- 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.map +1 -1
- package/dist/rules/legacy.js +71 -38
- 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 +132 -87
- 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 +4 -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.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- 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 +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +374 -38
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- 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/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 +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- 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 +5 -0
- 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 +13 -8
- 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 +71 -39
- 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 +147 -95
- 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 +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
package/src/mcp-root.ts
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* 1. An explicit `nifra mcp <dir>` argument is taken literally (human configuration wins).
|
|
6
6
|
* 2. A cwd guess walks UP to the nearest nifra marker (a `package.json` depending on `@nifrajs/*`,
|
|
7
7
|
* or a `nifra.config.ts` monorepo root), so running from a subdirectory still finds the project.
|
|
8
|
+
* When nothing above is a project but the spawn directory is a package-manager workspace root
|
|
9
|
+
* (`package.json` `workspaces`) with exactly one nifra member, the server adopts that member.
|
|
8
10
|
* 3. The client's MCP `roots` (its workspace folders) are requested after the handshake. When the
|
|
9
11
|
* guess found no project - or found one DISJOINT from every workspace root - and exactly one
|
|
10
|
-
* workspace root is a nifra project, the server
|
|
12
|
+
* workspace root (or one member of a workspace-root folder) is a nifra project, the server
|
|
13
|
+
* adopts it.
|
|
11
14
|
* 4. What cannot be resolved fails CLOSED: project-scoped tools refuse with a remediation message
|
|
12
15
|
* instead of describing whatever directory the server happened to start in.
|
|
13
16
|
*
|
|
@@ -15,12 +18,13 @@
|
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { readFile } from "node:fs/promises"
|
|
18
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
|
|
21
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
19
22
|
import { fileURLToPath } from "node:url"
|
|
20
23
|
|
|
21
24
|
/** How the current root was chosen. `arg` = explicit `nifra mcp <dir>`; `cwd` = the spawn directory;
|
|
22
|
-
* `parent` = a marker found walking up from cwd; `
|
|
23
|
-
|
|
25
|
+
* `parent` = a marker found walking up from cwd; `workspace` = the one nifra member of the spawn
|
|
26
|
+
* directory's package-manager workspaces; `client-root` = adopted from the client's MCP roots. */
|
|
27
|
+
export type McpRootSource = "arg" | "cwd" | "parent" | "workspace" | "client-root"
|
|
24
28
|
|
|
25
29
|
export interface McpRootState {
|
|
26
30
|
/** Absolute directory every project-scoped tool operates on. */
|
|
@@ -31,11 +35,17 @@ export interface McpRootState {
|
|
|
31
35
|
/** The client's workspace roots (absolute paths), or `null` until (unless) the client answers
|
|
32
36
|
* `roots/list`. `null` disables mismatch detection - no data is not a mismatch. */
|
|
33
37
|
readonly clientRoots: readonly string[] | null
|
|
38
|
+
/** Nifra members of the spawn directory's workspaces when there were several to choose from - the
|
|
39
|
+
* remediation list, since adopting one of them would be a guess. */
|
|
40
|
+
readonly workspaceCandidates?: readonly string[]
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
const MAX_CLIENT_ROOTS = 64
|
|
37
44
|
const MAX_ROOT_URI_LENGTH = 16_384
|
|
38
45
|
const MAX_ROOT_PATH_LENGTH = 4_096
|
|
46
|
+
/** Workspace members probed per workspace root. A monorepo larger than this is pointed at explicitly
|
|
47
|
+
* (`nifra mcp <dir>`) rather than scanned on every server start. */
|
|
48
|
+
const MAX_WORKSPACE_MEMBERS = 256
|
|
39
49
|
|
|
40
50
|
/** A directory is a nifra project when its `package.json` depends on any `@nifrajs/*` package, or it
|
|
41
51
|
* is a `nifra.config.ts` monorepo root. Backend-only projects (no web config, no `routes/`) count -
|
|
@@ -69,8 +79,56 @@ export async function findNifraRoot(start: string): Promise<string | null> {
|
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
/** The `workspaces` globs a `package.json` declares (the array form, or Yarn's `{ packages }`), or
|
|
83
|
+
* `null` when the directory is not a workspace root. Negated patterns are dropped: they only exclude. */
|
|
84
|
+
async function workspacePatterns(dir: string): Promise<readonly string[] | null> {
|
|
85
|
+
let field: unknown
|
|
86
|
+
try {
|
|
87
|
+
field = (
|
|
88
|
+
JSON.parse(await readFile(join(dir, "package.json"), "utf8")) as Record<string, unknown>
|
|
89
|
+
).workspaces
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
const list = Array.isArray(field)
|
|
94
|
+
? field
|
|
95
|
+
: typeof field === "object" && field !== null
|
|
96
|
+
? (field as { packages?: unknown }).packages
|
|
97
|
+
: undefined
|
|
98
|
+
if (!Array.isArray(list)) return null
|
|
99
|
+
return list.filter(
|
|
100
|
+
(pattern): pattern is string =>
|
|
101
|
+
typeof pattern === "string" && pattern !== "" && !pattern.startsWith("!"),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The nifra projects among `dir`'s package-manager workspace members, in declaration order. Empty when
|
|
107
|
+
* `dir` declares no `workspaces`. A member is a directory a pattern matches that holds a `package.json`;
|
|
108
|
+
* `node_modules` is never a member.
|
|
109
|
+
*/
|
|
110
|
+
export async function workspaceProjects(dir: string): Promise<string[]> {
|
|
111
|
+
const patterns = await workspacePatterns(dir)
|
|
112
|
+
if (patterns === null) return []
|
|
113
|
+
const members = new Set<string>()
|
|
114
|
+
let probed = 0
|
|
115
|
+
for (const pattern of patterns) {
|
|
116
|
+
const glob = new Bun.Glob(`${pattern.replace(/\/+$/, "")}/package.json`)
|
|
117
|
+
for await (const match of glob.scan({ cwd: dir, onlyFiles: true })) {
|
|
118
|
+
if (match.split(/[\\/]/).includes("node_modules")) continue
|
|
119
|
+
if (++probed > MAX_WORKSPACE_MEMBERS) break
|
|
120
|
+
members.add(dirname(join(dir, match)))
|
|
121
|
+
}
|
|
122
|
+
if (probed > MAX_WORKSPACE_MEMBERS) break
|
|
123
|
+
}
|
|
124
|
+
const projects: string[] = []
|
|
125
|
+
for (const member of members) if (await isNifraProjectDir(member)) projects.push(member)
|
|
126
|
+
return projects
|
|
127
|
+
}
|
|
128
|
+
|
|
72
129
|
/** Initial root state. An explicit dir is taken literally (no walk-up - the human named it); a cwd
|
|
73
|
-
* guess walks up so a subdirectory spawn still lands on the project
|
|
130
|
+
* guess walks up so a subdirectory spawn still lands on the project, then tries the spawn directory's
|
|
131
|
+
* workspace members so a monorepo root finds its one nifra app. */
|
|
74
132
|
export async function resolveRootState(
|
|
75
133
|
requested: string,
|
|
76
134
|
explicit: boolean,
|
|
@@ -92,7 +150,17 @@ export async function resolveRootState(
|
|
|
92
150
|
clientRoots: null,
|
|
93
151
|
}
|
|
94
152
|
}
|
|
95
|
-
|
|
153
|
+
const members = await workspaceProjects(requested)
|
|
154
|
+
if (members.length === 1) {
|
|
155
|
+
return { root: members[0] as string, source: "workspace", isProject: true, clientRoots: null }
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
root: requested,
|
|
159
|
+
source: "cwd",
|
|
160
|
+
isProject: false,
|
|
161
|
+
clientRoots: null,
|
|
162
|
+
...(members.length > 1 ? { workspaceCandidates: members } : {}),
|
|
163
|
+
}
|
|
96
164
|
}
|
|
97
165
|
|
|
98
166
|
/** `file://` roots from a `roots/list` result, as absolute paths. Non-file and malformed URIs are
|
|
@@ -151,9 +219,9 @@ export function rootMismatch(state: McpRootState): boolean {
|
|
|
151
219
|
/**
|
|
152
220
|
* Fold the client's workspace roots into the state, adopting one of them when the current root is
|
|
153
221
|
* wrong and the correction is unambiguous: the root was GUESSED (never an explicit `nifra mcp <dir>`),
|
|
154
|
-
* it is not a project - or is one disjoint from the whole workspace - and exactly ONE
|
|
155
|
-
*
|
|
156
|
-
* message lists the candidates instead.
|
|
222
|
+
* it is not a project - or is one disjoint from the whole workspace - and exactly ONE candidate exists
|
|
223
|
+
* (see {@link nifraCandidates}). Ambiguity (zero or several candidates) adopts nothing; the guard's
|
|
224
|
+
* remediation message lists the candidates instead.
|
|
157
225
|
*/
|
|
158
226
|
export async function applyClientRoots(
|
|
159
227
|
state: McpRootState,
|
|
@@ -162,8 +230,7 @@ export async function applyClientRoots(
|
|
|
162
230
|
const next: McpRootState = { ...state, clientRoots: paths }
|
|
163
231
|
if (state.source === "arg") return next
|
|
164
232
|
if (next.isProject && !rootMismatch(next)) return next
|
|
165
|
-
const candidates
|
|
166
|
-
for (const path of paths) if (await isNifraProjectDir(path)) candidates.push(path)
|
|
233
|
+
const candidates = await nifraCandidates(paths)
|
|
167
234
|
if (candidates.length !== 1) return next
|
|
168
235
|
return {
|
|
169
236
|
root: candidates[0] as string,
|
|
@@ -173,11 +240,15 @@ export async function applyClientRoots(
|
|
|
173
240
|
}
|
|
174
241
|
}
|
|
175
242
|
|
|
176
|
-
/** The nifra
|
|
177
|
-
*
|
|
243
|
+
/** The nifra projects the client's workspace roots name: each root that is a project itself, and
|
|
244
|
+
* otherwise the nifra members of a root that is a package-manager workspace. The remediation list
|
|
245
|
+
* when adoption was ambiguous. */
|
|
178
246
|
export async function nifraCandidates(paths: readonly string[]): Promise<string[]> {
|
|
179
247
|
const candidates: string[] = []
|
|
180
|
-
for (const path of paths)
|
|
248
|
+
for (const path of paths) {
|
|
249
|
+
if (await isNifraProjectDir(path)) candidates.push(path)
|
|
250
|
+
else for (const member of await workspaceProjects(path)) candidates.push(member)
|
|
251
|
+
}
|
|
181
252
|
return candidates
|
|
182
253
|
}
|
|
183
254
|
|
|
@@ -198,10 +269,15 @@ export async function rootVerdict(state: McpRootState): Promise<McpRootVerdict>
|
|
|
198
269
|
? ` Client workspace roots: ${state.clientRoots.join(", ")}.`
|
|
199
270
|
: ""
|
|
200
271
|
if (!state.isProject) {
|
|
201
|
-
const candidates =
|
|
272
|
+
const candidates = [
|
|
273
|
+
...new Set([
|
|
274
|
+
...(state.workspaceCandidates ?? []),
|
|
275
|
+
...(state.clientRoots !== null ? await nifraCandidates(state.clientRoots) : []),
|
|
276
|
+
]),
|
|
277
|
+
]
|
|
202
278
|
const hint =
|
|
203
279
|
candidates.length > 1
|
|
204
|
-
? ` Several workspace
|
|
280
|
+
? ` Several workspace folders are nifra projects (${candidates.join(", ")}) - pass one: \`nifra mcp <dir>\`.`
|
|
205
281
|
: ""
|
|
206
282
|
return {
|
|
207
283
|
blocked:
|
|
@@ -250,7 +326,8 @@ export interface ToolingDrift {
|
|
|
250
326
|
export const driftNote = (drift: ToolingDrift): string =>
|
|
251
327
|
`WARNING: this server runs nifra CLI ${drift.cli}, but the project installs ${drift.package} ${drift.project}. ` +
|
|
252
328
|
"Types, checks, and docs may describe a different version than your code builds with. " +
|
|
253
|
-
|
|
329
|
+
`Fix: re-pin the project's MCP launch to the nifra it installs with \`${syncMcpCommand(drift.cli)}\` (run in the project directory), then restart the agent. ` +
|
|
330
|
+
"If this server is not launched from the project's .mcp.json, run the project's own CLI instead (`bunx --bun nifra mcp` from the project directory, or point the client at ./node_modules/.bin/nifra)."
|
|
254
331
|
|
|
255
332
|
/** Feature-level agreement. Patch drift is not reported: it never changes the surface being described. */
|
|
256
333
|
const featureVersion = (version: string): string => version.split(".").slice(0, 2).join(".")
|
|
@@ -285,16 +362,34 @@ export async function detectToolingDrift(
|
|
|
285
362
|
root: string,
|
|
286
363
|
cliVersion: string,
|
|
287
364
|
): Promise<ToolingDrift | undefined> {
|
|
365
|
+
const installed = await installedNifraVersion(root)
|
|
366
|
+
if (installed === undefined) return undefined
|
|
367
|
+
return featureVersion(installed.version) === featureVersion(cliVersion)
|
|
368
|
+
? undefined
|
|
369
|
+
: { cli: cliVersion, project: installed.version, package: installed.package }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The nifra the project builds with: its `@nifrajs/cli` when installed, else its `@nifrajs/core`
|
|
374
|
+
* (`fixed` versioning keeps the two in lockstep, so either names the CLI that matches the project).
|
|
375
|
+
* The version drift detection compares against and the version `init-agents --sync-mcp` pins to.
|
|
376
|
+
*/
|
|
377
|
+
export async function installedNifraVersion(
|
|
378
|
+
root: string,
|
|
379
|
+
): Promise<{ readonly package: string; readonly version: string } | undefined> {
|
|
288
380
|
for (const name of ["@nifrajs/cli", "@nifrajs/core"]) {
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
291
|
-
return featureVersion(project) === featureVersion(cliVersion)
|
|
292
|
-
? undefined
|
|
293
|
-
: { cli: cliVersion, project, package: name }
|
|
381
|
+
const version = await installedVersion(root, name)
|
|
382
|
+
if (version !== undefined) return { package: name, version }
|
|
294
383
|
}
|
|
295
384
|
return undefined
|
|
296
385
|
}
|
|
297
386
|
|
|
387
|
+
/** The command that re-pins a project's MCP launch to the nifra it installs. Named with the running
|
|
388
|
+
* CLI's own exact version: the binary printing this is the one known to have `--sync-mcp`, and it pins
|
|
389
|
+
* to the project's version, not its own. */
|
|
390
|
+
export const syncMcpCommand = (cliVersion: string): string =>
|
|
391
|
+
`bunx @nifrajs/cli@${cliVersion} init-agents --sync-mcp`
|
|
392
|
+
|
|
298
393
|
/** Tool names served from the bundled corpus; they do not depend on the project root. */
|
|
299
394
|
const PROJECT_FREE_TOOLS = new Set(["nifra_docs", "nifra_example", "nifra_types", "nifra_learn"])
|
|
300
395
|
|
package/src/mcp-run.ts
CHANGED
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
import { existsSync, realpathSync } from "node:fs"
|
|
9
9
|
import { isAbsolute, relative, resolve, sep } from "node:path"
|
|
10
10
|
import { pathToFileURL } from "node:url"
|
|
11
|
-
import { type AppLike, runApp } from "@nifrajs/runner"
|
|
11
|
+
import { type AppLike, type RequestSpec, type RunResult, runApp } from "@nifrajs/runner"
|
|
12
|
+
import {
|
|
13
|
+
captureInto,
|
|
14
|
+
createDevFeed,
|
|
15
|
+
type DevFeed,
|
|
16
|
+
type DevLogLevel,
|
|
17
|
+
runWithDevRequest,
|
|
18
|
+
} from "@nifrajs/web/dev-feed"
|
|
19
|
+
import type { Diagnostic } from "@nifrajs/web/diagnostic"
|
|
20
|
+
import { BACKEND_APP_FILE } from "./app-files.ts"
|
|
12
21
|
import {
|
|
13
22
|
CHILD_INPUT_MAX_BYTES,
|
|
14
23
|
CHILD_OUTPUT_MAX_BYTES,
|
|
@@ -17,7 +26,7 @@ import {
|
|
|
17
26
|
serializeBoundedJson,
|
|
18
27
|
} from "./mcp-io.ts"
|
|
19
28
|
|
|
20
|
-
const ENTRY_CANDIDATES = [
|
|
29
|
+
const ENTRY_CANDIDATES = [BACKEND_APP_FILE, "app.ts", "src/app.ts"]
|
|
21
30
|
|
|
22
31
|
const errString = (err: unknown): string =>
|
|
23
32
|
err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
|
@@ -85,12 +94,75 @@ export async function runBackend(
|
|
|
85
94
|
if ("error" in loaded) return loaded
|
|
86
95
|
|
|
87
96
|
try {
|
|
88
|
-
return { results: await
|
|
97
|
+
return { results: await runCaptured(cwd, loaded.app, requests) }
|
|
89
98
|
} catch (err) {
|
|
90
99
|
return { error: errString(err) }
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
|
|
103
|
+
/** What `nifra_run` adds to the runner's result: what the app printed, and what failed, structured. */
|
|
104
|
+
export interface RunCapture {
|
|
105
|
+
readonly logs?: ReadonlyArray<{ readonly level: DevLogLevel; readonly message: string }>
|
|
106
|
+
readonly errors?: readonly Diagnostic[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const MAX_LOGS_PER_REQUEST = 100
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run `requests` one at a time, each inside its own request context, so what a handler logs - and
|
|
113
|
+
* core's `unhandled request error` line behind a bare 500 - lands on the result of the request that
|
|
114
|
+
* produced it, redacted, with the error as a Diagnostic (codeframe, cause, fix). Without this a 500 came
|
|
115
|
+
* back as `internal_error` and the reason only ever reached a stderr nobody reads.
|
|
116
|
+
*/
|
|
117
|
+
export async function runCaptured(
|
|
118
|
+
cwd: string,
|
|
119
|
+
app: AppLike,
|
|
120
|
+
requests: readonly unknown[],
|
|
121
|
+
): Promise<Array<RunResult & RunCapture>> {
|
|
122
|
+
const feed = createDevFeed({ root: cwd, persist: false })
|
|
123
|
+
const detach = captureInto(feed)
|
|
124
|
+
const runs: Array<{ readonly requestId: string; readonly result: RunResult }> = []
|
|
125
|
+
try {
|
|
126
|
+
for (const [index, spec] of requests.entries()) {
|
|
127
|
+
const requestId = `run${index + 1}`
|
|
128
|
+
const [method, path] = describeSpec(spec)
|
|
129
|
+
const [result] = await runWithDevRequest(
|
|
130
|
+
{ feedId: feed.id, requestId, method, path },
|
|
131
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: runApp validates each spec itself and reports a malformed one as that request's error.
|
|
132
|
+
() => runApp(app, [spec as RequestSpec]),
|
|
133
|
+
)
|
|
134
|
+
if (result !== undefined) runs.push({ requestId, result })
|
|
135
|
+
}
|
|
136
|
+
// A line a handler scheduled for later (a promise continuation, a 0 ms timer) still lands.
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
138
|
+
} finally {
|
|
139
|
+
detach()
|
|
140
|
+
}
|
|
141
|
+
return runs.map(({ requestId, result }) => withCapture(feed, requestId, result))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function describeSpec(spec: unknown): [string, string] {
|
|
145
|
+
if (typeof spec !== "object" || spec === null) return ["GET", "/"]
|
|
146
|
+
const method = "method" in spec && typeof spec.method === "string" ? spec.method : "GET"
|
|
147
|
+
const path = "path" in spec && typeof spec.path === "string" ? spec.path : "/"
|
|
148
|
+
return [method, path]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function withCapture(feed: DevFeed, requestId: string, result: RunResult): RunResult & RunCapture {
|
|
152
|
+
if (result.error !== undefined) {
|
|
153
|
+
const thrown = new Error(result.error.message)
|
|
154
|
+
thrown.name = result.error.name
|
|
155
|
+
if (result.error.stack !== undefined) thrown.stack = result.error.stack
|
|
156
|
+
feed.recordError(thrown, { category: "api", requestId })
|
|
157
|
+
}
|
|
158
|
+
const capture: { -readonly [K in keyof RunCapture]: RunCapture[K] } = {}
|
|
159
|
+
const logs = feed.logs({ requestId, limit: MAX_LOGS_PER_REQUEST }).logs
|
|
160
|
+
if (logs.length > 0) capture.logs = logs.map(({ level, message }) => ({ level, message }))
|
|
161
|
+
const errors = feed.errors({ requestId }).errors
|
|
162
|
+
if (errors.length > 0) capture.errors = errors.map((entry) => entry.diagnostic)
|
|
163
|
+
return { ...result, ...capture }
|
|
164
|
+
}
|
|
165
|
+
|
|
94
166
|
interface WorkerMessage {
|
|
95
167
|
readonly id?: unknown
|
|
96
168
|
readonly input?: { readonly requests?: unknown; readonly entry?: string }
|
|
@@ -113,13 +185,31 @@ function safeResponseId(value: unknown): number | string | null {
|
|
|
113
185
|
return null
|
|
114
186
|
}
|
|
115
187
|
|
|
116
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Keep stdout for the protocol. The app's console and any direct `process.stdout.write` go to stderr,
|
|
190
|
+
* or a stray `console.log` in a handler would corrupt the JSON the parent reads. Returns the writer
|
|
191
|
+
* that still reaches the real stdout.
|
|
192
|
+
*/
|
|
193
|
+
function reserveStdout(): (text: string) => void {
|
|
117
194
|
redirectConsoleToStderr()
|
|
195
|
+
const original: unknown = Reflect.get(process.stdout, "write")
|
|
196
|
+
const write = typeof original === "function" ? original.bind(process.stdout) : undefined
|
|
197
|
+
// Bound now, before any capture patches stderr: a redirected stdout line is captured once, as stdout.
|
|
198
|
+
const toStderr = process.stderr.write.bind(process.stderr)
|
|
199
|
+
Reflect.set(process.stdout, "write", (chunk: unknown, ...rest: unknown[]) =>
|
|
200
|
+
Reflect.apply(toStderr, process.stderr, [chunk, ...rest]),
|
|
201
|
+
)
|
|
202
|
+
return (text) => {
|
|
203
|
+
write?.(text)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runWorker(cwd: string, protocol: (text: string) => void): Promise<void> {
|
|
118
208
|
const apps = new Map<string, Promise<{ app: AppLike } | { error: string }>>()
|
|
119
209
|
const send = (id: unknown, output: unknown): void => {
|
|
120
210
|
const responseId = safeResponseId(id)
|
|
121
211
|
const encoded = serializeBoundedJson({ id: responseId, output }, CHILD_OUTPUT_MAX_BYTES - 1)
|
|
122
|
-
|
|
212
|
+
protocol(
|
|
123
213
|
`${encoded ?? JSON.stringify({ id: responseId, output: { error: "worker output exceeded the size limit" } })}\n`,
|
|
124
214
|
)
|
|
125
215
|
}
|
|
@@ -175,9 +265,7 @@ async function runWorker(cwd: string): Promise<void> {
|
|
|
175
265
|
continue
|
|
176
266
|
}
|
|
177
267
|
try {
|
|
178
|
-
send(id, {
|
|
179
|
-
results: await runApp(app.app, requests as Parameters<typeof runApp>[1]),
|
|
180
|
-
})
|
|
268
|
+
send(id, { results: await runCaptured(cwd, app.app, requests) })
|
|
181
269
|
} catch (err) {
|
|
182
270
|
send(id, { error: errString(err) })
|
|
183
271
|
}
|
|
@@ -188,8 +276,9 @@ async function runWorker(cwd: string): Promise<void> {
|
|
|
188
276
|
// this only executes when run directly (not when imported by a test).
|
|
189
277
|
if (import.meta.main) {
|
|
190
278
|
const cwd = process.argv[2] ?? process.cwd()
|
|
279
|
+
const protocol = reserveStdout()
|
|
191
280
|
if (process.argv.includes("--worker")) {
|
|
192
|
-
await runWorker(cwd)
|
|
281
|
+
await runWorker(cwd, protocol)
|
|
193
282
|
process.exit(0)
|
|
194
283
|
}
|
|
195
284
|
let output: unknown
|
|
@@ -207,8 +296,13 @@ if (import.meta.main) {
|
|
|
207
296
|
output = { error: "invalid input: expected JSON { requests: [...] }" }
|
|
208
297
|
}
|
|
209
298
|
}
|
|
210
|
-
process.stdout.write
|
|
299
|
+
// `Bun.write` to the fd, not the patched `process.stdout.write`, so the answer is flushed when it
|
|
300
|
+
// resolves. Then exit: the app's own handles (a pool, an interval) would keep this single-use
|
|
301
|
+
// process alive until the parent's timeout, and the parent waits for it to exit.
|
|
302
|
+
await Bun.write(
|
|
303
|
+
Bun.stdout,
|
|
211
304
|
serializeBoundedJson(output, CHILD_OUTPUT_MAX_BYTES, 2) ??
|
|
212
305
|
JSON.stringify({ error: `output exceeded ${CHILD_OUTPUT_MAX_BYTES} bytes` }),
|
|
213
306
|
)
|
|
307
|
+
process.exit(0)
|
|
214
308
|
}
|
package/src/mcp-ws.ts
CHANGED
|
@@ -324,5 +324,8 @@ if (import.meta.main) {
|
|
|
324
324
|
error: 'invalid input: expected JSON { "path": "/..." }',
|
|
325
325
|
} satisfies WebSocketVerificationResult
|
|
326
326
|
}
|
|
327
|
-
|
|
327
|
+
// Flushed, then exit: the app's own handles would keep this single-use process alive until the
|
|
328
|
+
// parent's timeout, as in mcp-run and mcp-render.
|
|
329
|
+
await Bun.write(Bun.stdout, JSON.stringify(output, null, 2))
|
|
330
|
+
process.exit(0)
|
|
328
331
|
}
|