@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/dist/mcp-exec.js
CHANGED
|
@@ -5,18 +5,36 @@
|
|
|
5
5
|
* loading. It has no JSON-RPC transport or root-trust policy, so those seams remain independently
|
|
6
6
|
* replaceable.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { realpathSync } from "node:fs";
|
|
9
|
+
import { readdir, stat } from "node:fs/promises";
|
|
10
|
+
import { relative, resolve } from "node:path";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import {
|
|
12
|
-
import { commandCatalog, commandMcpName, findCommandSpec, } from "./command-catalog.js";
|
|
12
|
+
import { BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.js";
|
|
13
|
+
import { commandCatalog, commandMcpInputSchema, commandMcpName, findCommandSpec, toCommandCatalogEntry, } from "./command-catalog.js";
|
|
13
14
|
import { collectContractProof } from "./contract-proof.js";
|
|
14
15
|
import { loadDocsCorpus } from "./docs-search.js";
|
|
15
16
|
import { loadExamplesCorpus } from "./examples.js";
|
|
16
17
|
import { dirError, openApiHandler, resolveProjectDir, } from "./mcp-context.js";
|
|
17
18
|
import { docsTools } from "./mcp-docs-tools.js";
|
|
18
|
-
import { CHILD_OUTPUT_MAX_BYTES, CHILD_TIMEOUT_MS,
|
|
19
|
+
import { CHILD_INPUT_MAX_BYTES, CHILD_OUTPUT_MAX_BYTES, CHILD_TIMEOUT_MS, readBoundedStream, timeoutMessage, validateLocalPort, } from "./mcp-io.js";
|
|
20
|
+
import { mcpProjectPathError, resolveMcpProjectPath } from "./mcp-path.js";
|
|
19
21
|
import { loadTypesCorpus } from "./types-search.js";
|
|
22
|
+
const CODEFRAME_SOURCE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|mdx)$/i;
|
|
23
|
+
/** The files a stack the caller supplies may show a codeframe from: project source, never a dotfile
|
|
24
|
+
* or anything under a dot directory (`.env`, `.git/`, `.nifra/`). */
|
|
25
|
+
function explainSourceGate(root) {
|
|
26
|
+
// The codeframe reads a native realpath, which expands a Windows short name (`RUNNER~1`); the root
|
|
27
|
+
// has to be spelled the same way or every file looks like it sits outside it.
|
|
28
|
+
let base = resolve(root);
|
|
29
|
+
try {
|
|
30
|
+
base = realpathSync.native(base);
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
return (file) => CODEFRAME_SOURCE.test(file) &&
|
|
34
|
+
!relative(base, file)
|
|
35
|
+
.split(/[\\/]/)
|
|
36
|
+
.some((segment) => segment.startsWith("."));
|
|
37
|
+
}
|
|
20
38
|
/** Path to a sibling child entry (`mcp-run` / `mcp-render` / `mcp-ws`), resolved next to this module (`.ts` in
|
|
21
39
|
* dev, `.js` once built). Each runs in a FRESH subprocess per call so the project's current code loads. */
|
|
22
40
|
export function toMcpTool(source, options) {
|
|
@@ -26,22 +44,44 @@ export function toMcpTool(source, options) {
|
|
|
26
44
|
const entry = "run" in source ? undefined : source;
|
|
27
45
|
const { cwd } = options;
|
|
28
46
|
const loadAppCached = options.loadAppCached ?? createCachedAppLoader(cwd);
|
|
47
|
+
const pathFields = ["config", "out", "lockfile", "before", "after", "baseline", "file"];
|
|
48
|
+
const catalogEntry = entry ?? toCommandCatalogEntry(spec);
|
|
29
49
|
return {
|
|
30
|
-
name: commandMcpName(
|
|
31
|
-
description:
|
|
32
|
-
inputSchema:
|
|
50
|
+
name: commandMcpName(catalogEntry.name),
|
|
51
|
+
description: catalogEntry.summary,
|
|
52
|
+
inputSchema: commandMcpInputSchema(catalogEntry),
|
|
33
53
|
handler: async (args, context) => {
|
|
34
54
|
const raw = { ...args };
|
|
55
|
+
for (const field of catalogEntry.cliOnlyFields) {
|
|
56
|
+
if (raw[field] !== undefined)
|
|
57
|
+
return JSON.stringify({ ok: false, error: `${field} is available only from the nifra CLI` }, null, 2);
|
|
58
|
+
}
|
|
35
59
|
const dir = raw.dir;
|
|
36
60
|
if (dir !== undefined && typeof dir !== "string")
|
|
37
61
|
return dirError(undefined);
|
|
38
62
|
const target = resolveProjectDir(cwd, dir);
|
|
39
63
|
if (target === null)
|
|
40
64
|
return dirError(dir);
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
65
|
+
for (const field of pathFields) {
|
|
66
|
+
const value = raw[field];
|
|
67
|
+
if (typeof value !== "string")
|
|
68
|
+
continue;
|
|
69
|
+
const safe = resolveMcpProjectPath(target, value);
|
|
70
|
+
if (safe === null)
|
|
71
|
+
return JSON.stringify({ ok: false, error: mcpProjectPathError(field, value) }, null, 2);
|
|
72
|
+
raw[field] = safe;
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(raw.files)) {
|
|
75
|
+
const safeFiles = [];
|
|
76
|
+
for (const value of raw.files) {
|
|
77
|
+
if (typeof value !== "string")
|
|
78
|
+
continue;
|
|
79
|
+
const safe = resolveMcpProjectPath(target, value);
|
|
80
|
+
if (safe === null)
|
|
81
|
+
return JSON.stringify({ ok: false, error: mcpProjectPathError("files", value) }, null, 2);
|
|
82
|
+
safeFiles.push(safe);
|
|
83
|
+
}
|
|
84
|
+
raw.files = safeFiles;
|
|
45
85
|
}
|
|
46
86
|
delete raw.dir;
|
|
47
87
|
let input;
|
|
@@ -80,7 +120,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
80
120
|
const legacyTools = [
|
|
81
121
|
{
|
|
82
122
|
name: "nifra_verify",
|
|
83
|
-
description: "Run the shared repository verification plan. Set release=true for the full release plan; the response preserves the declarative gate order and each gate's remediation.",
|
|
123
|
+
description: "Run the shared repository verification plan. Set release=true for the full release plan; the response preserves the declarative gate order and each gate's remediation. A gate whose script the project's package.json does not declare is reported as undeclared and not run; the result passes when every gate that ran passed.",
|
|
84
124
|
inputSchema: {
|
|
85
125
|
type: "object",
|
|
86
126
|
properties: {
|
|
@@ -149,9 +189,15 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
149
189
|
if (baseline !== undefined && baseline.trim() === "") {
|
|
150
190
|
return JSON.stringify({ ok: false, error: "baseline must not be empty" }, null, 2);
|
|
151
191
|
}
|
|
192
|
+
const safeBaseline = baseline === undefined ? undefined : resolveMcpProjectPath(target, baseline);
|
|
193
|
+
if (baseline !== undefined && safeBaseline === null) {
|
|
194
|
+
return JSON.stringify({ ok: false, error: mcpProjectPathError("baseline", baseline) }, null, 2);
|
|
195
|
+
}
|
|
152
196
|
try {
|
|
153
197
|
return JSON.stringify(await collectContractProof(target, {
|
|
154
|
-
...(
|
|
198
|
+
...(safeBaseline === undefined || safeBaseline === null
|
|
199
|
+
? {}
|
|
200
|
+
: { baselinePath: safeBaseline }),
|
|
155
201
|
...(raw.check === undefined ? {} : { check: raw.check === true }),
|
|
156
202
|
}), null, 2);
|
|
157
203
|
}
|
|
@@ -162,7 +208,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
162
208
|
},
|
|
163
209
|
{
|
|
164
210
|
name: "nifra_explain",
|
|
165
|
-
description: "Turn a nifra error into a STRUCTURED diagnostic instead of eyeballing a stack trace: a stable `code`, the top frame in YOUR source, a codeframe around the offending line, and - when nifra recognises the failure - the plain-language `cause` + `fix` + docs anchor. Pass `error` (and `stack` if you have it, e.g. from nifra_run/nifra_test output or a failing build) to explain a specific failure
|
|
211
|
+
description: "Turn a nifra error into a STRUCTURED diagnostic instead of eyeballing a stack trace: a stable `code`, the top frame in YOUR source, a codeframe around the offending line, and - when nifra recognises the failure - the plain-language `cause` + `fix` + docs anchor. Pass `error` (and `stack` if you have it, e.g. from nifra_run/nifra_test output or a failing build) to explain a specific failure. With no `error`, returns the running dev server's most recent error of any kind (SSR, loader, API, build, browser, hydration) with its request id - the server is found automatically; `port`/`dir` pick one when several run. nifra_errors lists them all. Returns the same JSON the dev overlay renders.",
|
|
166
212
|
inputSchema: {
|
|
167
213
|
type: "object",
|
|
168
214
|
properties: {
|
|
@@ -180,13 +226,17 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
180
226
|
},
|
|
181
227
|
port: {
|
|
182
228
|
type: "number",
|
|
183
|
-
description: "
|
|
229
|
+
description: "The dev server's port, when the workspace runs several. Optional: the project's `nifra dev` server is found automatically.",
|
|
230
|
+
},
|
|
231
|
+
dir: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "The app directory, when the workspace runs more than one dev server.",
|
|
184
234
|
},
|
|
185
235
|
},
|
|
186
236
|
additionalProperties: false,
|
|
187
237
|
},
|
|
188
|
-
handler: async (args) => {
|
|
189
|
-
const { error, stack, name, port } = args;
|
|
238
|
+
handler: async (args, context) => {
|
|
239
|
+
const { error, stack, name, port, dir } = args;
|
|
190
240
|
if (error !== undefined || stack !== undefined) {
|
|
191
241
|
const { buildDiagnostic } = await import("@nifrajs/web/diagnostic");
|
|
192
242
|
const e = new Error(error ?? "");
|
|
@@ -194,104 +244,78 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
194
244
|
e.name = name;
|
|
195
245
|
if (stack !== undefined)
|
|
196
246
|
e.stack = stack;
|
|
197
|
-
return JSON.stringify(buildDiagnostic(e, { root: cwd }), null, 2);
|
|
247
|
+
return JSON.stringify(buildDiagnostic(e, { root: cwd, showSource: explainSourceGate(cwd) }), null, 2);
|
|
198
248
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
message: `dev server at :${validPort} returned ${res.status}`,
|
|
215
|
-
}, null, 2);
|
|
216
|
-
}
|
|
217
|
-
return await readBoundedResponse(res);
|
|
218
|
-
}
|
|
219
|
-
catch (cause) {
|
|
220
|
-
return JSON.stringify({
|
|
221
|
-
code: "NIFRA_NONE",
|
|
222
|
-
message: `could not reach a nifra dev server at :${validPort} - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
223
|
-
}, null, 2);
|
|
224
|
-
}
|
|
249
|
+
const target = resolveProjectDir(cwd, dir);
|
|
250
|
+
if (target === null)
|
|
251
|
+
return dirError(dir);
|
|
252
|
+
if (port !== undefined && validateLocalPort(port) === undefined) {
|
|
253
|
+
return JSON.stringify({ code: "NIFRA_INVALID_PORT", message: "port must be an integer from 1 to 65535." }, null, 2);
|
|
254
|
+
}
|
|
255
|
+
const { explainLatest } = await import("./dev-feed-tool.js");
|
|
256
|
+
try {
|
|
257
|
+
return JSON.stringify(await explainLatest(target, port, context.signal), null, 2);
|
|
258
|
+
}
|
|
259
|
+
catch (cause) {
|
|
260
|
+
return JSON.stringify({
|
|
261
|
+
code: "NIFRA_NONE",
|
|
262
|
+
message: `could not read the dev server's errors - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
263
|
+
}, null, 2);
|
|
225
264
|
}
|
|
226
|
-
return JSON.stringify({
|
|
227
|
-
code: "NIFRA_NONE",
|
|
228
|
-
message: "Pass `error` (and `stack` if available) to explain a failure, or `port` to fetch the dev server's last error.",
|
|
229
|
-
}, null, 2);
|
|
230
265
|
},
|
|
231
266
|
},
|
|
232
267
|
{
|
|
233
268
|
name: "nifra_inspect",
|
|
234
|
-
description: "Observe what your requests ACTUALLY did on the running dev server
|
|
269
|
+
description: "Observe what your requests ACTUALLY did on the running dev server: one trace per request, `{ requestId, method, path, status, durationMs, bytes, isr, errorIds, logCount }`. After nifra_run or a real browser request, call this to SEE the outcome (the status, how long, ISR hit/miss, whether it logged or failed) instead of guessing; pass a trace's `requestId` to nifra_errors/nifra_logs for what that request failed with or printed. Every `nifra dev` server keeps these traces and is found automatically; `port`/`dir` pick one when several run. Narrow with `path` (a path prefix), `requestId`, `since` (a cursor from an earlier call) or `limit` (most recent N). An explicit `port` this project did not start falls back to the `@nifrajs/devtools` plugin's traces.",
|
|
235
270
|
inputSchema: {
|
|
236
271
|
type: "object",
|
|
237
272
|
properties: {
|
|
238
|
-
port: {
|
|
273
|
+
port: {
|
|
274
|
+
type: "number",
|
|
275
|
+
description: "The dev server's port. Optional: the project's server is found automatically.",
|
|
276
|
+
},
|
|
239
277
|
path: { type: "string", description: "Only traces whose path starts with this prefix." },
|
|
278
|
+
requestId: { type: "string", description: "Only the trace with this request id." },
|
|
279
|
+
since: {
|
|
280
|
+
type: "number",
|
|
281
|
+
description: "Only traces after this cursor (the `cursor` an earlier call returned).",
|
|
282
|
+
},
|
|
240
283
|
limit: { type: "number", description: "Return only the most recent N traces." },
|
|
284
|
+
dir: {
|
|
285
|
+
type: "string",
|
|
286
|
+
description: "The app directory, when the workspace runs more than one dev server.",
|
|
287
|
+
},
|
|
241
288
|
},
|
|
242
|
-
required: ["port"],
|
|
243
289
|
additionalProperties: false,
|
|
244
290
|
},
|
|
245
|
-
handler: async (args) => {
|
|
246
|
-
const { port, path, limit } = args;
|
|
247
|
-
|
|
291
|
+
handler: async (args, context) => {
|
|
292
|
+
const { port, path, requestId, since, limit, dir } = args;
|
|
293
|
+
const target = resolveProjectDir(cwd, dir);
|
|
294
|
+
if (target === null)
|
|
295
|
+
return dirError(dir);
|
|
296
|
+
if (port !== undefined && validateLocalPort(port) === undefined) {
|
|
248
297
|
return JSON.stringify({
|
|
249
|
-
|
|
250
|
-
|
|
298
|
+
requests: [],
|
|
299
|
+
code: "NIFRA_INVALID_PORT",
|
|
300
|
+
note: "port must be an integer from 1 to 65535.",
|
|
251
301
|
}, null, 2);
|
|
252
302
|
}
|
|
303
|
+
const { inspectRequests } = await import("./dev-feed-tool.js");
|
|
253
304
|
try {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
return JSON.stringify({
|
|
257
|
-
events: [],
|
|
258
|
-
code: "NIFRA_INVALID_PORT",
|
|
259
|
-
note: "port must be an integer from 1 to 65535.",
|
|
260
|
-
}, null, 2);
|
|
261
|
-
}
|
|
262
|
-
const url = new URL(`http://127.0.0.1:${validPort}/_nifra/devtools/state`);
|
|
263
|
-
if (path !== undefined)
|
|
264
|
-
url.searchParams.set("path", path);
|
|
265
|
-
if (limit !== undefined)
|
|
266
|
-
url.searchParams.set("limit", String(limit));
|
|
267
|
-
const res = await fetch(url, {
|
|
268
|
-
signal: AbortSignal.timeout(LOCAL_TOOL_FETCH_TIMEOUT_MS),
|
|
269
|
-
});
|
|
270
|
-
if (res.headers.get("x-nifra-devtools") !== "true")
|
|
271
|
-
return notNifraResponse("DevTools");
|
|
272
|
-
if (res.status === 404) {
|
|
273
|
-
return JSON.stringify({
|
|
274
|
-
events: [],
|
|
275
|
-
note: "No DevTools endpoint on that server. Mount `devtools()` from @nifrajs/web and run in development.",
|
|
276
|
-
}, null, 2);
|
|
277
|
-
}
|
|
278
|
-
if (!res.ok) {
|
|
279
|
-
return JSON.stringify({ events: [], note: `DevTools state returned ${res.status}.` }, null, 2);
|
|
280
|
-
}
|
|
281
|
-
return await readBoundedResponse(res);
|
|
305
|
+
const result = await inspectRequests(target, { port, path, requestId, since, limit }, context.signal);
|
|
306
|
+
return JSON.stringify(result, null, 2);
|
|
282
307
|
}
|
|
283
308
|
catch (cause) {
|
|
284
|
-
const validPort = validateLocalPort(port);
|
|
285
309
|
return JSON.stringify({
|
|
286
|
-
|
|
287
|
-
note: `Could not
|
|
310
|
+
requests: [],
|
|
311
|
+
note: `Could not read the dev server's request traces - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
288
312
|
}, null, 2);
|
|
289
313
|
}
|
|
290
314
|
},
|
|
291
315
|
},
|
|
292
316
|
{
|
|
293
317
|
name: "nifra_openapi",
|
|
294
|
-
description: 'Return this project\'s backend OpenAPI 3.1 document generated from backend.ts route schemas via @nifrajs/schema. Use `format:"json"` for machine edits (default) or `format:"yaml"` for humans. Pass `path` (a route prefix like /api/orders, mirroring nifra_routes) to narrow a large backend to operations under that prefix instead of the whole document. Frontend-only apps return a valid empty paths object.',
|
|
318
|
+
description: 'Return this project\'s backend OpenAPI 3.1 document generated from backend/app.ts route schemas via @nifrajs/schema. Use `format:"json"` for machine edits (default) or `format:"yaml"` for humans. Pass `path` (a route prefix like /api/orders, mirroring nifra_routes) to narrow a large backend to operations under that prefix instead of the whole document. Frontend-only apps return a valid empty paths object.',
|
|
295
319
|
inputSchema: {
|
|
296
320
|
type: "object",
|
|
297
321
|
properties: {
|
|
@@ -311,7 +335,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
311
335
|
},
|
|
312
336
|
{
|
|
313
337
|
name: "nifra_run",
|
|
314
|
-
description: "Run HTTP requests through this project's backend and return structured results
|
|
338
|
+
description: "Run HTTP requests through this project's backend and return structured results: status, headers, parsed body, `logs` (what the handler printed, per request, secrets redacted) and `errors` (a Diagnostic with codeframe, cause and fix for a thrown error or the unhandled error behind a bare 500). Use it to verify code after editing: by default the backend is re-loaded in a fresh process each call. Pass warm:true to reuse a hot worker while source files are unchanged; it restarts automatically when files change. Each request: { method?, path, body?, headers? }.",
|
|
315
339
|
inputSchema: {
|
|
316
340
|
type: "object",
|
|
317
341
|
properties: {
|
|
@@ -330,7 +354,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
330
354
|
},
|
|
331
355
|
entry: {
|
|
332
356
|
type: "string",
|
|
333
|
-
description: "Backend entry file (default: backend.ts | app.ts).",
|
|
357
|
+
description: "Backend entry file (default: backend/app.ts | app.ts).",
|
|
334
358
|
},
|
|
335
359
|
warm: {
|
|
336
360
|
type: "boolean",
|
|
@@ -394,7 +418,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
394
418
|
},
|
|
395
419
|
entry: {
|
|
396
420
|
type: "string",
|
|
397
|
-
description: "Backend entry file (default: backend.ts | app.ts).",
|
|
421
|
+
description: "Backend entry file (default: backend/app.ts | app.ts).",
|
|
398
422
|
},
|
|
399
423
|
},
|
|
400
424
|
required: ["path"],
|
|
@@ -410,7 +434,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
410
434
|
properties: {
|
|
411
435
|
pattern: {
|
|
412
436
|
type: "string",
|
|
413
|
-
description: "Optional test file/path pattern passed as an argv item to `bun test`.",
|
|
437
|
+
description: "Optional test file/path pattern inside the project, passed as an argv item to `bun test`.",
|
|
414
438
|
},
|
|
415
439
|
timeoutMs: {
|
|
416
440
|
type: "number",
|
|
@@ -437,7 +461,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
437
461
|
...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus),
|
|
438
462
|
{
|
|
439
463
|
name: "nifra_scaffold",
|
|
440
|
-
description: 'Map a URL path to the CORRECT routes/
|
|
464
|
+
description: 'Map a URL path to the CORRECT routes/ files and get a contract-correct route pair: the page and its .backend.ts half (loader + output schema). Agents routinely place file routes wrong - this applies the convention for you: ":id"/"[id]" → [id], "*rest" → [...rest], "/" → index. Pass path (e.g. "/users/:id"). Returns both files + the route contract + stubs (the page is ready-to-write for react/preact/solid/vanilla; for vue/svelte use nifra_example for the page body).',
|
|
441
465
|
inputSchema: {
|
|
442
466
|
type: "object",
|
|
443
467
|
properties: {
|
|
@@ -447,7 +471,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
447
471
|
},
|
|
448
472
|
write: {
|
|
449
473
|
type: "boolean",
|
|
450
|
-
description: "When true, create
|
|
474
|
+
description: "When true, create both files if a verified ready-to-write page stub exists. Refuses overwrite.",
|
|
451
475
|
},
|
|
452
476
|
variant: {
|
|
453
477
|
type: "string",
|
|
@@ -470,7 +494,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
470
494
|
return renderScaffold(path, framework, flavour);
|
|
471
495
|
const result = await writeScaffoldRoute(cwd, path, framework, flavour);
|
|
472
496
|
const status = result.written
|
|
473
|
-
? `Written: \`${result.file}\``
|
|
497
|
+
? `Written: \`${result.file}\` and \`${result.backend.file}\``
|
|
474
498
|
: `Not written: ${result.reason ?? "no write performed"}`;
|
|
475
499
|
return `${status}\n\n${renderScaffold(path, framework, flavour)}`;
|
|
476
500
|
},
|
|
@@ -486,13 +510,13 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
486
510
|
},
|
|
487
511
|
additionalProperties: false,
|
|
488
512
|
},
|
|
489
|
-
handler: async (args) => {
|
|
513
|
+
handler: async (args, context) => {
|
|
490
514
|
const opts = args;
|
|
491
515
|
const target = resolveProjectDir(cwd, opts.dir);
|
|
492
516
|
if (target === null)
|
|
493
517
|
return dirError(opts.dir);
|
|
494
518
|
const { runHydrationAssurance } = await import("./assure-hydration.js");
|
|
495
|
-
return JSON.stringify(await runHydrationAssurance(target, { interact: opts.interact === true }), null, 2);
|
|
519
|
+
return JSON.stringify(await runHydrationAssurance(target, { interact: opts.interact === true }, { signal: context.signal }), null, 2);
|
|
496
520
|
},
|
|
497
521
|
},
|
|
498
522
|
];
|
|
@@ -517,7 +541,21 @@ function childPath(name) {
|
|
|
517
541
|
export async function spawnChild(child, cwd, input, label, signal) {
|
|
518
542
|
if (signal?.aborted)
|
|
519
543
|
return `${label} cancelled before it started.`;
|
|
520
|
-
|
|
544
|
+
let encodedInput;
|
|
545
|
+
try {
|
|
546
|
+
encodedInput = JSON.stringify(input);
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return `${label} input is not serializable.`;
|
|
550
|
+
}
|
|
551
|
+
if (new TextEncoder().encode(encodedInput).byteLength > CHILD_INPUT_MAX_BYTES)
|
|
552
|
+
return `${label} input exceeded ${CHILD_INPUT_MAX_BYTES} bytes.`;
|
|
553
|
+
if (signal?.aborted)
|
|
554
|
+
return `${label} cancelled${cancellationSuffix(signal)}.`;
|
|
555
|
+
const proc = Bun.spawn([process.execPath, childPath(child), cwd], {
|
|
556
|
+
cwd,
|
|
557
|
+
// Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
|
|
558
|
+
env: process.env,
|
|
521
559
|
stdin: "pipe",
|
|
522
560
|
stdout: "pipe",
|
|
523
561
|
stderr: "pipe",
|
|
@@ -538,8 +576,12 @@ export async function spawnChild(child, cwd, input, label, signal) {
|
|
|
538
576
|
}, CHILD_TIMEOUT_MS);
|
|
539
577
|
let outputExceeded = false;
|
|
540
578
|
try {
|
|
541
|
-
|
|
542
|
-
|
|
579
|
+
if (signal?.aborted)
|
|
580
|
+
kill();
|
|
581
|
+
else {
|
|
582
|
+
proc.stdin.write(encodedInput);
|
|
583
|
+
await proc.stdin.end();
|
|
584
|
+
}
|
|
543
585
|
const [out, err] = await Promise.all([
|
|
544
586
|
readBoundedStream(proc.stdout, CHILD_OUTPUT_MAX_BYTES, () => {
|
|
545
587
|
outputExceeded = true;
|
|
@@ -552,7 +594,7 @@ export async function spawnChild(child, cwd, input, label, signal) {
|
|
|
552
594
|
]);
|
|
553
595
|
await proc.exited;
|
|
554
596
|
if (signal?.aborted) {
|
|
555
|
-
const reason =
|
|
597
|
+
const reason = cancellationSuffix(signal);
|
|
556
598
|
return `${label} cancelled${reason}.`;
|
|
557
599
|
}
|
|
558
600
|
if (outputExceeded) {
|
|
@@ -570,24 +612,42 @@ export async function spawnChild(child, cwd, input, label, signal) {
|
|
|
570
612
|
signal?.removeEventListener("abort", abort);
|
|
571
613
|
}
|
|
572
614
|
}
|
|
573
|
-
const
|
|
574
|
-
|
|
615
|
+
const WARM_RUN_SOURCE = /\.(?:[cm]?[jt]sx?|json)$/;
|
|
616
|
+
// Pruned while walking, not filtered after: a scan that descends into node_modules on every warm call
|
|
617
|
+
// costs more than the cold start the worker saves.
|
|
618
|
+
const WARM_RUN_SKIPPED_DIR = /^(?:\..*|node_modules|dist(?:-[a-z0-9]+)?|build|coverage)$/;
|
|
575
619
|
const WARM_RUN_EXTRA_FILES = ["bun.lock", "bun.lockb"];
|
|
620
|
+
const MAX_WARM_PENDING = 64;
|
|
576
621
|
async function warmRunFingerprint(cwd) {
|
|
577
622
|
const parts = [];
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (WARM_RUN_IGNORED.test(rel))
|
|
581
|
-
continue;
|
|
623
|
+
const walk = async (dir) => {
|
|
624
|
+
let entries;
|
|
582
625
|
try {
|
|
583
|
-
|
|
584
|
-
if (s.isFile())
|
|
585
|
-
parts.push(`${rel}:${s.mtimeMs}:${s.size}`);
|
|
626
|
+
entries = await readdir(resolve(cwd, dir), { withFileTypes: true });
|
|
586
627
|
}
|
|
587
628
|
catch {
|
|
588
|
-
|
|
629
|
+
return;
|
|
589
630
|
}
|
|
590
|
-
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
const rel = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
633
|
+
if (entry.isDirectory()) {
|
|
634
|
+
if (!WARM_RUN_SKIPPED_DIR.test(entry.name))
|
|
635
|
+
await walk(rel);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (entry.name.startsWith(".") || !WARM_RUN_SOURCE.test(entry.name))
|
|
639
|
+
continue;
|
|
640
|
+
try {
|
|
641
|
+
const s = await stat(resolve(cwd, rel));
|
|
642
|
+
if (s.isFile())
|
|
643
|
+
parts.push(`${rel}:${s.mtimeMs}:${s.size}`);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// A file can disappear while an agent is editing. The next call will rescan the settled tree.
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
await walk("");
|
|
591
651
|
for (const rel of WARM_RUN_EXTRA_FILES) {
|
|
592
652
|
parts.push(`${rel}:${await fileFingerprint(resolve(cwd, rel))}`);
|
|
593
653
|
}
|
|
@@ -609,13 +669,17 @@ export class WarmWorker {
|
|
|
609
669
|
proc;
|
|
610
670
|
pending = new Map();
|
|
611
671
|
stdoutBuffer = "";
|
|
672
|
+
stdoutBufferBytes = 0;
|
|
612
673
|
stderrBuffer = "";
|
|
613
674
|
nextId = 0;
|
|
614
675
|
closed = false;
|
|
615
676
|
constructor(child, cwd, fingerprint, label) {
|
|
616
677
|
this.fingerprint = fingerprint;
|
|
617
678
|
this.label = label;
|
|
618
|
-
this.proc = Bun.spawn([
|
|
679
|
+
this.proc = Bun.spawn([process.execPath, childPath(child), cwd, "--worker"], {
|
|
680
|
+
cwd,
|
|
681
|
+
// Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
|
|
682
|
+
env: process.env,
|
|
619
683
|
stdin: "pipe",
|
|
620
684
|
stdout: "pipe",
|
|
621
685
|
stderr: "pipe",
|
|
@@ -641,7 +705,20 @@ export class WarmWorker {
|
|
|
641
705
|
throw new Error(`warm ${this.label} worker is closed`);
|
|
642
706
|
if (signal?.aborted)
|
|
643
707
|
return `${this.label} cancelled before it started.`;
|
|
708
|
+
if (this.pending.size >= MAX_WARM_PENDING)
|
|
709
|
+
throw new Error(`warm ${this.label} worker request limit reached`);
|
|
644
710
|
const id = ++this.nextId;
|
|
711
|
+
let encodedInput;
|
|
712
|
+
try {
|
|
713
|
+
encodedInput = JSON.stringify({ id, input });
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
throw new Error(`warm ${this.label} input is not serializable`);
|
|
717
|
+
}
|
|
718
|
+
if (new TextEncoder().encode(encodedInput).byteLength > CHILD_INPUT_MAX_BYTES)
|
|
719
|
+
throw new Error(`warm ${this.label} input exceeded ${CHILD_INPUT_MAX_BYTES} bytes`);
|
|
720
|
+
if (signal?.aborted)
|
|
721
|
+
return `${this.label} cancelled${cancellationSuffix(signal)}.`;
|
|
645
722
|
return new Promise((resolve, reject) => {
|
|
646
723
|
const abort = () => {
|
|
647
724
|
// Per-request cancel: drop just THIS id and resolve its cancellation. The worker is shared
|
|
@@ -650,7 +727,8 @@ export class WarmWorker {
|
|
|
650
727
|
// cold rebuild. Leave it hot - `createWarmHandler` already replaces it on file change.
|
|
651
728
|
this.pending.delete(id);
|
|
652
729
|
clearTimeout(timer);
|
|
653
|
-
|
|
730
|
+
signal?.removeEventListener("abort", abort);
|
|
731
|
+
const reason = cancellationSuffix(signal);
|
|
654
732
|
resolve(`${this.label} cancelled${reason}.`);
|
|
655
733
|
};
|
|
656
734
|
// Same backstop as the cold path: a worker wedged mid-request would otherwise leave this
|
|
@@ -669,8 +747,15 @@ export class WarmWorker {
|
|
|
669
747
|
};
|
|
670
748
|
signal?.addEventListener("abort", abort, { once: true });
|
|
671
749
|
this.pending.set(id, { resolve, reject, cleanup });
|
|
750
|
+
// JSON serialization above can run user-defined `toJSON` hooks. Recheck after installing the
|
|
751
|
+
// listener and pending entry so an abort during that synchronous work cannot enqueue a request
|
|
752
|
+
// after cancellation, and so an already-aborted signal does not retain the listener.
|
|
753
|
+
if (signal?.aborted) {
|
|
754
|
+
abort();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
672
757
|
try {
|
|
673
|
-
this.proc.stdin.write(`${
|
|
758
|
+
this.proc.stdin.write(`${encodedInput}\n`);
|
|
674
759
|
}
|
|
675
760
|
catch (err) {
|
|
676
761
|
this.pending.delete(id);
|
|
@@ -691,8 +776,10 @@ export class WarmWorker {
|
|
|
691
776
|
this.stop();
|
|
692
777
|
return;
|
|
693
778
|
}
|
|
694
|
-
|
|
695
|
-
|
|
779
|
+
const text = decoder.decode(value, { stream: true });
|
|
780
|
+
this.stdoutBuffer += text;
|
|
781
|
+
this.stdoutBufferBytes += new TextEncoder().encode(text).byteLength;
|
|
782
|
+
if (this.stdoutBufferBytes > CHILD_OUTPUT_MAX_BYTES) {
|
|
696
783
|
this.stderrBuffer = boundedAppend(this.stderrBuffer, `warm ${this.label} worker stdout exceeded ${CHILD_OUTPUT_MAX_BYTES} bytes\n`);
|
|
697
784
|
this.stop();
|
|
698
785
|
return;
|
|
@@ -701,6 +788,7 @@ export class WarmWorker {
|
|
|
701
788
|
while (nl !== -1) {
|
|
702
789
|
const line = this.stdoutBuffer.slice(0, nl).trim();
|
|
703
790
|
this.stdoutBuffer = this.stdoutBuffer.slice(nl + 1);
|
|
791
|
+
this.stdoutBufferBytes = new TextEncoder().encode(this.stdoutBuffer).byteLength;
|
|
704
792
|
nl = this.stdoutBuffer.indexOf("\n");
|
|
705
793
|
if (line !== "")
|
|
706
794
|
this.handleLine(line);
|
|
@@ -724,18 +812,56 @@ export class WarmWorker {
|
|
|
724
812
|
}
|
|
725
813
|
catch {
|
|
726
814
|
this.stderrBuffer = boundedAppend(this.stderrBuffer, `invalid warm ${this.label} worker line: ${line}\n`);
|
|
815
|
+
this.stop();
|
|
727
816
|
return;
|
|
728
817
|
}
|
|
729
|
-
if (
|
|
818
|
+
if (message === null ||
|
|
819
|
+
typeof message !== "object" ||
|
|
820
|
+
Array.isArray(message) ||
|
|
821
|
+
typeof message.id !== "number" ||
|
|
822
|
+
!Number.isSafeInteger(message.id)) {
|
|
823
|
+
this.stderrBuffer = boundedAppend(this.stderrBuffer, `invalid warm ${this.label} worker response envelope\n`);
|
|
824
|
+
this.stop();
|
|
730
825
|
return;
|
|
731
|
-
|
|
826
|
+
}
|
|
827
|
+
const response = message;
|
|
828
|
+
const pending = this.pending.get(response.id);
|
|
732
829
|
if (pending === undefined)
|
|
733
830
|
return;
|
|
734
|
-
|
|
831
|
+
if (!Object.hasOwn(response, "output")) {
|
|
832
|
+
this.pending.delete(response.id);
|
|
833
|
+
pending.cleanup();
|
|
834
|
+
pending.reject(new Error(`warm ${this.label} worker response is malformed`));
|
|
835
|
+
this.stop();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
let output;
|
|
839
|
+
try {
|
|
840
|
+
output = JSON.stringify(response.output, null, 2);
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
this.pending.delete(response.id);
|
|
844
|
+
pending.cleanup();
|
|
845
|
+
pending.reject(new Error(`warm ${this.label} worker response is not serializable`));
|
|
846
|
+
this.stop();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (new TextEncoder().encode(output).byteLength > CHILD_OUTPUT_MAX_BYTES) {
|
|
850
|
+
this.pending.delete(response.id);
|
|
851
|
+
pending.cleanup();
|
|
852
|
+
pending.reject(new Error(`warm ${this.label} worker response exceeded the size limit`));
|
|
853
|
+
this.stop();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
this.pending.delete(response.id);
|
|
735
857
|
pending.cleanup();
|
|
736
|
-
pending.resolve(
|
|
858
|
+
pending.resolve(output);
|
|
737
859
|
}
|
|
738
860
|
}
|
|
861
|
+
function cancellationSuffix(signal) {
|
|
862
|
+
const reason = signal?.reason;
|
|
863
|
+
return typeof reason === "string" && reason.length > 0 ? `: ${reason.slice(0, 512)}` : "";
|
|
864
|
+
}
|
|
739
865
|
/** A warm handler that reuses a hot {@link WarmWorker} across calls and falls back to a one-shot fresh
|
|
740
866
|
* subprocess on any worker failure. Shared by `nifra_run` (`child: "mcp-run"`, `label: "run"`) and
|
|
741
867
|
* `nifra_render` (`child: "mcp-render"`, `label: "render"`) - the only differences are the engine and the
|
|
@@ -744,18 +870,23 @@ export function createWarmHandler(child, cwd, label) {
|
|
|
744
870
|
let worker;
|
|
745
871
|
return async (input, signal) => {
|
|
746
872
|
const fingerprint = await warmRunFingerprint(cwd);
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
873
|
+
let current = worker;
|
|
874
|
+
if (current === undefined || current.fingerprint !== fingerprint) {
|
|
875
|
+
current?.stop();
|
|
876
|
+
current = new WarmWorker(child, cwd, fingerprint, label);
|
|
877
|
+
worker = current;
|
|
750
878
|
}
|
|
751
879
|
try {
|
|
752
|
-
return await
|
|
880
|
+
return await current.request(input, signal);
|
|
753
881
|
}
|
|
754
882
|
catch {
|
|
755
|
-
worker.
|
|
756
|
-
|
|
883
|
+
// A source change can replace the shared worker while this request is in flight. Only clear
|
|
884
|
+
// the slot if this request still owns it; an old request must never tear down the replacement.
|
|
885
|
+
current.stop();
|
|
886
|
+
if (worker === current)
|
|
887
|
+
worker = undefined;
|
|
757
888
|
if (signal?.aborted) {
|
|
758
|
-
const reason =
|
|
889
|
+
const reason = cancellationSuffix(signal);
|
|
759
890
|
return `${label} cancelled${reason}.`;
|
|
760
891
|
}
|
|
761
892
|
return spawnChild(child, cwd, input, label, signal);
|
|
@@ -794,7 +925,7 @@ export async function renderHandler(cwd, args, context, warmRender) {
|
|
|
794
925
|
export async function wsHandler(cwd, args, context) {
|
|
795
926
|
return spawnChild("mcp-ws", cwd, args, "websocket", context.signal);
|
|
796
927
|
}
|
|
797
|
-
const APP_FINGERPRINT_FILES = [
|
|
928
|
+
const APP_FINGERPRINT_FILES = [CONFIG_FILE, FRAMEWORK_FILE, BACKEND_APP_FILE];
|
|
798
929
|
function cacheToken(input) {
|
|
799
930
|
let hash = 2166136261;
|
|
800
931
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -815,7 +946,8 @@ async function fileFingerprint(path) {
|
|
|
815
946
|
throw err;
|
|
816
947
|
}
|
|
817
948
|
}
|
|
818
|
-
|
|
949
|
+
/** `mtime:size` of the app's config, framework and backend entry files: what notices an edit. */
|
|
950
|
+
export async function appFingerprint(cwd) {
|
|
819
951
|
return (await Promise.all(APP_FINGERPRINT_FILES.map(async (file) => `${file}:${await fileFingerprint(resolve(cwd, file))}`))).join("|");
|
|
820
952
|
}
|
|
821
953
|
/** Cache LoadedApp inside one MCP server process, invalidating when config/backend mtimes change. */
|