@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/introspect.ts
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
snapshotProjectEvidence,
|
|
14
14
|
} from "@nifrajs/core/evidence"
|
|
15
15
|
import { compileRoutePattern, type RoutePatternSegment } from "@nifrajs/core/pattern"
|
|
16
|
-
import type { ReflectedRoute } from "@nifrajs/core/reflection"
|
|
16
|
+
import type { ReflectedMount, ReflectedRoute } from "@nifrajs/core/reflection"
|
|
17
17
|
import type { Manifest } from "@nifrajs/web"
|
|
18
18
|
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
19
19
|
import { normalizeMountPath } from "@nifrajs/web/route-manifest"
|
|
20
20
|
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
21
|
-
import type {
|
|
21
|
+
import type { IntrospectedApp } from "./load.ts"
|
|
22
22
|
import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
|
|
23
23
|
|
|
24
24
|
/** Read the backend's registered routes, if it's a `server()` with a `.routes()` method. */
|
|
@@ -225,6 +225,22 @@ export function apiRoutesIndexSection(routes: readonly ReflectedRoute[]): string
|
|
|
225
225
|
return `## API routes (backend/app.ts)\n\n${routes.length} route${routes.length === 1 ? "" : "s"}. Call \`nifra_context\` again with \`path\` (a route prefix) and/or \`kind: "api"\` for the body/query/response contracts + the exact \`client<typeof app>\` call form - or \`nifra_routes\` for the same as structured JSON.\n\n${lines.join("\n")}`
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/** The routes opaque mounts declare, under `prefix`, or `undefined` when there are none. Listed apart
|
|
229
|
+
* from the API routes: nifra serves them but never analyzed them. */
|
|
230
|
+
function declaredMountRoutesSection(
|
|
231
|
+
mounts: readonly ReflectedMount[],
|
|
232
|
+
prefix: string | undefined,
|
|
233
|
+
): string | undefined {
|
|
234
|
+
const lines = mounts.flatMap((mount) =>
|
|
235
|
+
(mount.routes ?? [])
|
|
236
|
+
.filter((route) => prefix === undefined || route.path.startsWith(prefix))
|
|
237
|
+
.map((route) => `- \`${route.method} ${route.path}\` - ${mount.path} (${mount.opaque})`),
|
|
238
|
+
)
|
|
239
|
+
return lines.length === 0
|
|
240
|
+
? undefined
|
|
241
|
+
: `## Declared by opaque mounts (not analyzed)\n\n${lines.join("\n")}`
|
|
242
|
+
}
|
|
243
|
+
|
|
228
244
|
/** Markdown section listing the file-routed pages (URL pattern → source file). */
|
|
229
245
|
export function pageRoutesSection(manifest: Manifest | undefined): string {
|
|
230
246
|
if (!manifest || manifest.routes.length === 0) {
|
|
@@ -236,6 +252,10 @@ export function pageRoutesSection(manifest: Manifest | undefined): string {
|
|
|
236
252
|
return `## Page routes (routes/)\n\n${lines.join("\n")}`
|
|
237
253
|
}
|
|
238
254
|
|
|
255
|
+
const PAGES_CONVENTION = `- **Pages:** file-routed under \`routes/\`. A route is two files: \`x.tsx\` (browser) and \`x.backend.ts\`
|
|
256
|
+
(\`loader\`, \`action\`, \`middleware\`; server only). Server code lives under \`backend/\`, code both sides
|
|
257
|
+
use under \`shared/\`, components under \`frontend/\`; the browser build refuses backend code outright.`
|
|
258
|
+
|
|
239
259
|
const CONVENTIONS = `## Conventions (summary)
|
|
240
260
|
|
|
241
261
|
- **Backend:** \`server()\` from \`@nifrajs/core\`; returning a value sends JSON. Validate untrusted input with
|
|
@@ -253,9 +273,7 @@ const CONVENTIONS = `## Conventions (summary)
|
|
|
253
273
|
\`res.data\` from the backend's route types, so the compiler catches any frontend/backend drift. Never
|
|
254
274
|
hand-roll \`fetch\` + ad-hoc response types for an internal API - that's exactly how screens drift. It
|
|
255
275
|
never throws: branch on \`res.ok ? res.data : res.error\`.
|
|
256
|
-
|
|
257
|
-
(\`loader\`, \`action\`, \`middleware\`; server only). Server code lives under \`backend/\`, code both sides
|
|
258
|
-
use under \`shared/\`, components under \`frontend/\`; the browser build refuses backend code outright.
|
|
276
|
+
${PAGES_CONVENTION}
|
|
259
277
|
- \`app.fetch(Request)\` is the universal entry. Full reference: this repo's \`AGENTS.md\`, or \`llms-full.txt\`.`
|
|
260
278
|
|
|
261
279
|
/** Optional narrowing for {@link describeProject} - a path prefix and/or one section. A filtered
|
|
@@ -274,15 +292,18 @@ export interface ContextFilter {
|
|
|
274
292
|
* body/query/response shapes + the typed-client `call` form), and omits the conventions block (the agent
|
|
275
293
|
* already has it from the index call).
|
|
276
294
|
*/
|
|
277
|
-
export function describeProject(app:
|
|
295
|
+
export function describeProject(app: IntrospectedApp, filter?: ContextFilter): string {
|
|
278
296
|
let manifest: Manifest | undefined
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
297
|
+
if (app.framework !== undefined) {
|
|
298
|
+
try {
|
|
299
|
+
manifest = discoverRoutes(app.routesDir)
|
|
300
|
+
} catch {
|
|
301
|
+
// No (or unreadable) routes/ - the page-routes section reports that.
|
|
302
|
+
}
|
|
283
303
|
}
|
|
284
304
|
const prefix = filter?.path
|
|
285
|
-
|
|
305
|
+
const evidence = snapshotProjectEvidence(app.backend)
|
|
306
|
+
let routes = reflectedRoutesFromEvidence(evidence)
|
|
286
307
|
let pages = manifest
|
|
287
308
|
if (prefix !== undefined && prefix !== "") {
|
|
288
309
|
routes = routes.filter((r) => r.path.startsWith(prefix))
|
|
@@ -296,20 +317,34 @@ export function describeProject(app: LoadedApp, filter?: ContextFilter): string
|
|
|
296
317
|
const sections: string[] = [`# nifra project: ${app.cwd.slice(app.cwd.lastIndexOf("/") + 1)}`]
|
|
297
318
|
if (isIndex) {
|
|
298
319
|
sections.push(
|
|
299
|
-
|
|
320
|
+
app.framework === undefined
|
|
321
|
+
? "An API-only nifra app (backend/app.ts, no web config) - Web-standard, one backend on Bun, Node, Deno, and the edge. This is the route INDEX; fetch a route's full body/query/response contract on demand (see below)."
|
|
322
|
+
: "A full-stack nifra app - Web-standard, one app on Bun, Node, Deno, and the edge. This is the route INDEX; fetch a route's full body/query/response contract on demand (see below).",
|
|
300
323
|
// Point agents at the verified surface, not recalled APIs - nifra is young, so training data is the
|
|
301
324
|
// wrong source. The MCP tools (nifra_docs / nifra_example) are checked against the installed version.
|
|
302
325
|
"> **Build against THIS surface + the verified tools - do not write nifra APIs from memory** (the\n> framework is young; recalled APIs drift). For framework code, call `nifra_example` (snippets\n> typechecked against the installed version) or `nifra_docs`; verify edits with `nifra_run` + `nifra_check`.",
|
|
326
|
+
)
|
|
327
|
+
if (app.framework !== undefined) {
|
|
303
328
|
// The authoritative answer, not the static reading `nifra_check`/`nifra_doctor` give: this runs on
|
|
304
329
|
// the LOADED config, so it is the same decision `nifra dev` and `nifra build` make. Stated in the
|
|
305
330
|
// index because it governs where a plugin goes and which toolchain compiles a component - an edit
|
|
306
331
|
// put in the other pipeline's slot is accepted and then silently never runs.
|
|
307
|
-
|
|
308
|
-
|
|
332
|
+
sections.push(
|
|
333
|
+
`**Bundler**: ${describePipeline(chooseBuildPipeline(app.resolvedPlugins))}. \`vitePlugins\` feed Vite; \`clientPlugins\`/\`serverPlugins\` feed Bun.build. Both phases (dev and build) use the same one.`,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
309
336
|
}
|
|
310
337
|
if (filter?.kind !== "pages") {
|
|
311
338
|
// Index → compact `METHOD path` list; a slice → the full schema + call-form detail.
|
|
312
339
|
sections.push(isIndex ? apiRoutesIndexSection(routes) : apiRoutesSection(routes))
|
|
340
|
+
const declared = declaredMountRoutesSection(evidence.mounts ?? [], prefix || undefined)
|
|
341
|
+
if (declared !== undefined) sections.push(declared)
|
|
342
|
+
}
|
|
343
|
+
if (app.framework === undefined) {
|
|
344
|
+
if (filter?.kind === "pages")
|
|
345
|
+
sections.push("## Page routes\n\nAn API-only app serves no pages.")
|
|
346
|
+
if (isIndex) sections.push(CONVENTIONS.replace(`${PAGES_CONVENTION}\n`, ""))
|
|
347
|
+
return sections.join("\n\n")
|
|
313
348
|
}
|
|
314
349
|
if (filter?.kind !== "api") sections.push(pageRoutesSection(pages))
|
|
315
350
|
if (isIndex) sections.push(CONVENTIONS)
|
|
@@ -368,7 +403,7 @@ export function routesToJsonFromEvidence(
|
|
|
368
403
|
}
|
|
369
404
|
|
|
370
405
|
export function routesToJson(
|
|
371
|
-
app:
|
|
406
|
+
app: IntrospectedApp,
|
|
372
407
|
pathPrefix?: string,
|
|
373
408
|
evidence?: ProjectEvidenceSnapshot,
|
|
374
409
|
): RouteJson[] {
|
|
@@ -382,7 +417,7 @@ export function routesToJson(
|
|
|
382
417
|
// `nifra routes` answers a different question: "what does this app actually serve, and with which
|
|
383
418
|
// methods?" - so an agent sees instantly that `POST /api/explain` is (or isn't) mounted, instead of
|
|
384
419
|
// discovering it via a 405. It marks which backend routes are AUTO-MOUNTED under the page router's
|
|
385
|
-
// `apiPrefix
|
|
420
|
+
// `apiPrefix`, and `--json` makes it machine-consumable. The table-building +
|
|
386
421
|
// rendering are pure (no fs / no module loads), so they're unit-tested; the cwd gathering is the thin
|
|
387
422
|
// async wrapper `describeRoutes`.
|
|
388
423
|
// ===================================================================================================
|
|
@@ -416,7 +451,7 @@ export interface PageRouteInput {
|
|
|
416
451
|
export interface RouteTableInput {
|
|
417
452
|
readonly pages: readonly PageRouteInput[]
|
|
418
453
|
readonly api: ReadonlyArray<Pick<ReflectedRoute, "method" | "path">>
|
|
419
|
-
/** The page router's `apiPrefix
|
|
454
|
+
/** The page router's `apiPrefix`; an API route at/under it is auto-mounted. Unset or `""`
|
|
420
455
|
* disables the auto-mount, so no API route is marked auto-mounted. */
|
|
421
456
|
readonly apiPrefix?: string
|
|
422
457
|
/** The backend declares its routes without the prefix and is served at `apiPrefix + path`: every API
|
|
@@ -440,7 +475,7 @@ function isUnderPrefix(path: string, prefix: string): boolean {
|
|
|
440
475
|
* joined methods, for stable output.
|
|
441
476
|
*/
|
|
442
477
|
export function buildRouteTable(input: RouteTableInput): RouteTableEntry[] {
|
|
443
|
-
const configured = input.apiPrefix ?? "
|
|
478
|
+
const configured = input.apiPrefix ?? ""
|
|
444
479
|
// Read the prefix as the server's mount table does (`/api/` and `/api/*` are `/api`).
|
|
445
480
|
const apiPrefix = configured === "" ? "" : (normalizeMountPath(configured) ?? configured)
|
|
446
481
|
const stripped = input.apiStrip === true && apiPrefix !== ""
|
|
@@ -683,12 +718,15 @@ interface MaybeActionModule {
|
|
|
683
718
|
* text (default) or JSON (`--json`). The page-route load mirrors what SSR would do; a load failure
|
|
684
719
|
* degrades to GET-only for that route (never throws the whole command).
|
|
685
720
|
*/
|
|
686
|
-
async function collectRouteTable(app:
|
|
721
|
+
async function collectRouteTable(app: IntrospectedApp): Promise<RouteTableEntry[]> {
|
|
722
|
+
const { framework } = app
|
|
687
723
|
let manifest: Manifest | undefined
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
724
|
+
if (framework !== undefined) {
|
|
725
|
+
try {
|
|
726
|
+
manifest = discoverRoutes(app.routesDir)
|
|
727
|
+
} catch {
|
|
728
|
+
// No (or unreadable) routes/ - fall through with no pages; the table still shows API routes.
|
|
729
|
+
}
|
|
692
730
|
}
|
|
693
731
|
const pages: PageRouteInput[] = []
|
|
694
732
|
for (const route of manifest?.routes ?? []) {
|
|
@@ -704,14 +742,14 @@ async function collectRouteTable(app: LoadedApp): Promise<RouteTableEntry[]> {
|
|
|
704
742
|
const rows = buildRouteTable({
|
|
705
743
|
pages,
|
|
706
744
|
api: backendRoutes(app.backend),
|
|
707
|
-
...(
|
|
708
|
-
...(
|
|
745
|
+
...(framework?.apiPrefix !== undefined ? { apiPrefix: framework.apiPrefix } : {}),
|
|
746
|
+
...(framework?.apiStrip !== undefined ? { apiStrip: framework.apiStrip } : {}),
|
|
709
747
|
})
|
|
710
748
|
return rows
|
|
711
749
|
}
|
|
712
750
|
|
|
713
751
|
export async function describeRoutes(
|
|
714
|
-
app:
|
|
752
|
+
app: IntrospectedApp,
|
|
715
753
|
opts: { readonly json?: boolean } = {},
|
|
716
754
|
): Promise<string> {
|
|
717
755
|
const rows = await collectRouteTable(app)
|
|
@@ -720,7 +758,7 @@ export async function describeRoutes(
|
|
|
720
758
|
|
|
721
759
|
/** Gather and render the public route graph for `nifra routes --graph`. */
|
|
722
760
|
export async function describeRouteGraph(
|
|
723
|
-
app:
|
|
761
|
+
app: IntrospectedApp,
|
|
724
762
|
opts: { readonly json?: boolean } = {},
|
|
725
763
|
): Promise<string> {
|
|
726
764
|
const graph = buildRouteGraph(await collectRouteTable(app))
|
package/src/levels-tool.ts
CHANGED
|
@@ -13,10 +13,19 @@
|
|
|
13
13
|
|
|
14
14
|
import { existsSync } from "node:fs"
|
|
15
15
|
import { resolve } from "node:path"
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
AssuranceReport,
|
|
18
|
+
InvariantSetup,
|
|
19
|
+
InvariantSetupResult,
|
|
20
|
+
InvariantsConfig,
|
|
21
|
+
} from "@nifrajs/core/assurance"
|
|
17
22
|
import { snapshotCapabilities } from "@nifrajs/core/capabilities"
|
|
18
23
|
import { buildNifraManifest, parseNifraManifest } from "@nifrajs/core/manifest"
|
|
19
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
type AdversarialContractOptions,
|
|
26
|
+
type ContractTestApp,
|
|
27
|
+
runAdversarialContract,
|
|
28
|
+
} from "@nifrajs/testing"
|
|
20
29
|
import {
|
|
21
30
|
type CapabilityProjectReport,
|
|
22
31
|
diffCapabilitySnapshots,
|
|
@@ -176,41 +185,129 @@ export async function collectVerificationLevels(
|
|
|
176
185
|
"no isolated invariant executor configured - add `invariants.executor` in nifra.assurance.ts",
|
|
177
186
|
)
|
|
178
187
|
} else {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
l4Reasons.push(...(await runInvariants(config.source, config.invariants, options.seed ?? 1)))
|
|
189
|
+
}
|
|
190
|
+
statuses.push({
|
|
191
|
+
level: 4,
|
|
192
|
+
name: "invariant-tested",
|
|
193
|
+
ok: l4Reasons.length === 0,
|
|
194
|
+
reasons: l4Reasons,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
return finalize(statuses, assurance.routes)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run L4 against the configured target. `setup` runs at most once, on the first request, and its
|
|
202
|
+
* `teardown` runs once the run ends - after a failure too - so an app can provision a throwaway target
|
|
203
|
+
* only when L4 actually exercises it.
|
|
204
|
+
*/
|
|
205
|
+
async function runInvariants(
|
|
206
|
+
source: unknown,
|
|
207
|
+
invariants: InvariantsConfig,
|
|
208
|
+
seed: number,
|
|
209
|
+
): Promise<string[]> {
|
|
210
|
+
let provisioned: Promise<InvariantSetupResult | undefined> | undefined
|
|
211
|
+
const provision = (setup: InvariantSetup): Promise<InvariantSetupResult | undefined> => {
|
|
212
|
+
provisioned ??= Promise.resolve()
|
|
213
|
+
.then(setup)
|
|
214
|
+
.then(
|
|
215
|
+
(result) => result ?? undefined,
|
|
216
|
+
(error: unknown) => {
|
|
217
|
+
throw new Error(`invariants.setup failed: ${messageOf(error)}`)
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
return provisioned
|
|
221
|
+
}
|
|
222
|
+
const fetch = async (request: Request): Promise<Response> => {
|
|
223
|
+
const ready = invariants.setup === undefined ? undefined : await provision(invariants.setup)
|
|
224
|
+
const executor = ready?.executor ?? invariants.executor
|
|
225
|
+
if (executor === undefined) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
"no invariant executor: invariants.setup returned none and invariants.executor is not set",
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
return executor(request)
|
|
231
|
+
}
|
|
232
|
+
const options: AdversarialContractOptions = {
|
|
233
|
+
seed,
|
|
234
|
+
runtimes: [{ name: "isolated", fetch }],
|
|
235
|
+
expectedValidationStatuses: [400, 422],
|
|
236
|
+
requireCoverage: true,
|
|
237
|
+
}
|
|
238
|
+
const runOptions: Record<string, unknown> = {}
|
|
239
|
+
for (const name of INVARIANT_RUN_OPTIONS) {
|
|
240
|
+
if (invariants[name] !== undefined) runOptions[name] = invariants[name]
|
|
241
|
+
}
|
|
242
|
+
const reasons: string[] = []
|
|
243
|
+
try {
|
|
244
|
+
const report = await runAdversarialContract(source as ContractTestApp, {
|
|
245
|
+
...options,
|
|
246
|
+
...runOptions,
|
|
184
247
|
})
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
248
|
+
const target = await targetFailure(provisioned, invariants)
|
|
249
|
+
if (target !== undefined) reasons.push(target)
|
|
250
|
+
else if (!report.ok) {
|
|
251
|
+
reasons.push(
|
|
252
|
+
...report.failures
|
|
188
253
|
.slice(0, 5)
|
|
189
254
|
.map(
|
|
190
255
|
(failure) =>
|
|
191
256
|
`${failure.runtime}: ${failure.id} - ${failure.message} (replay seed ${failure.replay.seed})`,
|
|
192
257
|
),
|
|
193
258
|
)
|
|
194
|
-
|
|
195
|
-
...
|
|
196
|
-
.slice(0, Math.max(0, 5 -
|
|
259
|
+
reasons.push(
|
|
260
|
+
...report.gaps
|
|
261
|
+
.slice(0, Math.max(0, 5 - reasons.length))
|
|
197
262
|
.map(
|
|
198
263
|
(gap) =>
|
|
199
264
|
`${gap.route ?? "contract"}${gap.target ? ` ${gap.target}` : ""}: ${gap.message}`,
|
|
200
265
|
),
|
|
201
266
|
)
|
|
202
267
|
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
reasons.push((await targetFailure(provisioned, invariants)) ?? messageOf(error))
|
|
270
|
+
} finally {
|
|
271
|
+
const ready = await provisioned?.catch(() => undefined)
|
|
272
|
+
if (ready?.teardown !== undefined) {
|
|
273
|
+
try {
|
|
274
|
+
await ready.teardown()
|
|
275
|
+
} catch (error) {
|
|
276
|
+
reasons.push(`invariants teardown failed: ${messageOf(error)}`)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
203
279
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
name: "invariant-tested",
|
|
207
|
-
ok: l4Reasons.length === 0,
|
|
208
|
-
reasons: l4Reasons,
|
|
209
|
-
})
|
|
280
|
+
return reasons
|
|
281
|
+
}
|
|
210
282
|
|
|
211
|
-
|
|
283
|
+
const INVARIANT_RUN_OPTIONS = [
|
|
284
|
+
"witnesses",
|
|
285
|
+
"prepareRequest",
|
|
286
|
+
"isRejected",
|
|
287
|
+
"include",
|
|
288
|
+
"maxMutationsPerInput",
|
|
289
|
+
] as const
|
|
290
|
+
|
|
291
|
+
/** The setup or missing-executor failure every case hit, if the target never came up. */
|
|
292
|
+
async function targetFailure(
|
|
293
|
+
provisioned: Promise<InvariantSetupResult | undefined> | undefined,
|
|
294
|
+
invariants: InvariantsConfig,
|
|
295
|
+
): Promise<string | undefined> {
|
|
296
|
+
if (provisioned === undefined) return undefined
|
|
297
|
+
try {
|
|
298
|
+
const ready = await provisioned
|
|
299
|
+
if (ready?.executor === undefined && invariants.executor === undefined) {
|
|
300
|
+
return "no invariant executor: invariants.setup returned none and invariants.executor is not set"
|
|
301
|
+
}
|
|
302
|
+
return undefined
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return messageOf(error)
|
|
305
|
+
}
|
|
212
306
|
}
|
|
213
307
|
|
|
308
|
+
const messageOf = (error: unknown): string =>
|
|
309
|
+
error instanceof Error ? error.message : String(error)
|
|
310
|
+
|
|
214
311
|
function finalize(
|
|
215
312
|
statuses: readonly { level: number; name: string; ok: boolean; reasons: string[] }[],
|
|
216
313
|
routes: readonly { readonly responseContract?: "enforce" | "warn" | "unchecked" }[],
|
package/src/load.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type PluginsField =
|
|
|
40
40
|
* don't).
|
|
41
41
|
*/
|
|
42
42
|
export interface NifraFramework {
|
|
43
|
+
/** Optional coordinated Vite 8 component graph. Tooling stays in nifra.config.ts. */
|
|
44
|
+
readonly componentPipeline?: Omit<
|
|
45
|
+
import("@nifrajs/web/server-components/build").ComponentPipelineOptions,
|
|
46
|
+
"root" | "outDir" | "port" | "plugins"
|
|
47
|
+
>
|
|
48
|
+
readonly serverComponents?: CreateWebAppOptions["serverComponents"]
|
|
43
49
|
/** The render adapter, e.g. `reactAdapter` from `@nifrajs/web-react`. */
|
|
44
50
|
readonly adapter: unknown
|
|
45
51
|
/** The client runtime module providing `mountRouter`, e.g. `"@nifrajs/web-react/client"`. */
|
|
@@ -72,8 +78,11 @@ export interface NifraFramework {
|
|
|
72
78
|
* targets use the socket peer either way. */
|
|
73
79
|
readonly clientIp?: "platform"
|
|
74
80
|
/** `nifra dev` settings. `indicator: false` turns the in-page issues badge off for every run
|
|
75
|
-
* (`nifra dev --no-indicator` does it for one); browser errors still reach `nifra errors`.
|
|
76
|
-
|
|
81
|
+
* (`nifra dev --no-indicator` does it for one); browser errors still reach `nifra errors`.
|
|
82
|
+
* `allowedHosts` lists host names the Bun dev server answers besides loopback ones; it refuses
|
|
83
|
+
* every other `Host` so a DNS-rebound page cannot read it. The Vite pipeline uses Vite's own
|
|
84
|
+
* `server.allowedHosts`. */
|
|
85
|
+
readonly dev?: { readonly indicator?: boolean; readonly allowedHosts?: readonly string[] }
|
|
77
86
|
/** Static files directory. Defaults to `<app>/public`; `false` disables it. */
|
|
78
87
|
readonly publicDir?: string | false
|
|
79
88
|
/** Client-visible environment prefix (default `"PUBLIC_"`; empty disables exposure). */
|
|
@@ -85,9 +94,9 @@ export interface NifraFramework {
|
|
|
85
94
|
readonly cssCodeSplit?: boolean
|
|
86
95
|
/** Framework-owned SSR stylesheet activation. Deferred mode is intended for aggregate CSS. */
|
|
87
96
|
readonly cssLoading?: CssLoadingMode
|
|
88
|
-
/** Path `backend/app.ts`'s backend is served at
|
|
89
|
-
* page file under it can never render, so the app refuses to start and `nifra build`
|
|
90
|
-
* build.
|
|
97
|
+
/** Path `backend/app.ts`'s backend is served at over HTTP, such as `"/api"`; unset or `""`, it is
|
|
98
|
+
* not mounted. A page file under it can never render, so the app refuses to start and `nifra build`
|
|
99
|
+
* refuses to build.
|
|
91
100
|
*
|
|
92
101
|
* The fields from here down reach the generated server entry, so like `use` they MUST be exported
|
|
93
102
|
* from `backend/framework.ts` (re-export them from `nifra.config.ts` when both exist). */
|
|
@@ -106,7 +115,14 @@ export interface NifraFramework {
|
|
|
106
115
|
|
|
107
116
|
/** The {@link NifraFramework} fields forwarded to `createWebApp` and imported by the generated server
|
|
108
117
|
* entry, in one list so every command that builds the app forwards the same set. */
|
|
109
|
-
export const FRAMEWORK_WEB_OPTIONS = [
|
|
118
|
+
export const FRAMEWORK_WEB_OPTIONS = [
|
|
119
|
+
"apiPrefix",
|
|
120
|
+
"apiStrip",
|
|
121
|
+
"mounts",
|
|
122
|
+
"csp",
|
|
123
|
+
"nonce",
|
|
124
|
+
"serverComponents",
|
|
125
|
+
] as const
|
|
110
126
|
export type FrameworkWebOption = (typeof FRAMEWORK_WEB_OPTIONS)[number]
|
|
111
127
|
|
|
112
128
|
/** Refuse a forwarded field of the wrong shape at load, naming the field, rather than letting it fail
|
|
@@ -115,6 +131,24 @@ function assertFrameworkWebOptions(fw: Partial<NifraFramework>, configFile: stri
|
|
|
115
131
|
const wrong = (field: string, expected: string): never => {
|
|
116
132
|
throw new Error(`[nifra] ${configFile}: \`${field}\` must be ${expected}.`)
|
|
117
133
|
}
|
|
134
|
+
if (fw.componentPipeline !== undefined) {
|
|
135
|
+
const pipeline = fw.componentPipeline
|
|
136
|
+
if (
|
|
137
|
+
typeof pipeline !== "object" ||
|
|
138
|
+
pipeline === null ||
|
|
139
|
+
!["react", "vue", "svelte", "solid", "preact"].includes(pipeline.adapter) ||
|
|
140
|
+
typeof pipeline.entries !== "object" ||
|
|
141
|
+
pipeline.entries === null ||
|
|
142
|
+
typeof pipeline.entries.client !== "string" ||
|
|
143
|
+
typeof pipeline.entries.ssr !== "string" ||
|
|
144
|
+
(pipeline.adapter === "react" && typeof pipeline.entries.rsc !== "string") ||
|
|
145
|
+
(pipeline.adapter !== "react" && pipeline.entries.rsc !== undefined)
|
|
146
|
+
)
|
|
147
|
+
wrong(
|
|
148
|
+
"componentPipeline",
|
|
149
|
+
"an adapter plus existing client/ssr entries and a React-only rsc entry",
|
|
150
|
+
)
|
|
151
|
+
}
|
|
118
152
|
if (fw.apiPrefix !== undefined && typeof fw.apiPrefix !== "string") {
|
|
119
153
|
wrong("apiPrefix", 'a path string such as "/api", or "" to turn the backend mount off')
|
|
120
154
|
}
|
|
@@ -143,6 +177,13 @@ function assertFrameworkWebOptions(fw: Partial<NifraFramework>, configFile: stri
|
|
|
143
177
|
const indicator: unknown = Reflect.get(fw.dev, "indicator")
|
|
144
178
|
if (indicator !== undefined && typeof indicator !== "boolean")
|
|
145
179
|
wrong("dev.indicator", "a boolean")
|
|
180
|
+
const allowedHosts: unknown = Reflect.get(fw.dev, "allowedHosts")
|
|
181
|
+
if (
|
|
182
|
+
allowedHosts !== undefined &&
|
|
183
|
+
(!Array.isArray(allowedHosts) ||
|
|
184
|
+
allowedHosts.some((host) => typeof host !== "string" || host.trim() === ""))
|
|
185
|
+
)
|
|
186
|
+
wrong("dev.allowedHosts", 'an array of host names such as ["app.test"]')
|
|
146
187
|
}
|
|
147
188
|
}
|
|
148
189
|
|
|
@@ -198,6 +239,23 @@ export interface LoadedApp {
|
|
|
198
239
|
readonly backend: unknown
|
|
199
240
|
}
|
|
200
241
|
|
|
242
|
+
/** An API-only app as the backend readers see it: `backend/app.ts` and no web config. */
|
|
243
|
+
export interface BackendOnlyApp {
|
|
244
|
+
readonly cwd: string
|
|
245
|
+
readonly backend: unknown
|
|
246
|
+
readonly framework?: undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** What the backend readers (routes, context, hooks, OpenAPI, the app's MCP surface) load. */
|
|
250
|
+
export type IntrospectedApp = LoadedApp | BackendOnlyApp
|
|
251
|
+
|
|
252
|
+
/** How a command reaches the app. The MCP server hands out one whose calls both refuse. */
|
|
253
|
+
export interface AppLoader {
|
|
254
|
+
(outDirName?: string): Promise<LoadedApp>
|
|
255
|
+
/** The whole app with a web config, else `backend/app.ts` alone. */
|
|
256
|
+
readonly backend: () => Promise<IntrospectedApp>
|
|
257
|
+
}
|
|
258
|
+
|
|
201
259
|
export interface LoadAppOptions {
|
|
202
260
|
/** Optional dynamic-import query used by long-lived MCP processes to bust Bun's module cache. */
|
|
203
261
|
readonly importQuery?: string
|
|
@@ -258,22 +316,24 @@ const isAdapter = (v: unknown): boolean => typeof v === "object" && v !== null
|
|
|
258
316
|
const importWithQuery = (path: string, query: string | undefined): Promise<unknown> =>
|
|
259
317
|
import(query === undefined || query === "" ? path : `${path}?${query}`)
|
|
260
318
|
|
|
319
|
+
/** Whether `cwd` has a web config file, the one thing that makes it more than its backend. */
|
|
320
|
+
export const hasAppConfig = (cwd: string): boolean =>
|
|
321
|
+
existsSync(resolve(cwd, CONFIG_FILE)) || existsSync(resolve(cwd, FRAMEWORK_FILE))
|
|
322
|
+
|
|
261
323
|
/**
|
|
262
|
-
* What reads only the backend
|
|
263
|
-
*
|
|
324
|
+
* What reads only the backend: the whole app when it has a web config, else `backend/app.ts`
|
|
325
|
+
* alone, so an API-only app needs no adapter for its routes, context, hooks or OpenAPI document.
|
|
264
326
|
*/
|
|
265
327
|
export async function loadBackendApp(
|
|
266
328
|
cwd: string,
|
|
267
329
|
options: LoadAppOptions = {},
|
|
268
|
-
): Promise<
|
|
269
|
-
if (
|
|
270
|
-
return loadApp(cwd, "dist", options)
|
|
271
|
-
}
|
|
330
|
+
): Promise<IntrospectedApp> {
|
|
331
|
+
if (hasAppConfig(cwd)) return loadApp(cwd, "dist", options)
|
|
272
332
|
assertCurrentLayout(cwd)
|
|
273
333
|
const backendPath = resolve(cwd, BACKEND_APP_FILE)
|
|
274
334
|
if (!existsSync(backendPath)) {
|
|
275
335
|
throw new Error(
|
|
276
|
-
`[nifra] no ${BACKEND_APP_FILE} in ${cwd}:
|
|
336
|
+
`[nifra] no ${CONFIG_FILE}, ${FRAMEWORK_FILE} or ${BACKEND_APP_FILE} in ${cwd}: a web app needs its config, an API-only app its ${BACKEND_APP_FILE}`,
|
|
277
337
|
)
|
|
278
338
|
}
|
|
279
339
|
const backend = (
|
|
@@ -303,7 +363,11 @@ export async function loadApp(
|
|
|
303
363
|
" nifra is zero-config but needs one of them exporting at least:\n" +
|
|
304
364
|
" export const adapter = reactAdapter // from @nifrajs/web-react\n" +
|
|
305
365
|
' export const clientModule = "@nifrajs/web-react/client"\n' +
|
|
306
|
-
" (create-nifra scaffolds this for you.)"
|
|
366
|
+
" (create-nifra scaffolds this for you.)" +
|
|
367
|
+
(existsSync(resolve(cwd, BACKEND_APP_FILE))
|
|
368
|
+
? `\n With ${BACKEND_APP_FILE} and no web config this is an API-only app: \`nifra routes\`,\n` +
|
|
369
|
+
" `nifra context`, `nifra openapi` and `nifra mcp` read its backend; pages need the config."
|
|
370
|
+
: ""),
|
|
307
371
|
)
|
|
308
372
|
}
|
|
309
373
|
if (!existsSync(resolve(cwd, "routes"))) {
|
package/src/mcp-context.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { realpathSync } from "node:fs"
|
|
9
9
|
import { readFile } from "node:fs/promises"
|
|
10
10
|
import { basename, isAbsolute, relative, resolve } from "node:path"
|
|
11
|
-
import type {
|
|
11
|
+
import type { AppLoader } from "./load.ts"
|
|
12
12
|
import { createCachedAppLoader } from "./mcp-exec.ts"
|
|
13
13
|
import { resolveMcpProjectPath } from "./mcp-path.ts"
|
|
14
14
|
import type { McpPrompt, McpResource, McpServerFeatures, McpTool } from "./mcp-protocol.ts"
|
|
@@ -20,11 +20,11 @@ function openApiFormat(args: Record<string, unknown>): "json" | "yaml" {
|
|
|
20
20
|
|
|
21
21
|
export async function openApiHandler(
|
|
22
22
|
args: Record<string, unknown>,
|
|
23
|
-
loadAppCached:
|
|
23
|
+
loadAppCached: AppLoader,
|
|
24
24
|
): Promise<string> {
|
|
25
25
|
const { renderOpenApiWithTypes } = await import("./openapi-tool.ts")
|
|
26
26
|
const pathPrefix = typeof args.path === "string" ? args.path : undefined
|
|
27
|
-
return renderOpenApiWithTypes(await loadAppCached(), openApiFormat(args), pathPrefix)
|
|
27
|
+
return renderOpenApiWithTypes(await loadAppCached.backend(), openApiFormat(args), pathPrefix)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async function readProjectFile(
|
|
@@ -57,7 +57,7 @@ function promptText(
|
|
|
57
57
|
|
|
58
58
|
export function projectResources(
|
|
59
59
|
cwd: string,
|
|
60
|
-
loadAppCached:
|
|
60
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
61
61
|
): McpResource[] {
|
|
62
62
|
return [
|
|
63
63
|
{
|
|
@@ -68,7 +68,7 @@ export function projectResources(
|
|
|
68
68
|
mimeType: "application/json",
|
|
69
69
|
read: async () => {
|
|
70
70
|
const { routesToJson } = await import("./introspect.ts")
|
|
71
|
-
return { text: JSON.stringify(routesToJson(await loadAppCached()), null, 2) }
|
|
71
|
+
return { text: JSON.stringify(routesToJson(await loadAppCached.backend()), null, 2) }
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
{
|
|
@@ -165,7 +165,7 @@ export function projectPrompts(): McpPrompt[] {
|
|
|
165
165
|
|
|
166
166
|
export function projectFeatures(
|
|
167
167
|
cwd: string,
|
|
168
|
-
loadAppCached:
|
|
168
|
+
loadAppCached: AppLoader = createCachedAppLoader(cwd),
|
|
169
169
|
): McpServerFeatures {
|
|
170
170
|
return { resources: projectResources(cwd, loadAppCached), prompts: projectPrompts() }
|
|
171
171
|
}
|
|
@@ -218,7 +218,7 @@ export function dirError(dir: string | undefined): string {
|
|
|
218
218
|
/** Build the project-scoped tools for `cwd`. */
|
|
219
219
|
export interface CommandMcpToolOptions {
|
|
220
220
|
readonly cwd: string
|
|
221
|
-
readonly loadAppCached?:
|
|
221
|
+
readonly loadAppCached?: AppLoader
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/** Adapt one executable catalog spec to its MCP descriptor and project-scoped handler. */
|