@nifrajs/cli 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +7 -15
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +162 -713
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +58 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +323 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +1 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +145 -54
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +306 -87
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +60 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +298 -33
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts +3 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +56 -16
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diagnostics.d.ts +86 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +85 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +41 -21
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -4
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +93 -74
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +71 -16
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +5 -6
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +3 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +263 -131
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +20 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +113 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-path.d.ts +10 -0
- package/dist/mcp-path.d.ts.map +1 -0
- package/dist/mcp-path.js +51 -0
- package/dist/mcp-path.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-reflect.d.ts.map +1 -1
- package/dist/mcp-reflect.js +2 -2
- package/dist/mcp-reflect.js.map +1 -1
- package/dist/mcp-render.d.ts.map +1 -1
- package/dist/mcp-render.js +64 -45
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +124 -25
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +159 -55
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +18 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +165 -82
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +982 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +28 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +12 -2
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +34 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts +21 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +823 -0
- package/dist/review.js.map +1 -0
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts +3 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +829 -34
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +154 -90
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +8 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts +3 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +33 -16
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +4123 -548
- package/docs/types.json +14391 -10060
- package/package.json +19 -11
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +199 -797
- package/src/check-scan.ts +394 -63
- package/src/check.ts +146 -58
- package/src/cli.ts +347 -92
- package/src/command-catalog.ts +407 -45
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +66 -15
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diagnostics.ts +179 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +44 -31
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +93 -82
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +144 -32
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +5 -7
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +301 -161
- package/src/mcp-http.ts +22 -5
- package/src/mcp-io.ts +130 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-path.ts +50 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-reflect.ts +3 -2
- package/src/mcp-render.ts +72 -42
- package/src/mcp-root.ts +130 -24
- package/src/mcp-run.ts +190 -56
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +193 -91
- package/src/migrate-layout.ts +1121 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +42 -8
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +988 -0
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +947 -35
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +175 -98
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +27 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +40 -16
package/src/mcp-exec.ts
CHANGED
|
@@ -6,17 +6,20 @@
|
|
|
6
6
|
* replaceable.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { type Dirent, realpathSync } from "node:fs"
|
|
10
|
+
import { readdir, stat } from "node:fs/promises"
|
|
11
|
+
import { relative, resolve } from "node:path"
|
|
11
12
|
import { fileURLToPath } from "node:url"
|
|
12
|
-
import {
|
|
13
|
+
import { BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
13
14
|
import {
|
|
14
15
|
type CommandCatalogEntry,
|
|
15
16
|
type CommandCtx,
|
|
16
17
|
type CommandSpec,
|
|
17
18
|
commandCatalog,
|
|
19
|
+
commandMcpInputSchema,
|
|
18
20
|
commandMcpName,
|
|
19
21
|
findCommandSpec,
|
|
22
|
+
toCommandCatalogEntry,
|
|
20
23
|
} from "./command-catalog.ts"
|
|
21
24
|
import { collectContractProof } from "./contract-proof.ts"
|
|
22
25
|
import { loadDocsCorpus } from "./docs-search.ts"
|
|
@@ -30,18 +33,35 @@ import {
|
|
|
30
33
|
} from "./mcp-context.ts"
|
|
31
34
|
import { docsTools } from "./mcp-docs-tools.ts"
|
|
32
35
|
import {
|
|
36
|
+
CHILD_INPUT_MAX_BYTES,
|
|
33
37
|
CHILD_OUTPUT_MAX_BYTES,
|
|
34
38
|
CHILD_TIMEOUT_MS,
|
|
35
|
-
LOCAL_TOOL_FETCH_TIMEOUT_MS,
|
|
36
|
-
notNifraResponse,
|
|
37
|
-
readBoundedResponse,
|
|
38
39
|
readBoundedStream,
|
|
39
40
|
timeoutMessage,
|
|
40
41
|
validateLocalPort,
|
|
41
42
|
} from "./mcp-io.ts"
|
|
43
|
+
import { mcpProjectPathError, resolveMcpProjectPath } from "./mcp-path.ts"
|
|
42
44
|
import type { McpTool, McpToolContext } from "./mcp-protocol.ts"
|
|
43
45
|
import { loadTypesCorpus } from "./types-search.ts"
|
|
44
46
|
|
|
47
|
+
const CODEFRAME_SOURCE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|mdx)$/i
|
|
48
|
+
|
|
49
|
+
/** 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
|
+
function explainSourceGate(root: string): (file: string) => boolean {
|
|
52
|
+
// The codeframe reads a native realpath, which expands a Windows short name (`RUNNER~1`); the root
|
|
53
|
+
// has to be spelled the same way or every file looks like it sits outside it.
|
|
54
|
+
let base = resolve(root)
|
|
55
|
+
try {
|
|
56
|
+
base = realpathSync.native(base)
|
|
57
|
+
} catch {}
|
|
58
|
+
return (file) =>
|
|
59
|
+
CODEFRAME_SOURCE.test(file) &&
|
|
60
|
+
!relative(base, file)
|
|
61
|
+
.split(/[\\/]/)
|
|
62
|
+
.some((segment) => segment.startsWith("."))
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
/** Path to a sibling child entry (`mcp-run` / `mcp-render` / `mcp-ws`), resolved next to this module (`.ts` in
|
|
46
66
|
* dev, `.js` once built). Each runs in a FRESH subprocess per call so the project's current code loads. */
|
|
47
67
|
|
|
@@ -54,25 +74,49 @@ export function toMcpTool(
|
|
|
54
74
|
const entry = "run" in source ? undefined : source
|
|
55
75
|
const { cwd } = options
|
|
56
76
|
const loadAppCached = options.loadAppCached ?? createCachedAppLoader(cwd)
|
|
77
|
+
const pathFields = ["config", "out", "lockfile", "before", "after", "baseline", "file"] as const
|
|
78
|
+
const catalogEntry = entry ?? toCommandCatalogEntry(spec)
|
|
57
79
|
return {
|
|
58
|
-
name: commandMcpName(
|
|
59
|
-
description:
|
|
60
|
-
inputSchema:
|
|
80
|
+
name: commandMcpName(catalogEntry.name),
|
|
81
|
+
description: catalogEntry.summary,
|
|
82
|
+
inputSchema: commandMcpInputSchema(catalogEntry),
|
|
61
83
|
handler: async (args: Record<string, unknown>, context: McpToolContext) => {
|
|
62
84
|
const raw = { ...args }
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const target = resolveProjectDir(cwd, dir as string | undefined)
|
|
66
|
-
if (target === null) return dirError(dir as string | undefined)
|
|
67
|
-
if (typeof raw.config === "string") {
|
|
68
|
-
const config = resolve(target, raw.config)
|
|
69
|
-
if (config !== target && !config.startsWith(`${target}${sep}`))
|
|
85
|
+
for (const field of catalogEntry.cliOnlyFields) {
|
|
86
|
+
if (raw[field] !== undefined)
|
|
70
87
|
return JSON.stringify(
|
|
71
|
-
{ ok: false, error:
|
|
88
|
+
{ ok: false, error: `${field} is available only from the nifra CLI` },
|
|
72
89
|
null,
|
|
73
90
|
2,
|
|
74
91
|
)
|
|
75
92
|
}
|
|
93
|
+
const dir = raw.dir
|
|
94
|
+
if (dir !== undefined && typeof dir !== "string") return dirError(undefined)
|
|
95
|
+
const target = resolveProjectDir(cwd, dir as string | undefined)
|
|
96
|
+
if (target === null) return dirError(dir as string | undefined)
|
|
97
|
+
for (const field of pathFields) {
|
|
98
|
+
const value = raw[field]
|
|
99
|
+
if (typeof value !== "string") continue
|
|
100
|
+
const safe = resolveMcpProjectPath(target, value)
|
|
101
|
+
if (safe === null)
|
|
102
|
+
return JSON.stringify({ ok: false, error: mcpProjectPathError(field, value) }, null, 2)
|
|
103
|
+
raw[field] = safe
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(raw.files)) {
|
|
106
|
+
const safeFiles: string[] = []
|
|
107
|
+
for (const value of raw.files) {
|
|
108
|
+
if (typeof value !== "string") continue
|
|
109
|
+
const safe = resolveMcpProjectPath(target, value)
|
|
110
|
+
if (safe === null)
|
|
111
|
+
return JSON.stringify(
|
|
112
|
+
{ ok: false, error: mcpProjectPathError("files", value) },
|
|
113
|
+
null,
|
|
114
|
+
2,
|
|
115
|
+
)
|
|
116
|
+
safeFiles.push(safe)
|
|
117
|
+
}
|
|
118
|
+
raw.files = safeFiles
|
|
119
|
+
}
|
|
76
120
|
delete raw.dir
|
|
77
121
|
let input: unknown
|
|
78
122
|
try {
|
|
@@ -121,7 +165,7 @@ export function projectTools(
|
|
|
121
165
|
{
|
|
122
166
|
name: "nifra_verify",
|
|
123
167
|
description:
|
|
124
|
-
"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.",
|
|
168
|
+
"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.",
|
|
125
169
|
inputSchema: {
|
|
126
170
|
type: "object",
|
|
127
171
|
properties: {
|
|
@@ -194,10 +238,21 @@ export function projectTools(
|
|
|
194
238
|
if (baseline !== undefined && baseline.trim() === "") {
|
|
195
239
|
return JSON.stringify({ ok: false, error: "baseline must not be empty" }, null, 2)
|
|
196
240
|
}
|
|
241
|
+
const safeBaseline =
|
|
242
|
+
baseline === undefined ? undefined : resolveMcpProjectPath(target, baseline)
|
|
243
|
+
if (baseline !== undefined && safeBaseline === null) {
|
|
244
|
+
return JSON.stringify(
|
|
245
|
+
{ ok: false, error: mcpProjectPathError("baseline", baseline) },
|
|
246
|
+
null,
|
|
247
|
+
2,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
197
250
|
try {
|
|
198
251
|
return JSON.stringify(
|
|
199
252
|
await collectContractProof(target, {
|
|
200
|
-
...(
|
|
253
|
+
...(safeBaseline === undefined || safeBaseline === null
|
|
254
|
+
? {}
|
|
255
|
+
: { baselinePath: safeBaseline }),
|
|
201
256
|
...(raw.check === undefined ? {} : { check: raw.check === true }),
|
|
202
257
|
}),
|
|
203
258
|
null,
|
|
@@ -215,7 +270,7 @@ export function projectTools(
|
|
|
215
270
|
{
|
|
216
271
|
name: "nifra_explain",
|
|
217
272
|
description:
|
|
218
|
-
"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
|
|
273
|
+
"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.",
|
|
219
274
|
inputSchema: {
|
|
220
275
|
type: "object",
|
|
221
276
|
properties: {
|
|
@@ -237,144 +292,119 @@ export function projectTools(
|
|
|
237
292
|
port: {
|
|
238
293
|
type: "number",
|
|
239
294
|
description:
|
|
240
|
-
"
|
|
295
|
+
"The dev server's port, when the workspace runs several. Optional: the project's `nifra dev` server is found automatically.",
|
|
296
|
+
},
|
|
297
|
+
dir: {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: "The app directory, when the workspace runs more than one dev server.",
|
|
241
300
|
},
|
|
242
301
|
},
|
|
243
302
|
additionalProperties: false,
|
|
244
303
|
},
|
|
245
|
-
handler: async (args) => {
|
|
246
|
-
const { error, stack, name, port } = args as {
|
|
304
|
+
handler: async (args, context) => {
|
|
305
|
+
const { error, stack, name, port, dir } = args as {
|
|
247
306
|
error?: string
|
|
248
307
|
stack?: string
|
|
249
308
|
name?: string
|
|
250
309
|
port?: number
|
|
310
|
+
dir?: string
|
|
251
311
|
}
|
|
252
312
|
if (error !== undefined || stack !== undefined) {
|
|
253
313
|
const { buildDiagnostic } = await import("@nifrajs/web/diagnostic")
|
|
254
314
|
const e = new Error(error ?? "")
|
|
255
315
|
if (name !== undefined) e.name = name
|
|
256
316
|
if (stack !== undefined) e.stack = stack
|
|
257
|
-
return JSON.stringify(
|
|
317
|
+
return JSON.stringify(
|
|
318
|
+
buildDiagnostic(e, { root: cwd, showSource: explainSourceGate(cwd) }),
|
|
319
|
+
null,
|
|
320
|
+
2,
|
|
321
|
+
)
|
|
258
322
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
},
|
|
281
|
-
null,
|
|
282
|
-
2,
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
return await readBoundedResponse(res)
|
|
286
|
-
} catch (cause) {
|
|
287
|
-
return JSON.stringify(
|
|
288
|
-
{
|
|
289
|
-
code: "NIFRA_NONE",
|
|
290
|
-
message: `could not reach a nifra dev server at :${validPort} - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
291
|
-
},
|
|
292
|
-
null,
|
|
293
|
-
2,
|
|
294
|
-
)
|
|
295
|
-
}
|
|
323
|
+
const target = resolveProjectDir(cwd, dir)
|
|
324
|
+
if (target === null) return dirError(dir)
|
|
325
|
+
if (port !== undefined && validateLocalPort(port) === undefined) {
|
|
326
|
+
return JSON.stringify(
|
|
327
|
+
{ code: "NIFRA_INVALID_PORT", message: "port must be an integer from 1 to 65535." },
|
|
328
|
+
null,
|
|
329
|
+
2,
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
const { explainLatest } = await import("./dev-feed-tool.ts")
|
|
333
|
+
try {
|
|
334
|
+
return JSON.stringify(await explainLatest(target, port, context.signal), null, 2)
|
|
335
|
+
} catch (cause) {
|
|
336
|
+
return JSON.stringify(
|
|
337
|
+
{
|
|
338
|
+
code: "NIFRA_NONE",
|
|
339
|
+
message: `could not read the dev server's errors - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
340
|
+
},
|
|
341
|
+
null,
|
|
342
|
+
2,
|
|
343
|
+
)
|
|
296
344
|
}
|
|
297
|
-
return JSON.stringify(
|
|
298
|
-
{
|
|
299
|
-
code: "NIFRA_NONE",
|
|
300
|
-
message:
|
|
301
|
-
"Pass `error` (and `stack` if available) to explain a failure, or `port` to fetch the dev server's last error.",
|
|
302
|
-
},
|
|
303
|
-
null,
|
|
304
|
-
2,
|
|
305
|
-
)
|
|
306
345
|
},
|
|
307
346
|
},
|
|
308
347
|
{
|
|
309
348
|
name: "nifra_inspect",
|
|
310
349
|
description:
|
|
311
|
-
"Observe what your requests ACTUALLY did on the running dev server
|
|
350
|
+
"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.",
|
|
312
351
|
inputSchema: {
|
|
313
352
|
type: "object",
|
|
314
353
|
properties: {
|
|
315
|
-
port: {
|
|
354
|
+
port: {
|
|
355
|
+
type: "number",
|
|
356
|
+
description:
|
|
357
|
+
"The dev server's port. Optional: the project's server is found automatically.",
|
|
358
|
+
},
|
|
316
359
|
path: { type: "string", description: "Only traces whose path starts with this prefix." },
|
|
360
|
+
requestId: { type: "string", description: "Only the trace with this request id." },
|
|
361
|
+
since: {
|
|
362
|
+
type: "number",
|
|
363
|
+
description: "Only traces after this cursor (the `cursor` an earlier call returned).",
|
|
364
|
+
},
|
|
317
365
|
limit: { type: "number", description: "Return only the most recent N traces." },
|
|
366
|
+
dir: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "The app directory, when the workspace runs more than one dev server.",
|
|
369
|
+
},
|
|
318
370
|
},
|
|
319
|
-
required: ["port"],
|
|
320
371
|
additionalProperties: false,
|
|
321
372
|
},
|
|
322
|
-
handler: async (args) => {
|
|
323
|
-
const { port, path, limit } = args as {
|
|
324
|
-
|
|
373
|
+
handler: async (args, context) => {
|
|
374
|
+
const { port, path, requestId, since, limit, dir } = args as {
|
|
375
|
+
port?: number
|
|
376
|
+
path?: string
|
|
377
|
+
requestId?: string
|
|
378
|
+
since?: number
|
|
379
|
+
limit?: number
|
|
380
|
+
dir?: string
|
|
381
|
+
}
|
|
382
|
+
const target = resolveProjectDir(cwd, dir)
|
|
383
|
+
if (target === null) return dirError(dir)
|
|
384
|
+
if (port !== undefined && validateLocalPort(port) === undefined) {
|
|
325
385
|
return JSON.stringify(
|
|
326
386
|
{
|
|
327
|
-
|
|
328
|
-
|
|
387
|
+
requests: [],
|
|
388
|
+
code: "NIFRA_INVALID_PORT",
|
|
389
|
+
note: "port must be an integer from 1 to 65535.",
|
|
329
390
|
},
|
|
330
391
|
null,
|
|
331
392
|
2,
|
|
332
393
|
)
|
|
333
394
|
}
|
|
395
|
+
const { inspectRequests } = await import("./dev-feed-tool.ts")
|
|
334
396
|
try {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
note: "port must be an integer from 1 to 65535.",
|
|
342
|
-
},
|
|
343
|
-
null,
|
|
344
|
-
2,
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
const url = new URL(`http://127.0.0.1:${validPort}/_nifra/devtools/state`)
|
|
348
|
-
if (path !== undefined) url.searchParams.set("path", path)
|
|
349
|
-
if (limit !== undefined) url.searchParams.set("limit", String(limit))
|
|
350
|
-
const res = await fetch(url, {
|
|
351
|
-
signal: AbortSignal.timeout(LOCAL_TOOL_FETCH_TIMEOUT_MS),
|
|
352
|
-
})
|
|
353
|
-
if (res.headers.get("x-nifra-devtools") !== "true") return notNifraResponse("DevTools")
|
|
354
|
-
if (res.status === 404) {
|
|
355
|
-
return JSON.stringify(
|
|
356
|
-
{
|
|
357
|
-
events: [],
|
|
358
|
-
note: "No DevTools endpoint on that server. Mount `devtools()` from @nifrajs/web and run in development.",
|
|
359
|
-
},
|
|
360
|
-
null,
|
|
361
|
-
2,
|
|
362
|
-
)
|
|
363
|
-
}
|
|
364
|
-
if (!res.ok) {
|
|
365
|
-
return JSON.stringify(
|
|
366
|
-
{ events: [], note: `DevTools state returned ${res.status}.` },
|
|
367
|
-
null,
|
|
368
|
-
2,
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
return await readBoundedResponse(res)
|
|
397
|
+
const result = await inspectRequests(
|
|
398
|
+
target,
|
|
399
|
+
{ port, path, requestId, since, limit },
|
|
400
|
+
context.signal,
|
|
401
|
+
)
|
|
402
|
+
return JSON.stringify(result, null, 2)
|
|
372
403
|
} catch (cause) {
|
|
373
|
-
const validPort = validateLocalPort(port)
|
|
374
404
|
return JSON.stringify(
|
|
375
405
|
{
|
|
376
|
-
|
|
377
|
-
note: `Could not
|
|
406
|
+
requests: [],
|
|
407
|
+
note: `Could not read the dev server's request traces - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
378
408
|
},
|
|
379
409
|
null,
|
|
380
410
|
2,
|
|
@@ -385,7 +415,7 @@ export function projectTools(
|
|
|
385
415
|
{
|
|
386
416
|
name: "nifra_openapi",
|
|
387
417
|
description:
|
|
388
|
-
'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.',
|
|
418
|
+
'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.',
|
|
389
419
|
inputSchema: {
|
|
390
420
|
type: "object",
|
|
391
421
|
properties: {
|
|
@@ -407,7 +437,7 @@ export function projectTools(
|
|
|
407
437
|
{
|
|
408
438
|
name: "nifra_run",
|
|
409
439
|
description:
|
|
410
|
-
"Run HTTP requests through this project's backend and return structured results
|
|
440
|
+
"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? }.",
|
|
411
441
|
inputSchema: {
|
|
412
442
|
type: "object",
|
|
413
443
|
properties: {
|
|
@@ -426,7 +456,7 @@ export function projectTools(
|
|
|
426
456
|
},
|
|
427
457
|
entry: {
|
|
428
458
|
type: "string",
|
|
429
|
-
description: "Backend entry file (default: backend.ts | app.ts).",
|
|
459
|
+
description: "Backend entry file (default: backend/app.ts | app.ts).",
|
|
430
460
|
},
|
|
431
461
|
warm: {
|
|
432
462
|
type: "boolean",
|
|
@@ -496,7 +526,7 @@ export function projectTools(
|
|
|
496
526
|
},
|
|
497
527
|
entry: {
|
|
498
528
|
type: "string",
|
|
499
|
-
description: "Backend entry file (default: backend.ts | app.ts).",
|
|
529
|
+
description: "Backend entry file (default: backend/app.ts | app.ts).",
|
|
500
530
|
},
|
|
501
531
|
},
|
|
502
532
|
required: ["path"],
|
|
@@ -513,7 +543,8 @@ export function projectTools(
|
|
|
513
543
|
properties: {
|
|
514
544
|
pattern: {
|
|
515
545
|
type: "string",
|
|
516
|
-
description:
|
|
546
|
+
description:
|
|
547
|
+
"Optional test file/path pattern inside the project, passed as an argv item to `bun test`.",
|
|
517
548
|
},
|
|
518
549
|
timeoutMs: {
|
|
519
550
|
type: "number",
|
|
@@ -545,7 +576,7 @@ export function projectTools(
|
|
|
545
576
|
{
|
|
546
577
|
name: "nifra_scaffold",
|
|
547
578
|
description:
|
|
548
|
-
'Map a URL path to the CORRECT routes/
|
|
579
|
+
'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).',
|
|
549
580
|
inputSchema: {
|
|
550
581
|
type: "object",
|
|
551
582
|
properties: {
|
|
@@ -556,7 +587,7 @@ export function projectTools(
|
|
|
556
587
|
write: {
|
|
557
588
|
type: "boolean",
|
|
558
589
|
description:
|
|
559
|
-
"When true, create
|
|
590
|
+
"When true, create both files if a verified ready-to-write page stub exists. Refuses overwrite.",
|
|
560
591
|
},
|
|
561
592
|
variant: {
|
|
562
593
|
type: "string",
|
|
@@ -584,7 +615,7 @@ export function projectTools(
|
|
|
584
615
|
if (write !== true) return renderScaffold(path, framework, flavour)
|
|
585
616
|
const result = await writeScaffoldRoute(cwd, path, framework, flavour)
|
|
586
617
|
const status = result.written
|
|
587
|
-
? `Written: \`${result.file}\``
|
|
618
|
+
? `Written: \`${result.file}\` and \`${result.backend.file}\``
|
|
588
619
|
: `Not written: ${result.reason ?? "no write performed"}`
|
|
589
620
|
return `${status}\n\n${renderScaffold(path, framework, flavour)}`
|
|
590
621
|
},
|
|
@@ -600,13 +631,17 @@ export function projectTools(
|
|
|
600
631
|
},
|
|
601
632
|
additionalProperties: false,
|
|
602
633
|
},
|
|
603
|
-
handler: async (args) => {
|
|
634
|
+
handler: async (args, context) => {
|
|
604
635
|
const opts = args as { dir?: string; interact?: boolean }
|
|
605
636
|
const target = resolveProjectDir(cwd, opts.dir)
|
|
606
637
|
if (target === null) return dirError(opts.dir)
|
|
607
638
|
const { runHydrationAssurance } = await import("./assure-hydration.ts")
|
|
608
639
|
return JSON.stringify(
|
|
609
|
-
await runHydrationAssurance(
|
|
640
|
+
await runHydrationAssurance(
|
|
641
|
+
target,
|
|
642
|
+
{ interact: opts.interact === true },
|
|
643
|
+
{ signal: context.signal },
|
|
644
|
+
),
|
|
610
645
|
null,
|
|
611
646
|
2,
|
|
612
647
|
)
|
|
@@ -641,7 +676,19 @@ export async function spawnChild(
|
|
|
641
676
|
signal?: AbortSignal,
|
|
642
677
|
): Promise<string> {
|
|
643
678
|
if (signal?.aborted) return `${label} cancelled before it started.`
|
|
644
|
-
|
|
679
|
+
let encodedInput: string
|
|
680
|
+
try {
|
|
681
|
+
encodedInput = JSON.stringify(input)
|
|
682
|
+
} catch {
|
|
683
|
+
return `${label} input is not serializable.`
|
|
684
|
+
}
|
|
685
|
+
if (new TextEncoder().encode(encodedInput).byteLength > CHILD_INPUT_MAX_BYTES)
|
|
686
|
+
return `${label} input exceeded ${CHILD_INPUT_MAX_BYTES} bytes.`
|
|
687
|
+
if (signal?.aborted) return `${label} cancelled${cancellationSuffix(signal)}.`
|
|
688
|
+
const proc = Bun.spawn([process.execPath, childPath(child), cwd], {
|
|
689
|
+
cwd,
|
|
690
|
+
// Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
|
|
691
|
+
env: process.env,
|
|
645
692
|
stdin: "pipe",
|
|
646
693
|
stdout: "pipe",
|
|
647
694
|
stderr: "pipe",
|
|
@@ -661,8 +708,11 @@ export async function spawnChild(
|
|
|
661
708
|
}, CHILD_TIMEOUT_MS)
|
|
662
709
|
let outputExceeded = false
|
|
663
710
|
try {
|
|
664
|
-
|
|
665
|
-
|
|
711
|
+
if (signal?.aborted) kill()
|
|
712
|
+
else {
|
|
713
|
+
proc.stdin.write(encodedInput)
|
|
714
|
+
await proc.stdin.end()
|
|
715
|
+
}
|
|
666
716
|
const [out, err] = await Promise.all([
|
|
667
717
|
readBoundedStream(proc.stdout, CHILD_OUTPUT_MAX_BYTES, () => {
|
|
668
718
|
outputExceeded = true
|
|
@@ -675,7 +725,7 @@ export async function spawnChild(
|
|
|
675
725
|
])
|
|
676
726
|
await proc.exited
|
|
677
727
|
if (signal?.aborted) {
|
|
678
|
-
const reason =
|
|
728
|
+
const reason = cancellationSuffix(signal)
|
|
679
729
|
return `${label} cancelled${reason}.`
|
|
680
730
|
}
|
|
681
731
|
if (outputExceeded) {
|
|
@@ -692,23 +742,38 @@ export async function spawnChild(
|
|
|
692
742
|
}
|
|
693
743
|
}
|
|
694
744
|
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
745
|
+
const WARM_RUN_SOURCE = /\.(?:[cm]?[jt]sx?|json)$/
|
|
746
|
+
// Pruned while walking, not filtered after: a scan that descends into node_modules on every warm call
|
|
747
|
+
// costs more than the cold start the worker saves.
|
|
748
|
+
const WARM_RUN_SKIPPED_DIR = /^(?:\..*|node_modules|dist(?:-[a-z0-9]+)?|build|coverage)$/
|
|
698
749
|
const WARM_RUN_EXTRA_FILES = ["bun.lock", "bun.lockb"] as const
|
|
750
|
+
const MAX_WARM_PENDING = 64
|
|
699
751
|
|
|
700
752
|
async function warmRunFingerprint(cwd: string): Promise<string> {
|
|
701
753
|
const parts: string[] = []
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (WARM_RUN_IGNORED.test(rel)) continue
|
|
754
|
+
const walk = async (dir: string): Promise<void> => {
|
|
755
|
+
let entries: Dirent[]
|
|
705
756
|
try {
|
|
706
|
-
|
|
707
|
-
if (s.isFile()) parts.push(`${rel}:${s.mtimeMs}:${s.size}`)
|
|
757
|
+
entries = await readdir(resolve(cwd, dir), { withFileTypes: true })
|
|
708
758
|
} catch {
|
|
709
|
-
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
for (const entry of entries) {
|
|
762
|
+
const rel = dir === "" ? entry.name : `${dir}/${entry.name}`
|
|
763
|
+
if (entry.isDirectory()) {
|
|
764
|
+
if (!WARM_RUN_SKIPPED_DIR.test(entry.name)) await walk(rel)
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
if (entry.name.startsWith(".") || !WARM_RUN_SOURCE.test(entry.name)) continue
|
|
768
|
+
try {
|
|
769
|
+
const s = await stat(resolve(cwd, rel))
|
|
770
|
+
if (s.isFile()) parts.push(`${rel}:${s.mtimeMs}:${s.size}`)
|
|
771
|
+
} catch {
|
|
772
|
+
// A file can disappear while an agent is editing. The next call will rescan the settled tree.
|
|
773
|
+
}
|
|
710
774
|
}
|
|
711
775
|
}
|
|
776
|
+
await walk("")
|
|
712
777
|
for (const rel of WARM_RUN_EXTRA_FILES) {
|
|
713
778
|
parts.push(`${rel}:${await fileFingerprint(resolve(cwd, rel))}`)
|
|
714
779
|
}
|
|
@@ -743,6 +808,7 @@ export class WarmWorker {
|
|
|
743
808
|
}
|
|
744
809
|
>()
|
|
745
810
|
private stdoutBuffer = ""
|
|
811
|
+
private stdoutBufferBytes = 0
|
|
746
812
|
private stderrBuffer = ""
|
|
747
813
|
private nextId = 0
|
|
748
814
|
private closed = false
|
|
@@ -753,7 +819,10 @@ export class WarmWorker {
|
|
|
753
819
|
readonly fingerprint: string,
|
|
754
820
|
private readonly label: string,
|
|
755
821
|
) {
|
|
756
|
-
this.proc = Bun.spawn([
|
|
822
|
+
this.proc = Bun.spawn([process.execPath, childPath(child), cwd, "--worker"], {
|
|
823
|
+
cwd,
|
|
824
|
+
// Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
|
|
825
|
+
env: process.env,
|
|
757
826
|
stdin: "pipe",
|
|
758
827
|
stdout: "pipe",
|
|
759
828
|
stderr: "pipe",
|
|
@@ -780,7 +849,18 @@ export class WarmWorker {
|
|
|
780
849
|
async request(input: unknown, signal?: AbortSignal): Promise<string> {
|
|
781
850
|
if (this.closed) throw new Error(`warm ${this.label} worker is closed`)
|
|
782
851
|
if (signal?.aborted) return `${this.label} cancelled before it started.`
|
|
852
|
+
if (this.pending.size >= MAX_WARM_PENDING)
|
|
853
|
+
throw new Error(`warm ${this.label} worker request limit reached`)
|
|
783
854
|
const id = ++this.nextId
|
|
855
|
+
let encodedInput: string
|
|
856
|
+
try {
|
|
857
|
+
encodedInput = JSON.stringify({ id, input })
|
|
858
|
+
} catch {
|
|
859
|
+
throw new Error(`warm ${this.label} input is not serializable`)
|
|
860
|
+
}
|
|
861
|
+
if (new TextEncoder().encode(encodedInput).byteLength > CHILD_INPUT_MAX_BYTES)
|
|
862
|
+
throw new Error(`warm ${this.label} input exceeded ${CHILD_INPUT_MAX_BYTES} bytes`)
|
|
863
|
+
if (signal?.aborted) return `${this.label} cancelled${cancellationSuffix(signal)}.`
|
|
784
864
|
return new Promise((resolve, reject) => {
|
|
785
865
|
const abort = (): void => {
|
|
786
866
|
// Per-request cancel: drop just THIS id and resolve its cancellation. The worker is shared
|
|
@@ -789,7 +869,8 @@ export class WarmWorker {
|
|
|
789
869
|
// cold rebuild. Leave it hot - `createWarmHandler` already replaces it on file change.
|
|
790
870
|
this.pending.delete(id)
|
|
791
871
|
clearTimeout(timer)
|
|
792
|
-
|
|
872
|
+
signal?.removeEventListener("abort", abort)
|
|
873
|
+
const reason = cancellationSuffix(signal)
|
|
793
874
|
resolve(`${this.label} cancelled${reason}.`)
|
|
794
875
|
}
|
|
795
876
|
// Same backstop as the cold path: a worker wedged mid-request would otherwise leave this
|
|
@@ -807,8 +888,15 @@ export class WarmWorker {
|
|
|
807
888
|
}
|
|
808
889
|
signal?.addEventListener("abort", abort, { once: true })
|
|
809
890
|
this.pending.set(id, { resolve, reject, cleanup })
|
|
891
|
+
// JSON serialization above can run user-defined `toJSON` hooks. Recheck after installing the
|
|
892
|
+
// listener and pending entry so an abort during that synchronous work cannot enqueue a request
|
|
893
|
+
// after cancellation, and so an already-aborted signal does not retain the listener.
|
|
894
|
+
if (signal?.aborted) {
|
|
895
|
+
abort()
|
|
896
|
+
return
|
|
897
|
+
}
|
|
810
898
|
try {
|
|
811
|
-
this.proc.stdin.write(`${
|
|
899
|
+
this.proc.stdin.write(`${encodedInput}\n`)
|
|
812
900
|
} catch (err) {
|
|
813
901
|
this.pending.delete(id)
|
|
814
902
|
cleanup()
|
|
@@ -831,8 +919,10 @@ export class WarmWorker {
|
|
|
831
919
|
this.stop()
|
|
832
920
|
return
|
|
833
921
|
}
|
|
834
|
-
|
|
835
|
-
|
|
922
|
+
const text = decoder.decode(value, { stream: true })
|
|
923
|
+
this.stdoutBuffer += text
|
|
924
|
+
this.stdoutBufferBytes += new TextEncoder().encode(text).byteLength
|
|
925
|
+
if (this.stdoutBufferBytes > CHILD_OUTPUT_MAX_BYTES) {
|
|
836
926
|
this.stderrBuffer = boundedAppend(
|
|
837
927
|
this.stderrBuffer,
|
|
838
928
|
`warm ${this.label} worker stdout exceeded ${CHILD_OUTPUT_MAX_BYTES} bytes\n`,
|
|
@@ -844,6 +934,7 @@ export class WarmWorker {
|
|
|
844
934
|
while (nl !== -1) {
|
|
845
935
|
const line = this.stdoutBuffer.slice(0, nl).trim()
|
|
846
936
|
this.stdoutBuffer = this.stdoutBuffer.slice(nl + 1)
|
|
937
|
+
this.stdoutBufferBytes = new TextEncoder().encode(this.stdoutBuffer).byteLength
|
|
847
938
|
nl = this.stdoutBuffer.indexOf("\n")
|
|
848
939
|
if (line !== "") this.handleLine(line)
|
|
849
940
|
}
|
|
@@ -861,25 +952,69 @@ export class WarmWorker {
|
|
|
861
952
|
}
|
|
862
953
|
|
|
863
954
|
private handleLine(line: string): void {
|
|
864
|
-
let message:
|
|
955
|
+
let message: unknown
|
|
865
956
|
try {
|
|
866
|
-
message = JSON.parse(line) as
|
|
957
|
+
message = JSON.parse(line) as unknown
|
|
867
958
|
} catch {
|
|
868
959
|
this.stderrBuffer = boundedAppend(
|
|
869
960
|
this.stderrBuffer,
|
|
870
961
|
`invalid warm ${this.label} worker line: ${line}\n`,
|
|
871
962
|
)
|
|
963
|
+
this.stop()
|
|
964
|
+
return
|
|
965
|
+
}
|
|
966
|
+
if (
|
|
967
|
+
message === null ||
|
|
968
|
+
typeof message !== "object" ||
|
|
969
|
+
Array.isArray(message) ||
|
|
970
|
+
typeof (message as { id?: unknown }).id !== "number" ||
|
|
971
|
+
!Number.isSafeInteger((message as { id: number }).id)
|
|
972
|
+
) {
|
|
973
|
+
this.stderrBuffer = boundedAppend(
|
|
974
|
+
this.stderrBuffer,
|
|
975
|
+
`invalid warm ${this.label} worker response envelope\n`,
|
|
976
|
+
)
|
|
977
|
+
this.stop()
|
|
872
978
|
return
|
|
873
979
|
}
|
|
874
|
-
|
|
875
|
-
const pending = this.pending.get(
|
|
980
|
+
const response = message as { id: number; output?: unknown }
|
|
981
|
+
const pending = this.pending.get(response.id)
|
|
876
982
|
if (pending === undefined) return
|
|
877
|
-
|
|
983
|
+
if (!Object.hasOwn(response, "output")) {
|
|
984
|
+
this.pending.delete(response.id)
|
|
985
|
+
pending.cleanup()
|
|
986
|
+
pending.reject(new Error(`warm ${this.label} worker response is malformed`))
|
|
987
|
+
this.stop()
|
|
988
|
+
return
|
|
989
|
+
}
|
|
990
|
+
let output: string
|
|
991
|
+
try {
|
|
992
|
+
output = JSON.stringify(response.output, null, 2)
|
|
993
|
+
} catch {
|
|
994
|
+
this.pending.delete(response.id)
|
|
995
|
+
pending.cleanup()
|
|
996
|
+
pending.reject(new Error(`warm ${this.label} worker response is not serializable`))
|
|
997
|
+
this.stop()
|
|
998
|
+
return
|
|
999
|
+
}
|
|
1000
|
+
if (new TextEncoder().encode(output).byteLength > CHILD_OUTPUT_MAX_BYTES) {
|
|
1001
|
+
this.pending.delete(response.id)
|
|
1002
|
+
pending.cleanup()
|
|
1003
|
+
pending.reject(new Error(`warm ${this.label} worker response exceeded the size limit`))
|
|
1004
|
+
this.stop()
|
|
1005
|
+
return
|
|
1006
|
+
}
|
|
1007
|
+
this.pending.delete(response.id)
|
|
878
1008
|
pending.cleanup()
|
|
879
|
-
pending.resolve(
|
|
1009
|
+
pending.resolve(output)
|
|
880
1010
|
}
|
|
881
1011
|
}
|
|
882
1012
|
|
|
1013
|
+
function cancellationSuffix(signal: AbortSignal | undefined): string {
|
|
1014
|
+
const reason = signal?.reason
|
|
1015
|
+
return typeof reason === "string" && reason.length > 0 ? `: ${reason.slice(0, 512)}` : ""
|
|
1016
|
+
}
|
|
1017
|
+
|
|
883
1018
|
/** A warm handler that reuses a hot {@link WarmWorker} across calls and falls back to a one-shot fresh
|
|
884
1019
|
* subprocess on any worker failure. Shared by `nifra_run` (`child: "mcp-run"`, `label: "run"`) and
|
|
885
1020
|
* `nifra_render` (`child: "mcp-render"`, `label: "render"`) - the only differences are the engine and the
|
|
@@ -892,17 +1027,21 @@ export function createWarmHandler(
|
|
|
892
1027
|
let worker: WarmWorker | undefined
|
|
893
1028
|
return async (input, signal) => {
|
|
894
1029
|
const fingerprint = await warmRunFingerprint(cwd)
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1030
|
+
let current = worker
|
|
1031
|
+
if (current === undefined || current.fingerprint !== fingerprint) {
|
|
1032
|
+
current?.stop()
|
|
1033
|
+
current = new WarmWorker(child, cwd, fingerprint, label)
|
|
1034
|
+
worker = current
|
|
898
1035
|
}
|
|
899
1036
|
try {
|
|
900
|
-
return await
|
|
1037
|
+
return await current.request(input, signal)
|
|
901
1038
|
} catch {
|
|
902
|
-
worker.
|
|
903
|
-
|
|
1039
|
+
// A source change can replace the shared worker while this request is in flight. Only clear
|
|
1040
|
+
// the slot if this request still owns it; an old request must never tear down the replacement.
|
|
1041
|
+
current.stop()
|
|
1042
|
+
if (worker === current) worker = undefined
|
|
904
1043
|
if (signal?.aborted) {
|
|
905
|
-
const reason =
|
|
1044
|
+
const reason = cancellationSuffix(signal)
|
|
906
1045
|
return `${label} cancelled${reason}.`
|
|
907
1046
|
}
|
|
908
1047
|
return spawnChild(child, cwd, input, label, signal)
|
|
@@ -969,7 +1108,7 @@ export interface CachedAppLoaderOptions {
|
|
|
969
1108
|
readonly fingerprint?: (cwd: string) => Promise<string>
|
|
970
1109
|
}
|
|
971
1110
|
|
|
972
|
-
const APP_FINGERPRINT_FILES = [
|
|
1111
|
+
const APP_FINGERPRINT_FILES = [CONFIG_FILE, FRAMEWORK_FILE, BACKEND_APP_FILE] as const
|
|
973
1112
|
|
|
974
1113
|
function cacheToken(input: string): string {
|
|
975
1114
|
let hash = 2166136261
|
|
@@ -992,7 +1131,8 @@ async function fileFingerprint(path: string): Promise<string> {
|
|
|
992
1131
|
}
|
|
993
1132
|
}
|
|
994
1133
|
|
|
995
|
-
|
|
1134
|
+
/** `mtime:size` of the app's config, framework and backend entry files: what notices an edit. */
|
|
1135
|
+
export async function appFingerprint(cwd: string): Promise<string> {
|
|
996
1136
|
return (
|
|
997
1137
|
await Promise.all(
|
|
998
1138
|
APP_FINGERPRINT_FILES.map(
|