@nifrajs/cli 4.0.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/mcp-io.ts
CHANGED
|
@@ -16,6 +16,80 @@ 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
|
+
/** How long a stopped child gets to exit after SIGTERM before it is sent SIGKILL. */
|
|
40
|
+
export const KILL_GRACE_MS = 2_000
|
|
41
|
+
|
|
42
|
+
/** Stopping a child: `stop` sends SIGTERM, `dispose` drops the SIGKILL still pending once it is done. */
|
|
43
|
+
export interface EscalatingStop {
|
|
44
|
+
readonly stop: () => void
|
|
45
|
+
readonly dispose: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Stop a child with SIGTERM, then SIGKILL once `graceMs` passes. Project code can catch SIGTERM
|
|
50
|
+
* (a graceful-shutdown hook waiting on a hung request), and a child that never exits keeps its
|
|
51
|
+
* caller waiting and outlives it. `send` delivers one signal to the child or its group.
|
|
52
|
+
*/
|
|
53
|
+
export function escalatingStop(
|
|
54
|
+
send: (signal: NodeJS.Signals) => void,
|
|
55
|
+
graceMs: number = KILL_GRACE_MS,
|
|
56
|
+
): EscalatingStop {
|
|
57
|
+
let escalation: ReturnType<typeof setTimeout> | undefined
|
|
58
|
+
let disposed = false
|
|
59
|
+
return {
|
|
60
|
+
stop: () => {
|
|
61
|
+
if (disposed) return
|
|
62
|
+
send("SIGTERM")
|
|
63
|
+
escalation ??= setTimeout(() => send("SIGKILL"), graceMs)
|
|
64
|
+
},
|
|
65
|
+
dispose: () => {
|
|
66
|
+
disposed = true
|
|
67
|
+
clearTimeout(escalation)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A child in its own group no longer gets the Ctrl+C the terminal sends its parent: on SIGINT,
|
|
76
|
+
* SIGTERM or SIGHUP run `stop`, then let this process take the signal as it would have. Returns the
|
|
77
|
+
* function that removes the listeners.
|
|
78
|
+
*/
|
|
79
|
+
export function forwardTerminalSignals(stop: () => void): () => void {
|
|
80
|
+
if (!OWN_PROCESS_GROUP) return () => {}
|
|
81
|
+
const remove = (): void => {
|
|
82
|
+
for (const name of FORWARDED_SIGNALS) process.off(name, forward)
|
|
83
|
+
}
|
|
84
|
+
const forward = (signal: NodeJS.Signals): void => {
|
|
85
|
+
stop()
|
|
86
|
+
remove()
|
|
87
|
+
process.kill(process.pid, signal)
|
|
88
|
+
}
|
|
89
|
+
for (const name of FORWARDED_SIGNALS) process.on(name, forward)
|
|
90
|
+
return remove
|
|
91
|
+
}
|
|
92
|
+
|
|
19
93
|
/** Accept only a concrete TCP port. Port 0 (bind-any-free-port) is not a valid read target. */
|
|
20
94
|
export function validateLocalPort(port: unknown): number | undefined {
|
|
21
95
|
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-path.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs"
|
|
2
|
-
import {
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path"
|
|
3
|
+
import { writeTargetOf } from "./project-path.ts"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Resolve an MCP path without allowing lexical or symlink escapes from the selected project.
|
|
6
7
|
*
|
|
7
8
|
* MCP callers are not equivalent to a local CLI user: the selected checkout is their filesystem
|
|
8
9
|
* capability boundary. The nearest-existing-ancestor walk also supports output files that do not
|
|
9
|
-
* exist yet while still canonicalizing every existing path component
|
|
10
|
+
* exist yet while still canonicalizing every existing path component, and a dangling symlink is
|
|
11
|
+
* judged by where it points.
|
|
10
12
|
*/
|
|
11
13
|
export function resolveMcpProjectPath(root: string, value: string): string | null {
|
|
12
14
|
if (value.length === 0 || isAbsolute(value)) return null
|
|
@@ -23,26 +25,10 @@ export function resolveMcpProjectPath(root: string, value: string): string | nul
|
|
|
23
25
|
return null
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const canonicalProbe = realpathSync(probe)
|
|
31
|
-
const canonicalRelative = relative(canonicalRoot, canonicalProbe)
|
|
32
|
-
if (canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) return null
|
|
33
|
-
|
|
34
|
-
const candidate = resolve(canonicalProbe, ...missing.reverse())
|
|
35
|
-
const candidateRelative = relative(canonicalRoot, candidate)
|
|
36
|
-
return candidateRelative.startsWith("..") || isAbsolute(candidateRelative) ? null : candidate
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (!(error && typeof error === "object" && (error as { code?: string }).code === "ENOENT"))
|
|
39
|
-
return null
|
|
40
|
-
const parent = resolve(probe, "..")
|
|
41
|
-
if (parent === probe) return null
|
|
42
|
-
missing.push(basename(probe))
|
|
43
|
-
probe = parent
|
|
44
|
-
}
|
|
45
|
-
}
|
|
28
|
+
const candidate = writeTargetOf(target)
|
|
29
|
+
if (candidate === undefined) return null
|
|
30
|
+
const candidateRelative = relative(canonicalRoot, candidate)
|
|
31
|
+
return candidateRelative.startsWith("..") || isAbsolute(candidateRelative) ? null : candidate
|
|
46
32
|
}
|
|
47
33
|
|
|
48
34
|
export function mcpProjectPathError(label: string, value: string): string {
|
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
|
|
|
@@ -180,6 +181,13 @@ function appFeatures(cwd: string, surface: IsolatedSurface): McpServerFeatures {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
const ROOTS_REQUEST_ID = "nifra:roots/list"
|
|
184
|
+
/** Messages whose answer reads nothing the app declares: the handshake is never held up by its import. */
|
|
185
|
+
const ANSWERED_WITHOUT_THE_APP: ReadonlySet<string> = new Set([
|
|
186
|
+
"initialize",
|
|
187
|
+
"ping",
|
|
188
|
+
"notifications/initialized",
|
|
189
|
+
"notifications/roots/list_changed",
|
|
190
|
+
])
|
|
183
191
|
const MAX_STDIO_MESSAGE_BYTES = 8 * 1024 * 1024
|
|
184
192
|
const STDIO_ENCODER = new TextEncoder()
|
|
185
193
|
|
|
@@ -206,8 +214,12 @@ async function createProjectContext(root: string): Promise<ProjectContext> {
|
|
|
206
214
|
)
|
|
207
215
|
const allResources: McpResource[] = []
|
|
208
216
|
const allPrompts: McpPrompt[] = []
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
// Each app loads in its own subprocess, so they load side by side.
|
|
218
|
+
const loaded = await Promise.all(
|
|
219
|
+
apps.map(async (app) => ({ app, surface: await app.surface() })),
|
|
220
|
+
)
|
|
221
|
+
for (const { app, surface } of loaded) {
|
|
222
|
+
const ns = namespaceForApp(app.name, [], appFeatures(app.cwd, surface))
|
|
211
223
|
allResources.push(...(ns.features.resources ?? []))
|
|
212
224
|
allPrompts.push(...(ns.features.prompts ?? []))
|
|
213
225
|
}
|
|
@@ -250,7 +262,9 @@ export async function runMcpServer(
|
|
|
250
262
|
return
|
|
251
263
|
}
|
|
252
264
|
}
|
|
253
|
-
|
|
265
|
+
// Loaded in the background: the app's import can take seconds, and the handshake needs none of it.
|
|
266
|
+
let ctx = createProjectContext(rootState.root)
|
|
267
|
+
ctx.catch(() => {})
|
|
254
268
|
let drift: ToolingDrift | undefined = await detectToolingDrift(rootState.root, version)
|
|
255
269
|
const serverInfo = { name: "nifra", version }
|
|
256
270
|
const state = createMcpProtocolState()
|
|
@@ -285,7 +299,8 @@ export async function runMcpServer(
|
|
|
285
299
|
if (result === undefined) return
|
|
286
300
|
const next = await applyClientRoots(rootState, pathsFromRootsResult(result))
|
|
287
301
|
if (next.root !== rootState.root) {
|
|
288
|
-
ctx =
|
|
302
|
+
ctx = createProjectContext(next.root)
|
|
303
|
+
ctx.catch(() => {})
|
|
289
304
|
drift = await detectToolingDrift(next.root, version)
|
|
290
305
|
}
|
|
291
306
|
rootState = next
|
|
@@ -308,16 +323,42 @@ export async function runMcpServer(
|
|
|
308
323
|
if (message.method === "initialize") {
|
|
309
324
|
rootsSupported = clientSupportsRoots(message.params)
|
|
310
325
|
}
|
|
326
|
+
// An install during the session changes the nifra the project builds with, and this process keeps
|
|
327
|
+
// the code it started with: re-read the installed version per call so no answer is for a stale one.
|
|
328
|
+
if (message.method === "tools/call") drift = await detectToolingDrift(rootState.root, version)
|
|
311
329
|
|
|
330
|
+
const method = message.method ?? ""
|
|
331
|
+
if (ANSWERED_WITHOUT_THE_APP.has(method)) {
|
|
332
|
+
const response = await handleRpc(
|
|
333
|
+
message,
|
|
334
|
+
[],
|
|
335
|
+
serverInfo,
|
|
336
|
+
{
|
|
337
|
+
resources: projectResources(rootState.root, noAppInServer),
|
|
338
|
+
prompts: projectPrompts(),
|
|
339
|
+
instructions: rootInstructions(rootState, drift),
|
|
340
|
+
},
|
|
341
|
+
{ state, sendNotification: send, exposeToolErrors: true },
|
|
342
|
+
)
|
|
343
|
+
if (response) send(response)
|
|
344
|
+
// Ask for the client's workspace roots once the handshake completes, and again whenever the
|
|
345
|
+
// client says they changed. The answer comes back through the interception above.
|
|
346
|
+
if (method.startsWith("notifications/")) requestRoots()
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
const project = await ctx
|
|
312
350
|
let activeTools: McpTool[]
|
|
313
|
-
if (
|
|
351
|
+
if (project.apps) {
|
|
352
|
+
const loaded = await Promise.all(
|
|
353
|
+
project.apps.map(async (app) => ({ app, surface: await app.surface() })),
|
|
354
|
+
)
|
|
314
355
|
const allTools: McpTool[] = []
|
|
315
|
-
for (const {
|
|
356
|
+
for (const { app, surface } of loaded) {
|
|
316
357
|
const tools = [
|
|
317
|
-
...refuseVersionSensitive(isolatedProjectTools(
|
|
318
|
-
...
|
|
358
|
+
...refuseVersionSensitive(isolatedProjectTools(app.cwd), drift),
|
|
359
|
+
...surface.tools,
|
|
319
360
|
]
|
|
320
|
-
const ns = namespaceForApp(name, tools, { resources: [], prompts: [] })
|
|
361
|
+
const ns = namespaceForApp(app.name, tools, { resources: [], prompts: [] })
|
|
321
362
|
allTools.push(...ns.tools)
|
|
322
363
|
}
|
|
323
364
|
activeTools = [
|
|
@@ -330,7 +371,7 @@ export async function runMcpServer(
|
|
|
330
371
|
} else {
|
|
331
372
|
activeTools = [
|
|
332
373
|
...refuseVersionSensitive(isolatedProjectTools(rootState.root), drift),
|
|
333
|
-
...(await
|
|
374
|
+
...(await project.surface()).tools,
|
|
334
375
|
]
|
|
335
376
|
}
|
|
336
377
|
const verdict = await rootVerdict(rootState)
|
|
@@ -342,7 +383,7 @@ export async function runMcpServer(
|
|
|
342
383
|
)
|
|
343
384
|
|
|
344
385
|
const features: McpServerFeatures = {
|
|
345
|
-
...
|
|
386
|
+
...project.features,
|
|
346
387
|
instructions: rootInstructions(rootState, drift),
|
|
347
388
|
}
|
|
348
389
|
const response = await handleRpc(message, activeTools, serverInfo, features, {
|
|
@@ -352,14 +393,6 @@ export async function runMcpServer(
|
|
|
352
393
|
exposeToolErrors: true,
|
|
353
394
|
})
|
|
354
395
|
if (response) send(response)
|
|
355
|
-
// Ask for the client's workspace roots once the handshake completes, and again whenever the
|
|
356
|
-
// client says they changed. The answer comes back through the interception above.
|
|
357
|
-
if (
|
|
358
|
-
message.method === "notifications/initialized" ||
|
|
359
|
-
message.method === "notifications/roots/list_changed"
|
|
360
|
-
) {
|
|
361
|
-
requestRoots()
|
|
362
|
-
}
|
|
363
396
|
}
|
|
364
397
|
|
|
365
398
|
const decoder = new TextDecoder()
|
|
@@ -418,6 +451,7 @@ export async function runMcpServer(
|
|
|
418
451
|
send(rpcError(null, -32600, "MCP message exceeded the size limit"))
|
|
419
452
|
}
|
|
420
453
|
}
|
|
454
|
+
closeWarmWorkers()
|
|
421
455
|
}
|
|
422
456
|
|
|
423
457
|
function responseIdOf(message: JsonRpcResponse | JsonRpcNotification): string | number | null {
|