@nifrajs/cli 4.0.0 → 5.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 +3 -0
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +7 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +13 -3
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +20 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +75 -22
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1475 -310
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +192 -65
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -5
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +18 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +49 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +21 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +138 -38
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +28 -3
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +2 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +17 -11
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +16 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +396 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +2 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -0
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +5 -3
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +55 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +93 -15
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +31 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +37 -7
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +2 -2
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-exec.d.ts +12 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +50 -13
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +17 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +40 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -2
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +7 -0
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +958 -71
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +94 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +864 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts.map +1 -1
- package/dist/recipes/4.0.0.js +2 -0
- package/dist/recipes/4.0.0.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +38 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +10 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +10 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +7 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +319 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +158 -43
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +24 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +221 -3
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +12 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +281 -68
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +192 -40
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +163 -7
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +42 -18
- package/dist/sync-manifest.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +12 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +69 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +420 -77
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +11 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +42 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +54 -6
- package/docs/llms-full.txt +1539 -138
- package/docs/types.json +1456 -206
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +15 -2
- package/src/capabilities-tool.ts +15 -3
- package/src/check-diagnostics.ts +73 -9
- package/src/check-scan.ts +1773 -381
- package/src/check.ts +237 -76
- package/src/cli.ts +141 -5
- package/src/client-module.ts +61 -0
- package/src/command-catalog.ts +181 -47
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +34 -3
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +17 -11
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +424 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +25 -0
- package/src/internal/typescript-import.ts +10 -3
- package/src/introspect.ts +65 -27
- package/src/levels-tool.ts +117 -20
- package/src/load.ts +78 -14
- package/src/mcp-context.ts +7 -7
- package/src/mcp-exec.ts +60 -17
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +41 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +8 -3
- package/src/migrate-layout.ts +1109 -70
- package/src/output-schemas.ts +1084 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/recipes/4.0.0.ts +2 -0
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +39 -10
- package/src/rules/codes.ts +10 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +351 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +184 -51
- package/src/rules/pages.ts +238 -3
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +352 -71
- package/src/rules/zones.ts +205 -38
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +134 -7
- package/src/sync-manifest.ts +48 -18
- package/src/test-tool.ts +18 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +487 -71
- package/src/verification-plan.ts +24 -0
- package/src/version-drift.ts +67 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/mcp-exec.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { type Dirent, realpathSync } from "node:fs"
|
|
10
10
|
import { readdir, stat } from "node:fs/promises"
|
|
11
|
-
import { relative, resolve } from "node:path"
|
|
11
|
+
import { isAbsolute, relative, resolve } from "node:path"
|
|
12
12
|
import { fileURLToPath } from "node:url"
|
|
13
13
|
import { BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
14
14
|
import {
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { collectContractProof } from "./contract-proof.ts"
|
|
25
25
|
import { loadDocsCorpus } from "./docs-search.ts"
|
|
26
26
|
import { loadExamplesCorpus } from "./examples.ts"
|
|
27
|
-
import type { LoadAppOptions, LoadedApp } from "./load.ts"
|
|
27
|
+
import type { AppLoader, IntrospectedApp, LoadAppOptions, LoadedApp } from "./load.ts"
|
|
28
28
|
import {
|
|
29
29
|
type CommandMcpToolOptions,
|
|
30
30
|
dirError,
|
|
@@ -36,7 +36,9 @@ import {
|
|
|
36
36
|
CHILD_INPUT_MAX_BYTES,
|
|
37
37
|
CHILD_OUTPUT_MAX_BYTES,
|
|
38
38
|
CHILD_TIMEOUT_MS,
|
|
39
|
+
OWN_PROCESS_GROUP,
|
|
39
40
|
readBoundedStream,
|
|
41
|
+
signalProcessGroup,
|
|
40
42
|
timeoutMessage,
|
|
41
43
|
validateLocalPort,
|
|
42
44
|
} from "./mcp-io.ts"
|
|
@@ -46,20 +48,29 @@ import { loadTypesCorpus } from "./types-search.ts"
|
|
|
46
48
|
|
|
47
49
|
const CODEFRAME_SOURCE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|mdx)$/i
|
|
48
50
|
|
|
51
|
+
type GatePaths = Pick<typeof import("node:path"), "isAbsolute" | "relative" | "resolve">
|
|
52
|
+
|
|
49
53
|
/** 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
|
-
|
|
54
|
+
* or anything under a dot directory (`.env`, `.git/`, `.nifra/`), and never a file outside the root -
|
|
55
|
+
* which on Windows includes one on another drive, where `relative` answers with an absolute path. */
|
|
56
|
+
export function explainSourceGate(
|
|
57
|
+
root: string,
|
|
58
|
+
paths: GatePaths = { isAbsolute, relative, resolve },
|
|
59
|
+
): (file: string) => boolean {
|
|
52
60
|
// The codeframe reads a native realpath, which expands a Windows short name (`RUNNER~1`); the root
|
|
53
61
|
// has to be spelled the same way or every file looks like it sits outside it.
|
|
54
|
-
let base = resolve(root)
|
|
62
|
+
let base = paths.resolve(root)
|
|
55
63
|
try {
|
|
56
64
|
base = realpathSync.native(base)
|
|
57
65
|
} catch {}
|
|
58
|
-
return (file) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
.
|
|
66
|
+
return (file) => {
|
|
67
|
+
const inside = paths.relative(base, file)
|
|
68
|
+
return (
|
|
69
|
+
CODEFRAME_SOURCE.test(file) &&
|
|
70
|
+
!paths.isAbsolute(inside) &&
|
|
71
|
+
!inside.split(/[\\/]/).some((segment) => segment.startsWith("."))
|
|
72
|
+
)
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
/** Path to a sibling child entry (`mcp-run` / `mcp-render` / `mcp-ws`), resolved next to this module (`.ts` in
|
|
@@ -74,7 +85,16 @@ export function toMcpTool(
|
|
|
74
85
|
const entry = "run" in source ? undefined : source
|
|
75
86
|
const { cwd } = options
|
|
76
87
|
const loadAppCached = options.loadAppCached ?? createCachedAppLoader(cwd)
|
|
77
|
-
const pathFields = [
|
|
88
|
+
const pathFields = [
|
|
89
|
+
"config",
|
|
90
|
+
"out",
|
|
91
|
+
"lockfile",
|
|
92
|
+
"before",
|
|
93
|
+
"after",
|
|
94
|
+
"baseline",
|
|
95
|
+
"file",
|
|
96
|
+
"since",
|
|
97
|
+
] as const
|
|
78
98
|
const catalogEntry = entry ?? toCommandCatalogEntry(spec)
|
|
79
99
|
return {
|
|
80
100
|
name: commandMcpName(catalogEntry.name),
|
|
@@ -143,7 +163,7 @@ export function toMcpTool(
|
|
|
143
163
|
|
|
144
164
|
export function catalogProjectTools(
|
|
145
165
|
cwd: string,
|
|
146
|
-
loadAppCached:
|
|
166
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
147
167
|
): McpTool[] {
|
|
148
168
|
return commandCatalog
|
|
149
169
|
.filter((entry) => entry.transports.includes("mcp"))
|
|
@@ -156,11 +176,11 @@ export function catalogProjectTools(
|
|
|
156
176
|
|
|
157
177
|
export function projectTools(
|
|
158
178
|
cwd: string,
|
|
159
|
-
loadAppCached:
|
|
179
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
160
180
|
): McpTool[] {
|
|
161
181
|
const warmRun = createWarmHandler("mcp-run", cwd, "run")
|
|
162
182
|
const warmRender = createWarmHandler("mcp-render", cwd, "render")
|
|
163
|
-
const catalogTools = catalogProjectTools(cwd,
|
|
183
|
+
const catalogTools = catalogProjectTools(cwd, loadAppCached)
|
|
164
184
|
const legacyTools: McpTool[] = [
|
|
165
185
|
{
|
|
166
186
|
name: "nifra_verify",
|
|
@@ -692,12 +712,13 @@ export async function spawnChild(
|
|
|
692
712
|
stdin: "pipe",
|
|
693
713
|
stdout: "pipe",
|
|
694
714
|
stderr: "pipe",
|
|
715
|
+
detached: OWN_PROCESS_GROUP,
|
|
695
716
|
})
|
|
696
717
|
let killed = false
|
|
697
718
|
const kill = (): void => {
|
|
698
719
|
if (killed) return
|
|
699
720
|
killed = true
|
|
700
|
-
proc
|
|
721
|
+
signalProcessGroup(proc, "SIGTERM")
|
|
701
722
|
}
|
|
702
723
|
const abort = (): void => kill()
|
|
703
724
|
signal?.addEventListener("abort", abort, { once: true })
|
|
@@ -791,6 +812,16 @@ type PipeSubprocess = ReturnType<typeof Bun.spawn> & {
|
|
|
791
812
|
readonly stderr: ReadableStream<Uint8Array>
|
|
792
813
|
}
|
|
793
814
|
|
|
815
|
+
const liveWarmWorkers = new Set<WarmWorker>()
|
|
816
|
+
let warmWorkersClosed = false
|
|
817
|
+
|
|
818
|
+
/** Stop every warm worker and start no more, once the MCP client has closed stdin. A live worker
|
|
819
|
+
* holds the process open, so the server would otherwise never exit. */
|
|
820
|
+
export function closeWarmWorkers(): void {
|
|
821
|
+
warmWorkersClosed = true
|
|
822
|
+
for (const worker of liveWarmWorkers) worker.stop()
|
|
823
|
+
}
|
|
824
|
+
|
|
794
825
|
/** A persistent `mcp-run`/`mcp-render` `--worker` subprocess: the backend/web app is loaded ONCE and
|
|
795
826
|
* reused across newline-delimited `{ id, input }` requests, replying `{ id, output }`. The same machinery
|
|
796
827
|
* powers both `nifra_run warm` and `nifra_render warm` - `child` selects which engine, `label` shapes the
|
|
@@ -827,10 +858,12 @@ export class WarmWorker {
|
|
|
827
858
|
stdout: "pipe",
|
|
828
859
|
stderr: "pipe",
|
|
829
860
|
}) as PipeSubprocess
|
|
861
|
+
liveWarmWorkers.add(this)
|
|
830
862
|
void this.readStdout()
|
|
831
863
|
void this.readStderr()
|
|
832
864
|
void this.proc.exited.then((code) => {
|
|
833
865
|
this.closed = true
|
|
866
|
+
liveWarmWorkers.delete(this)
|
|
834
867
|
const err = new Error(
|
|
835
868
|
`warm ${this.label} worker exited (${code})${this.stderrBuffer ? `:\n${this.stderrBuffer}` : ""}`,
|
|
836
869
|
)
|
|
@@ -1025,8 +1058,10 @@ export function createWarmHandler(
|
|
|
1025
1058
|
label: string,
|
|
1026
1059
|
): (input: unknown, signal?: AbortSignal) => Promise<string> {
|
|
1027
1060
|
let worker: WarmWorker | undefined
|
|
1061
|
+
const shuttingDown = `${label} cancelled: the MCP server is shutting down.`
|
|
1028
1062
|
return async (input, signal) => {
|
|
1029
1063
|
const fingerprint = await warmRunFingerprint(cwd)
|
|
1064
|
+
if (warmWorkersClosed) return shuttingDown
|
|
1030
1065
|
let current = worker
|
|
1031
1066
|
if (current === undefined || current.fingerprint !== fingerprint) {
|
|
1032
1067
|
current?.stop()
|
|
@@ -1044,6 +1079,7 @@ export function createWarmHandler(
|
|
|
1044
1079
|
const reason = cancellationSuffix(signal)
|
|
1045
1080
|
return `${label} cancelled${reason}.`
|
|
1046
1081
|
}
|
|
1082
|
+
if (warmWorkersClosed) return shuttingDown
|
|
1047
1083
|
return spawnChild(child, cwd, input, label, signal)
|
|
1048
1084
|
}
|
|
1049
1085
|
}
|
|
@@ -1146,7 +1182,7 @@ export async function appFingerprint(cwd: string): Promise<string> {
|
|
|
1146
1182
|
export function createCachedAppLoader(
|
|
1147
1183
|
cwd: string,
|
|
1148
1184
|
options: CachedAppLoaderOptions = {},
|
|
1149
|
-
):
|
|
1185
|
+
): AppLoader {
|
|
1150
1186
|
const loadAppCached =
|
|
1151
1187
|
options.loadApp ??
|
|
1152
1188
|
(async (root: string, outDirName?: string, loadOptions?: LoadAppOptions) => {
|
|
@@ -1162,7 +1198,7 @@ export function createCachedAppLoader(
|
|
|
1162
1198
|
}
|
|
1163
1199
|
| undefined
|
|
1164
1200
|
|
|
1165
|
-
|
|
1201
|
+
const whole = async (outDirName = "dist"): Promise<LoadedApp> => {
|
|
1166
1202
|
const nextFingerprint = await fingerprint(cwd)
|
|
1167
1203
|
if (
|
|
1168
1204
|
cached !== undefined &&
|
|
@@ -1177,4 +1213,11 @@ export function createCachedAppLoader(
|
|
|
1177
1213
|
cached = { outDirName, fingerprint: nextFingerprint, app }
|
|
1178
1214
|
return app
|
|
1179
1215
|
}
|
|
1216
|
+
const backend = async (): Promise<IntrospectedApp> => {
|
|
1217
|
+
const { hasAppConfig, loadBackendApp } = await import("./load.ts")
|
|
1218
|
+
if (hasAppConfig(cwd)) return whole()
|
|
1219
|
+
// The fingerprint names backend/app.ts, so an edit loads a fresh module and no edit reuses it.
|
|
1220
|
+
return loadBackendApp(cwd, { importQuery: cacheToken(await fingerprint(cwd)) })
|
|
1221
|
+
}
|
|
1222
|
+
return Object.assign(whole, { backend })
|
|
1180
1223
|
}
|
package/src/mcp-http.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
|
|
|
29
29
|
export type { TypeEntry } from "./types-search.ts"
|
|
30
30
|
|
|
31
31
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
32
|
-
const VERSION = "
|
|
32
|
+
const VERSION = "5.0.0"
|
|
33
33
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION }
|
|
34
34
|
// Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
|
|
35
35
|
const docsHealth = (tools: McpTool[]): string =>
|
package/src/mcp-io.ts
CHANGED
|
@@ -16,6 +16,47 @@ export const CHILD_INPUT_MAX_BYTES = 4 * 1024 * 1024
|
|
|
16
16
|
export const LOCAL_TOOL_FETCH_TIMEOUT_MS = 2_000
|
|
17
17
|
export const LOCAL_TOOL_MAX_RESPONSE_BYTES = 1_048_576
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* On POSIX a bounded child is spawned `detached`, leading its own process group, so stopping it
|
|
21
|
+
* reaches every process it started. Otherwise a grandchild still holding the pipes keeps the read,
|
|
22
|
+
* and the tool call, open past its timeout.
|
|
23
|
+
*/
|
|
24
|
+
export const OWN_PROCESS_GROUP = process.platform !== "win32"
|
|
25
|
+
|
|
26
|
+
/** Signal a child spawned with `detached: OWN_PROCESS_GROUP`: its whole group on POSIX. */
|
|
27
|
+
export function signalProcessGroup(
|
|
28
|
+
proc: { readonly pid: number; kill(signal?: NodeJS.Signals): void },
|
|
29
|
+
signal: NodeJS.Signals,
|
|
30
|
+
): void {
|
|
31
|
+
try {
|
|
32
|
+
if (OWN_PROCESS_GROUP) process.kill(-proc.pid, signal)
|
|
33
|
+
else proc.kill(signal)
|
|
34
|
+
} catch {
|
|
35
|
+
// The group has already exited.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A child in its own group no longer gets the Ctrl+C the terminal sends its parent: on SIGINT,
|
|
43
|
+
* SIGTERM or SIGHUP run `stop`, then let this process take the signal as it would have. Returns the
|
|
44
|
+
* function that removes the listeners.
|
|
45
|
+
*/
|
|
46
|
+
export function forwardTerminalSignals(stop: () => void): () => void {
|
|
47
|
+
if (!OWN_PROCESS_GROUP) return () => {}
|
|
48
|
+
const remove = (): void => {
|
|
49
|
+
for (const name of FORWARDED_SIGNALS) process.off(name, forward)
|
|
50
|
+
}
|
|
51
|
+
const forward = (signal: NodeJS.Signals): void => {
|
|
52
|
+
stop()
|
|
53
|
+
remove()
|
|
54
|
+
process.kill(process.pid, signal)
|
|
55
|
+
}
|
|
56
|
+
for (const name of FORWARDED_SIGNALS) process.on(name, forward)
|
|
57
|
+
return remove
|
|
58
|
+
}
|
|
59
|
+
|
|
19
60
|
/** Accept only a concrete TCP port. Port 0 (bind-any-free-port) is not a valid read target. */
|
|
20
61
|
export function validateLocalPort(port: unknown): number | undefined {
|
|
21
62
|
return typeof port === "number" && Number.isInteger(port) && port >= 1 && port <= 65_535
|
package/src/mcp-isolate.ts
CHANGED
|
@@ -13,7 +13,13 @@ import { fileURLToPath } from "node:url"
|
|
|
13
13
|
import { CONFIG_FILE } from "./app-files.ts"
|
|
14
14
|
import type { NifraMonorepoConfig } from "./load.ts"
|
|
15
15
|
import { appFingerprint } from "./mcp-exec.ts"
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
CHILD_TIMEOUT_MS,
|
|
18
|
+
OWN_PROCESS_GROUP,
|
|
19
|
+
readBoundedLines,
|
|
20
|
+
readBoundedStream,
|
|
21
|
+
signalProcessGroup,
|
|
22
|
+
} from "./mcp-io.ts"
|
|
17
23
|
import type {
|
|
18
24
|
AppSurface,
|
|
19
25
|
ProjectChildAnswer,
|
|
@@ -121,6 +127,7 @@ export async function askProjectChild(
|
|
|
121
127
|
stdin: "pipe",
|
|
122
128
|
stdout: "pipe",
|
|
123
129
|
stderr: "pipe",
|
|
130
|
+
detached: OWN_PROCESS_GROUP,
|
|
124
131
|
})
|
|
125
132
|
} catch (error) {
|
|
126
133
|
return failed(
|
|
@@ -131,7 +138,7 @@ export async function askProjectChild(
|
|
|
131
138
|
const stop = (reason: "timeout" | "cancel"): void => {
|
|
132
139
|
if (stopped !== undefined) return
|
|
133
140
|
stopped = reason
|
|
134
|
-
proc
|
|
141
|
+
signalProcessGroup(proc, "SIGKILL")
|
|
135
142
|
}
|
|
136
143
|
const timer = timeoutMs === undefined ? undefined : setTimeout(() => stop("timeout"), timeoutMs)
|
|
137
144
|
const onAbort = (): void => stop("cancel")
|
package/src/mcp-project-child.ts
CHANGED
package/src/mcp-root.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Everything here is pure state + fs probes; the I/O wiring lives in `./mcp.ts`.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { readFile } from "node:fs/promises"
|
|
20
|
+
import { readdir, readFile, stat } from "node:fs/promises"
|
|
21
21
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
22
22
|
import { fileURLToPath } from "node:url"
|
|
23
23
|
|
|
@@ -46,6 +46,8 @@ const MAX_ROOT_PATH_LENGTH = 4_096
|
|
|
46
46
|
/** Workspace members probed per workspace root. A monorepo larger than this is pointed at explicitly
|
|
47
47
|
* (`nifra mcp <dir>`) rather than scanned on every server start. */
|
|
48
48
|
const MAX_WORKSPACE_MEMBERS = 256
|
|
49
|
+
/** Directories one workspace probe may read while matching member patterns. */
|
|
50
|
+
const MAX_WORKSPACE_DIRECTORIES = 4_096
|
|
49
51
|
|
|
50
52
|
/** A directory is a nifra project when its `package.json` depends on any `@nifrajs/*` package, or it
|
|
51
53
|
* is a `nifra.config.ts` monorepo root. Backend-only projects (no web config, no `routes/`) count -
|
|
@@ -102,6 +104,57 @@ async function workspacePatterns(dir: string): Promise<readonly string[] | null>
|
|
|
102
104
|
)
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The directories under `dir` that `pattern` matches and that hold a `package.json`, in name order. A
|
|
109
|
+
* walk rather than a glob scan: a `**` pattern would otherwise descend every `node_modules` below it,
|
|
110
|
+
* where a member never lives. It starts at the pattern's literal prefix, stops at the pattern's depth
|
|
111
|
+
* unless it has a `**`, skips dot directories, and draws on one directory budget for the whole probe.
|
|
112
|
+
*/
|
|
113
|
+
async function workspaceMatches(
|
|
114
|
+
dir: string,
|
|
115
|
+
pattern: string,
|
|
116
|
+
budget: { directories: number },
|
|
117
|
+
): Promise<string[]> {
|
|
118
|
+
const segments = pattern
|
|
119
|
+
.replace(/^\.\//, "")
|
|
120
|
+
.replace(/\/+$/, "")
|
|
121
|
+
.split("/")
|
|
122
|
+
.filter((segment) => segment !== "" && segment !== ".")
|
|
123
|
+
const firstGlob = segments.findIndex((segment) => /[*?[{]/.test(segment))
|
|
124
|
+
const prefix = firstGlob === -1 ? segments : segments.slice(0, firstGlob)
|
|
125
|
+
const depth = segments.includes("**") ? Number.POSITIVE_INFINITY : segments.length
|
|
126
|
+
const glob = new Bun.Glob(segments.join("/"))
|
|
127
|
+
const found: string[] = []
|
|
128
|
+
const queue: { readonly path: string; readonly depth: number }[] = [
|
|
129
|
+
{ path: prefix.join("/"), depth: prefix.length },
|
|
130
|
+
]
|
|
131
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
132
|
+
if (budget.directories <= 0) break
|
|
133
|
+
budget.directories -= 1
|
|
134
|
+
const absolute = join(dir, next.path)
|
|
135
|
+
if (next.path !== "" && glob.match(next.path) && (await isFile(join(absolute, "package.json"))))
|
|
136
|
+
found.push(next.path)
|
|
137
|
+
if (next.depth >= depth) continue
|
|
138
|
+
const entries = await readdir(absolute, { withFileTypes: true }).catch(() => [])
|
|
139
|
+
const names = entries
|
|
140
|
+
.filter(
|
|
141
|
+
(entry) =>
|
|
142
|
+
entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith("."),
|
|
143
|
+
)
|
|
144
|
+
.map((entry) => entry.name)
|
|
145
|
+
.sort()
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
queue.push({ path: next.path === "" ? name : `${next.path}/${name}`, depth: next.depth + 1 })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return found
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isFile = (path: string): Promise<boolean> =>
|
|
154
|
+
stat(path)
|
|
155
|
+
.then((info) => info.isFile())
|
|
156
|
+
.catch(() => false)
|
|
157
|
+
|
|
105
158
|
/**
|
|
106
159
|
* The nifra projects among `dir`'s package-manager workspace members, in declaration order. Empty when
|
|
107
160
|
* `dir` declares no `workspaces`. A member is a directory a pattern matches that holds a `package.json`;
|
|
@@ -111,15 +164,12 @@ export async function workspaceProjects(dir: string): Promise<string[]> {
|
|
|
111
164
|
const patterns = await workspacePatterns(dir)
|
|
112
165
|
if (patterns === null) return []
|
|
113
166
|
const members = new Set<string>()
|
|
114
|
-
|
|
167
|
+
const budget = { directories: MAX_WORKSPACE_DIRECTORIES }
|
|
115
168
|
for (const pattern of patterns) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (++probed > MAX_WORKSPACE_MEMBERS) break
|
|
120
|
-
members.add(dirname(join(dir, match)))
|
|
169
|
+
for (const member of await workspaceMatches(dir, pattern, budget)) {
|
|
170
|
+
if (members.size === MAX_WORKSPACE_MEMBERS) break
|
|
171
|
+
members.add(join(dir, member))
|
|
121
172
|
}
|
|
122
|
-
if (probed > MAX_WORKSPACE_MEMBERS) break
|
|
123
173
|
}
|
|
124
174
|
const projects: string[] = []
|
|
125
175
|
for (const member of members) if (await isNifraProjectDir(member)) projects.push(member)
|
package/src/mcp-ws.ts
CHANGED
|
@@ -21,7 +21,7 @@ interface RunningServerLike {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface ListenableApp {
|
|
24
|
-
listen(port: number): RunningServerLike
|
|
24
|
+
listen(port: number, options: { readonly hostname: string }): RunningServerLike
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
interface NormalizedInput {
|
|
@@ -206,7 +206,9 @@ export async function runWebSocket(
|
|
|
206
206
|
let running: RunningServerLike | undefined
|
|
207
207
|
let socket: WebSocket | undefined
|
|
208
208
|
try {
|
|
209
|
-
|
|
209
|
+
// Loopback only: the project's backend, with its environment loaded, must not be reachable
|
|
210
|
+
// from the network for the length of a verification.
|
|
211
|
+
running = app.listen(0, { hostname: "127.0.0.1" })
|
|
210
212
|
const url = `ws://127.0.0.1:${running.port}${normalized.path}`
|
|
211
213
|
socket = new WebSocket(url)
|
|
212
214
|
socket.binaryType = "arraybuffer"
|
package/src/mcp.ts
CHANGED
|
@@ -52,7 +52,7 @@ import { stat } from "node:fs/promises"
|
|
|
52
52
|
import { resolve } from "node:path"
|
|
53
53
|
import { loadDocsCorpus } from "./docs-search.ts"
|
|
54
54
|
import { loadExamplesCorpus } from "./examples.ts"
|
|
55
|
-
import type {
|
|
55
|
+
import type { AppLoader } from "./load.ts"
|
|
56
56
|
import { loadMonorepoApps } from "./load.ts"
|
|
57
57
|
import { delegateToProjectCli, refuseVersionSensitive } from "./mcp-delegate.ts"
|
|
58
58
|
import { docsTools } from "./mcp-docs-tools.ts"
|
|
@@ -108,7 +108,7 @@ export {
|
|
|
108
108
|
} from "./mcp-io.ts"
|
|
109
109
|
export { clientSupportsRoots, guardTools } from "./mcp-root.ts"
|
|
110
110
|
|
|
111
|
-
import { projectTools } from "./mcp-exec.ts"
|
|
111
|
+
import { closeWarmWorkers, projectTools } from "./mcp-exec.ts"
|
|
112
112
|
import {
|
|
113
113
|
createAppSurface,
|
|
114
114
|
detectMonorepoIsolated,
|
|
@@ -147,10 +147,11 @@ export {
|
|
|
147
147
|
* The loader the server builds its tool and resource lists with. It never loads: every handler that
|
|
148
148
|
* reaches the app runs in a project subprocess (`./mcp-isolate.ts`), so one that tried here fails.
|
|
149
149
|
*/
|
|
150
|
-
const
|
|
150
|
+
const refuseApp = (): Promise<never> =>
|
|
151
151
|
Promise.reject(
|
|
152
152
|
new Error("[nifra] the MCP server does not load the app; project code runs in a subprocess"),
|
|
153
153
|
)
|
|
154
|
+
const noAppInServer: AppLoader = Object.assign(refuseApp, { backend: refuseApp })
|
|
154
155
|
|
|
155
156
|
const isolatedToolsByDir = new Map<string, McpTool[]>()
|
|
156
157
|
|
|
@@ -308,6 +309,9 @@ export async function runMcpServer(
|
|
|
308
309
|
if (message.method === "initialize") {
|
|
309
310
|
rootsSupported = clientSupportsRoots(message.params)
|
|
310
311
|
}
|
|
312
|
+
// An install during the session changes the nifra the project builds with, and this process keeps
|
|
313
|
+
// the code it started with: re-read the installed version per call so no answer is for a stale one.
|
|
314
|
+
if (message.method === "tools/call") drift = await detectToolingDrift(rootState.root, version)
|
|
311
315
|
|
|
312
316
|
let activeTools: McpTool[]
|
|
313
317
|
if (ctx.apps) {
|
|
@@ -418,6 +422,7 @@ export async function runMcpServer(
|
|
|
418
422
|
send(rpcError(null, -32600, "MCP message exceeded the size limit"))
|
|
419
423
|
}
|
|
420
424
|
}
|
|
425
|
+
closeWarmWorkers()
|
|
421
426
|
}
|
|
422
427
|
|
|
423
428
|
function responseIdOf(message: JsonRpcResponse | JsonRpcNotification): string | number | null {
|