@nifrajs/cli 2.2.0 → 2.3.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 +25 -25
- package/dist/assure.d.ts +2 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +17 -10
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +7 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +68 -16
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +61 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +332 -86
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +30 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +132 -36
- package/dist/cli.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.js +7 -7
- package/dist/docs-search.d.ts +3 -3
- package/dist/docs-search.js +6 -6
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.js +8 -8
- package/dist/examples.d.ts +5 -5
- package/dist/examples.js +5 -5
- package/dist/init-agents.d.ts +14 -14
- package/dist/init-agents.js +14 -14
- package/dist/internal/typescript-import.d.ts +24 -0
- package/dist/internal/typescript-import.d.ts.map +1 -0
- package/dist/internal/typescript-import.js +32 -0
- package/dist/internal/typescript-import.js.map +1 -0
- package/dist/introspect.d.ts +12 -12
- package/dist/introspect.js +24 -24
- package/dist/levels-tool.d.ts +8 -8
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -30
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +6 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +15 -7
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +4 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-docs-tools.d.ts +1 -1
- package/dist/mcp-docs-tools.js +9 -9
- package/dist/mcp-http.d.ts +5 -5
- package/dist/mcp-http.js +7 -7
- package/dist/mcp-protocol.d.ts +2 -2
- package/dist/mcp-protocol.js +2 -2
- package/dist/mcp-render.d.ts +3 -3
- package/dist/mcp-render.js +6 -6
- package/dist/mcp-run.d.ts +2 -2
- package/dist/mcp-run.js +2 -2
- package/dist/mcp.d.ts +14 -14
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +75 -45
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +1 -1
- package/dist/openapi-tool.js +2 -2
- package/dist/pipeline-guard.d.ts +1 -1
- package/dist/pipeline-guard.js +1 -1
- package/dist/port.d.ts +9 -9
- package/dist/port.js +27 -27
- package/dist/recipes/1.8.0.d.ts +2 -2
- package/dist/recipes/1.8.0.js +5 -5
- package/dist/recipes/index.d.ts +2 -2
- package/dist/recipes/index.js +1 -1
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +8 -8
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.d.ts.map +1 -0
- package/dist/sync-routes.js +55 -0
- package/dist/sync-routes.js.map +1 -0
- package/dist/types-search.d.ts +5 -5
- package/dist/types-search.js +7 -7
- package/dist/upgrade.d.ts +7 -7
- package/dist/upgrade.js +12 -12
- package/dist/verification.d.ts +51 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +80 -0
- package/dist/verification.js.map +1 -0
- package/docs/examples.json +174 -90
- package/docs/llms-full.txt +1696 -747
- package/docs/types.json +1892 -3105
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +405 -103
- package/src/cli.ts +141 -38
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +10 -10
- package/src/examples.ts +7 -7
- package/src/init-agents.ts +17 -17
- package/src/internal/typescript-import.ts +33 -0
- package/src/introspect.ts +28 -28
- package/src/levels-tool.ts +37 -31
- package/src/load.ts +21 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-docs-tools.ts +9 -9
- package/src/mcp-http.ts +7 -7
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +6 -6
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +82 -45
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +1 -1
- package/src/port.ts +31 -31
- package/src/recipes/1.8.0.ts +5 -5
- package/src/recipes/index.ts +2 -2
- package/src/scaffold.ts +8 -8
- package/src/sync-routes.ts +69 -0
- package/src/types-search.ts +8 -8
- package/src/upgrade.ts +12 -12
- package/src/verification.ts +123 -0
package/src/cli.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* `nifra`
|
|
3
|
+
* `nifra` - the zero-config CLI for a nifra app. Reads `framework.ts` + `backend.ts` + `routes/` from
|
|
4
4
|
* the project root (see {@link loadApp}) and wires the right `@nifrajs/web` entrypoint:
|
|
5
5
|
*
|
|
6
|
-
* nifra dev true-HMR dev server (Vite middleware + nifra SSR)
|
|
7
|
-
* nifra build emit a complete target-specific deploy directory
|
|
8
|
-
* nifra start run the default Bun build
|
|
6
|
+
* nifra dev true-HMR dev server (Vite middleware + nifra SSR) - @nifrajs/web/vite
|
|
7
|
+
* nifra build emit a complete target-specific deploy directory - @nifrajs/web/build
|
|
8
|
+
* nifra start run the default Bun build - dist/server.js
|
|
9
9
|
*
|
|
10
10
|
* Bun-only (it runs the framework's TS + Bun plugins directly). The *output* runs anywhere.
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync } from "node:fs"
|
|
13
13
|
import { resolve } from "node:path"
|
|
14
14
|
import { inProcessClient } from "@nifrajs/client"
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
createWebApp,
|
|
17
|
+
DEFAULT_DEV_PORT,
|
|
18
|
+
type RenderAdapter,
|
|
19
|
+
SERVER_FN_MODULE,
|
|
20
|
+
SERVER_ONLY_MODULE,
|
|
21
|
+
} from "@nifrajs/web"
|
|
16
22
|
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
17
23
|
import type { BunPlugin } from "bun"
|
|
18
24
|
import { describeProject, describeRoutes } from "./introspect.ts"
|
|
@@ -30,15 +36,15 @@ export interface Flags {
|
|
|
30
36
|
/** `nifra build --vite`: force the client + server through Vite/Rollup. Without a flag the pipeline is
|
|
31
37
|
* chosen per app (`chooseBuildPipeline`) - Bun unless the app's only transforms are `vitePlugins`. */
|
|
32
38
|
readonly vite: boolean
|
|
33
|
-
/** `nifra dev --bun`: run the Bun-pipeline dev server (Bun.serve native HMR
|
|
34
|
-
* Refresh with state preserved
|
|
39
|
+
/** `nifra dev --bun`: run the Bun-pipeline dev server (Bun.serve native HMR - including React Fast
|
|
40
|
+
* Refresh with state preserved - and no Vite in the process). Default stays Vite for its plugin
|
|
35
41
|
* ecosystem; `--bun` trades that for one bundler across dev and prod. Refuses CSS-Modules apps.
|
|
36
42
|
*
|
|
37
43
|
* `nifra build --bun`: force the Bun build. Refuses when that would drop the app's `vitePlugins`. */
|
|
38
44
|
readonly bun: boolean
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
const HELP = `nifra
|
|
47
|
+
const HELP = `nifra - zero-config dev/build/start for a nifra app
|
|
42
48
|
|
|
43
49
|
Usage:
|
|
44
50
|
nifra dev [--port <n>] [--poll] Start the true-HMR dev server (Vite). Default port ${DEFAULT_DEV_PORT}.
|
|
@@ -84,7 +90,7 @@ Usage:
|
|
|
84
90
|
nifra_example (verified snippets), nifra_scaffold (route→file),
|
|
85
91
|
nifra_check (drift gate + fixes), nifra_levels (verification
|
|
86
92
|
ladder), nifra_doctor (deps + duplicate installs).
|
|
87
|
-
nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example)
|
|
93
|
+
nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example) -
|
|
88
94
|
self-host on a VPS so any remote agent can learn nifra. Default :8787.
|
|
89
95
|
nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
|
|
90
96
|
server-only imports in routes/). Run as "done"; --json for agents;
|
|
@@ -92,13 +98,16 @@ Usage:
|
|
|
92
98
|
nifra sync-manifest Regenerate a committed web server-manifest.ts from routes/ WITHOUT
|
|
93
99
|
a full build - clears server-manifest drift after a route add/rename
|
|
94
100
|
(a new hydrating component still needs a full build).
|
|
101
|
+
nifra sync-routes Regenerate nifra-routes.d.ts from routes/ so navigate({ to, search })
|
|
102
|
+
is typed against each static route's searchSchema (a stale shape is a
|
|
103
|
+
tsc error). Include the file in your tsconfig. Pure file write.
|
|
95
104
|
nifra snapshot [--out <file>] Write the backend's API contract (routes + schemas) as plain
|
|
96
|
-
JSON
|
|
105
|
+
JSON - the baseline for \`nifra diff\`. Default api-snapshot.json.
|
|
97
106
|
nifra diff [<baseline>] [--json] Breaking-change gate: re-snapshot the contract and compare
|
|
98
107
|
against the committed baseline. Direction-aware (a new required
|
|
99
108
|
request field or a removed response field breaks; widening a
|
|
100
109
|
request enum or adding a response field doesn't) and fails
|
|
101
|
-
closed. Exits non-zero on any breaking change
|
|
110
|
+
closed. Exits non-zero on any breaking change - run it in CI.
|
|
102
111
|
nifra assure [--config <file>] [--json] Route-assurance gate: load nifra.assurance.ts, classify every
|
|
103
112
|
reflected backend route, and fail when required enforcement
|
|
104
113
|
evidence is missing/forbidden or a route is unclassified.
|
|
@@ -141,7 +150,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
|
|
|
141
150
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
|
|
142
151
|
|
|
143
152
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
144
|
-
const CLI_VERSION = "2.
|
|
153
|
+
const CLI_VERSION = "2.3.0"
|
|
145
154
|
|
|
146
155
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
147
156
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
@@ -149,20 +158,54 @@ const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
|
|
|
149
158
|
const apiOf = (backend: unknown): { api?: unknown } =>
|
|
150
159
|
backend === undefined ? {} : { api: inProcessClient(backend as never) }
|
|
151
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Render an error for the CLI, unwrapping the detail a bare `.message` drops.
|
|
163
|
+
*
|
|
164
|
+
* `Bun.build` reports a bundling failure as an `AggregateError` whose `.message` is a generic
|
|
165
|
+
* "Bundle failed" and whose `.errors` hold the real causes - the unresolved import, the plugin that
|
|
166
|
+
* threw, each with a file and line. `buildClient`/`buildServer` catch a RETURNED `{ success: false }`
|
|
167
|
+
* and print `result.logs`, but a `Bun.build` that THROWS (a plugin `onLoad`/`onResolve` that throws, a
|
|
168
|
+
* resolution failure) rejects before that check runs, so the AggregateError reaches the CLI whole and a
|
|
169
|
+
* catch printing only `.message` throws the causes away. That is the difference between "Bundle failed"
|
|
170
|
+
* and "Could not resolve ./db from routes/x.tsx". So unwrap an AggregateError - directly, or one carried
|
|
171
|
+
* as a `.cause` - into a line per underlying error.
|
|
172
|
+
*/
|
|
173
|
+
export function formatCliError(err: unknown): string {
|
|
174
|
+
const aggregate =
|
|
175
|
+
err instanceof AggregateError
|
|
176
|
+
? err
|
|
177
|
+
: err instanceof Error && err.cause instanceof AggregateError
|
|
178
|
+
? err.cause
|
|
179
|
+
: undefined
|
|
180
|
+
if (aggregate !== undefined && aggregate.errors.length > 0) {
|
|
181
|
+
const outer = err instanceof Error ? err.message : ""
|
|
182
|
+
const head = outer !== "" && outer !== "Bundle failed" ? outer : "build failed"
|
|
183
|
+
// `String(e)` (not `e.message`) so a Bun BuildMessage/ResolveMessage renders with its file + line,
|
|
184
|
+
// not just the bare text. Multi-line entries are indented to stay under the head. Deduplicated
|
|
185
|
+
// because Bun can repeat one cause across `.errors`.
|
|
186
|
+
const details = [...new Set(aggregate.errors.map((e) => String(e).trim()))].map(
|
|
187
|
+
(line) => ` - ${line.replace(/\n/g, "\n ")}`,
|
|
188
|
+
)
|
|
189
|
+
return [head, ...details].join("\n")
|
|
190
|
+
}
|
|
191
|
+
if (err instanceof Error) return err.message
|
|
192
|
+
return String(err)
|
|
193
|
+
}
|
|
194
|
+
|
|
152
195
|
/**
|
|
153
196
|
* Refuse `nifra dev --bun` for an app Bun's DEV-server bundler cannot compile.
|
|
154
197
|
*
|
|
155
198
|
* Bun's dev server and `Bun.build` are not the same bundler. `Bun.build` transforms `*.module.css` into a
|
|
156
199
|
* scoped class map (the Bun production build of a CSS-Modules app works, verified), but the dev server's
|
|
157
200
|
* bundler does not: the import compiles to a dangling reference and the browser throws
|
|
158
|
-
* `ReferenceError: import_X_module is not defined` from inside the component
|
|
201
|
+
* `ReferenceError: import_X_module is not defined` from inside the component - a message that names
|
|
159
202
|
* neither CSS Modules nor the dev server, so the cause is invisible.
|
|
160
203
|
*
|
|
161
204
|
* A silently-broken client is the one outcome worth failing the command over, so this checks up front and
|
|
162
205
|
* names the trade plus both ways out. Narrow by design: only the transform proven missing is refused, so
|
|
163
206
|
* an app without CSS Modules still gets the Bun dev loop.
|
|
164
207
|
*/
|
|
165
|
-
async function assertBunDevSupportsApp(app: LoadedApp): Promise<void> {
|
|
208
|
+
export async function assertBunDevSupportsApp(app: LoadedApp): Promise<void> {
|
|
166
209
|
const { Glob } = await import("bun")
|
|
167
210
|
const offenders: string[] = []
|
|
168
211
|
for await (const file of new Glob("**/*.module.css").scan({ cwd: app.cwd, dot: false })) {
|
|
@@ -170,6 +213,58 @@ async function assertBunDevSupportsApp(app: LoadedApp): Promise<void> {
|
|
|
170
213
|
offenders.push(file)
|
|
171
214
|
if (offenders.length >= 5) break
|
|
172
215
|
}
|
|
216
|
+
// Server functions are the same trade with a worse consequence. The client build replaces a `*.fn`
|
|
217
|
+
// module with stubs so its body never reaches a browser; Bun's dev-server bundler takes no plugins, so
|
|
218
|
+
// there it would be bundled WHOLE - the function bodies and every secret they import. Measured, not
|
|
219
|
+
// assumed: a runtime `Bun.plugin` onLoad does not reach `HTMLBundle`, so the module ships raw.
|
|
220
|
+
//
|
|
221
|
+
// `*.server` modules are the same trade again: the client build EMPTIES them, and with no plugins
|
|
222
|
+
// here they ship whole.
|
|
223
|
+
//
|
|
224
|
+
// Both are matched with the SAME regexes the transforms use, imported rather than re-encoded. A
|
|
225
|
+
// hand-written glob here is how these two gates drifted from the pipelines they guard: the old
|
|
226
|
+
// `**/*.fn.{ts,tsx,js,jsx}` missed `.fn.mts`, `.fn.cts`, `.fn.mjs` and `.fn.cjs`, every one of which
|
|
227
|
+
// both build pipelines stub - so a module the framework calls a server function was waved straight
|
|
228
|
+
// into the browser by the check that exists to stop exactly that. A gate written in a second dialect
|
|
229
|
+
// is a gate that eventually disagrees with the thing it guards.
|
|
230
|
+
const leaking: string[] = []
|
|
231
|
+
const serverOnly: string[] = []
|
|
232
|
+
for await (const file of new Glob("**/*.{server,fn,ts,tsx,js,jsx,mts,cts,mjs,cjs}").scan({
|
|
233
|
+
cwd: app.cwd,
|
|
234
|
+
dot: false,
|
|
235
|
+
})) {
|
|
236
|
+
if (/(^|\/)(node_modules|dist|\.nifra|\.git)\//.test(file)) continue
|
|
237
|
+
// Strip the source extension once so the conventions' regexes see `src/todos.fn` / `src/db.server`,
|
|
238
|
+
// which is the shape they match. The glob also includes the extensionless `.fn` / `.server` forms;
|
|
239
|
+
// those already have the matcher-ready shape and pass through unchanged.
|
|
240
|
+
const stem = file.replace(/\.[cm]?[jt]sx?$/, "")
|
|
241
|
+
if (SERVER_FN_MODULE.test(stem)) {
|
|
242
|
+
if (leaking.length < 5) leaking.push(file)
|
|
243
|
+
} else if (SERVER_ONLY_MODULE.test(stem)) {
|
|
244
|
+
if (serverOnly.length < 5) serverOnly.push(file)
|
|
245
|
+
}
|
|
246
|
+
if (leaking.length >= 5 && serverOnly.length >= 5) break
|
|
247
|
+
}
|
|
248
|
+
if (serverOnly.length > 0) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"[nifra] `nifra dev --bun` can't empty `*.server` modules: Bun's DEV-server bundler takes no " +
|
|
251
|
+
"plugins, so a server-only module would be bundled WHOLE into the browser - along with whatever " +
|
|
252
|
+
"it imports and any secret it holds. `nifra build` and `nifra dev` (Vite) both replace it with " +
|
|
253
|
+
"an empty module, so only this pipeline is affected.\n" +
|
|
254
|
+
` Server-only modules found: ${serverOnly.join(", ")}\n` +
|
|
255
|
+
" Use `nifra dev` (the Vite pipeline), or reach these from a route/loader for the Bun dev loop.",
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
if (leaking.length > 0) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
"[nifra] `nifra dev --bun` can't transform server functions: Bun's DEV-server bundler takes no " +
|
|
261
|
+
"plugins, so a `*.fn` module would be bundled WHOLE into the browser - its function bodies and " +
|
|
262
|
+
"anything they import, secrets included. `nifra build` and `nifra dev` (Vite) both replace it " +
|
|
263
|
+
"with client stubs, so only this pipeline is affected.\n" +
|
|
264
|
+
` Server-function modules found: ${leaking.join(", ")}\n` +
|
|
265
|
+
" Use `nifra dev` (the Vite pipeline), or move these calls behind a route for the Bun dev loop.",
|
|
266
|
+
)
|
|
267
|
+
}
|
|
173
268
|
if (offenders.length === 0) return
|
|
174
269
|
throw new Error(
|
|
175
270
|
"[nifra] `nifra dev --bun` can't compile CSS Modules: Bun's DEV-server bundler has no `*.module.css` " +
|
|
@@ -181,7 +276,7 @@ async function assertBunDevSupportsApp(app: LoadedApp): Promise<void> {
|
|
|
181
276
|
}
|
|
182
277
|
|
|
183
278
|
async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
184
|
-
// `--bun`: the Bun-pipeline dev server
|
|
279
|
+
// `--bun`: the Bun-pipeline dev server - Bun.serve's native HMR bundles + hot-reloads the client while
|
|
185
280
|
// Bun's runtime resolves SSR, with no Vite in the process at all. The mirror of `nifra build --vite`:
|
|
186
281
|
// each pipeline is selectable in BOTH phases, and neither ever runs inside the other.
|
|
187
282
|
if (flags.bun) {
|
|
@@ -193,7 +288,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
193
288
|
outDir,
|
|
194
289
|
clientModule: fw.clientModule,
|
|
195
290
|
port: flags.port,
|
|
196
|
-
// The Bun pipeline's own plugins (SFC compilers etc.)
|
|
291
|
+
// The Bun pipeline's own plugins (SFC compilers etc.) - never `vitePlugins`, which belong to the
|
|
197
292
|
// other pipeline; `assertPipelineSeparation` already refuses a plugin sitting in the wrong slot.
|
|
198
293
|
plugins: asBunPlugins(app.resolvedPlugins.clientPlugins),
|
|
199
294
|
...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
|
|
@@ -237,6 +332,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
237
332
|
clientModule: fw.clientModule,
|
|
238
333
|
plugins: app.resolvedPlugins.vitePlugins,
|
|
239
334
|
...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
|
|
335
|
+
...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
|
|
240
336
|
poll: flags.poll,
|
|
241
337
|
port: flags.port,
|
|
242
338
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
@@ -256,9 +352,9 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
256
352
|
}
|
|
257
353
|
|
|
258
354
|
/**
|
|
259
|
-
* `nifra build --target <t>`
|
|
355
|
+
* `nifra build --target <t>` - package the engine (buildClient + buildServer + prerender) into one
|
|
260
356
|
* command that emits a full deploy dir, so an app no longer hand-writes build-bun.ts + _worker.ts +
|
|
261
|
-
* _routes.json per target. The adapter is imported from `framework.ts` (the edge-bundlable file
|
|
357
|
+
* _routes.json per target. The adapter is imported from `framework.ts` (the edge-bundlable file - never
|
|
262
358
|
* `nifra.config.ts`, which pulls in Vite plugins), and the backend from `backend.ts` when present;
|
|
263
359
|
* `buildTarget` generates the per-target server entry from those + the app's `routes/`.
|
|
264
360
|
*/
|
|
@@ -278,7 +374,7 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
|
|
|
278
374
|
flags.vite ? "vite" : flags.bun ? "bun" : undefined,
|
|
279
375
|
)
|
|
280
376
|
const useVite = decision.pipeline === "vite"
|
|
281
|
-
// Same deploy-dir output
|
|
377
|
+
// Same deploy-dir output - buildTargetVite delegates to the same orchestrator as buildTarget - so only
|
|
282
378
|
// the bundler and the plugin FORMAT (Vite plugins, not Bun) differ.
|
|
283
379
|
const buildTarget = useVite
|
|
284
380
|
? (await import("@nifrajs/web/build-vite")).buildTargetVite
|
|
@@ -317,7 +413,7 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
|
|
|
317
413
|
...(fw.define ? { define: fw.define } : {}),
|
|
318
414
|
...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
|
|
319
415
|
...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
|
|
320
|
-
// The static target needs a built app to drive prerendering
|
|
416
|
+
// The static target needs a built app to drive prerendering - only build it when targeting static.
|
|
321
417
|
...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
|
|
322
418
|
})
|
|
323
419
|
console.log(`nifra build (${target}${useVite ? ", vite" : ""}) → ${result.run}`)
|
|
@@ -330,7 +426,7 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
|
|
|
330
426
|
/** Build the app FACTORY for the `static` target's prerender pass. Registers the
|
|
331
427
|
* framework's SSR Bun plugins (so `.vue`/`.svelte`/Solid routes import) once, then return a factory that
|
|
332
428
|
* `createWebApp`s the app for the client build's manifest. The client entry MUST be the real content-hashed
|
|
333
|
-
* bundle (`client.entry`)
|
|
429
|
+
* bundle (`client.entry`) - it's the hydration `<script src>` the prerendered HTML emits, so a placeholder
|
|
334
430
|
* would 404 and the pages would render but never hydrate (inert controls). Styles/route-preload are wired
|
|
335
431
|
* from the same manifest so the static HTML matches the deployed app. */
|
|
336
432
|
async function buildPrerenderApp(
|
|
@@ -371,7 +467,7 @@ async function start(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
371
467
|
)
|
|
372
468
|
}
|
|
373
469
|
throw new Error(
|
|
374
|
-
`[nifra] no ${serverFile}
|
|
470
|
+
`[nifra] no ${serverFile} - run \`nifra build\` or \`nifra build --target bun\` first.`,
|
|
375
471
|
)
|
|
376
472
|
}
|
|
377
473
|
Bun.env.PORT = String(flags.port)
|
|
@@ -416,14 +512,14 @@ async function main(): Promise<void> {
|
|
|
416
512
|
console.log(`nifra (@nifrajs/cli) ${CLI_VERSION}`)
|
|
417
513
|
return
|
|
418
514
|
}
|
|
419
|
-
// `mcp` runs a long-lived stdio server and loads the project lazily per-tool
|
|
515
|
+
// `mcp` runs a long-lived stdio server and loads the project lazily per-tool - it must not go through
|
|
420
516
|
// the eager `loadApp` below (which would fail fast on a project that's API-only / not yet built).
|
|
421
517
|
if (command === "mcp") {
|
|
422
518
|
const { runMcpServer } = await import("./mcp.ts")
|
|
423
519
|
await runMcpServer(process.cwd(), CLI_VERSION)
|
|
424
520
|
return
|
|
425
521
|
}
|
|
426
|
-
// `docs-mcp` runs the PUBLIC docs MCP over HTTP
|
|
522
|
+
// `docs-mcp` runs the PUBLIC docs MCP over HTTP - project-independent (serves the bundled corpus), so
|
|
427
523
|
// it self-hosts anywhere Bun runs (a VPS behind a reverse proxy, a container). Long-lived; no loadApp.
|
|
428
524
|
if (command === "docs-mcp") {
|
|
429
525
|
const { handleMcpHttp } = await import("./mcp-http.ts")
|
|
@@ -436,7 +532,7 @@ async function main(): Promise<void> {
|
|
|
436
532
|
console.log(`nifra docs MCP (HTTP) → ${server.url}`)
|
|
437
533
|
return
|
|
438
534
|
}
|
|
439
|
-
// `check` is a pure cwd-based gate (typecheck + lint)
|
|
535
|
+
// `check` is a pure cwd-based gate (typecheck + lint) - it must run even when the project doesn't
|
|
440
536
|
// load (API-only, not built yet), so it dispatches before the eager `loadApp` below.
|
|
441
537
|
if (command === "check") {
|
|
442
538
|
const { runCheck } = await import("./check.ts")
|
|
@@ -468,7 +564,14 @@ async function main(): Promise<void> {
|
|
|
468
564
|
if (!(await runSyncManifest(process.cwd()))) process.exitCode = 1
|
|
469
565
|
return
|
|
470
566
|
}
|
|
471
|
-
// `
|
|
567
|
+
// `sync-routes` regenerates the route-search types `.d.ts` (typed `navigate({ to, search })`) from
|
|
568
|
+
// `routes/`. Pure fs/text (needs only the route tree, not `framework.ts`), so dispatch before `loadApp`.
|
|
569
|
+
if (command === "sync-routes") {
|
|
570
|
+
const { runSyncRoutes } = await import("./sync-routes.ts")
|
|
571
|
+
if (!(await runSyncRoutes(process.cwd()))) process.exitCode = 1
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
// `doctor` is a pure cwd check (imports vs declared deps) - like `check`, dispatch before `loadApp`
|
|
472
575
|
// so it runs on an API-only / not-yet-built project.
|
|
473
576
|
if (command === "doctor") {
|
|
474
577
|
const { runDoctor } = await import("./doctor.ts")
|
|
@@ -481,7 +584,7 @@ async function main(): Promise<void> {
|
|
|
481
584
|
process.exitCode = 1
|
|
482
585
|
return
|
|
483
586
|
}
|
|
484
|
-
// `snapshot` / `diff` load ONLY backend.ts (the API contract)
|
|
587
|
+
// `snapshot` / `diff` load ONLY backend.ts (the API contract) - like `check`, they must run on an
|
|
485
588
|
// API-only project, so they dispatch before the eager `loadApp`.
|
|
486
589
|
if (command === "snapshot") {
|
|
487
590
|
const { runSnapshot } = await import("./diff-tool.ts")
|
|
@@ -495,7 +598,7 @@ async function main(): Promise<void> {
|
|
|
495
598
|
try {
|
|
496
599
|
await runSnapshot(process.cwd(), out !== undefined ? { out } : {})
|
|
497
600
|
} catch (err) {
|
|
498
|
-
console.error(
|
|
601
|
+
console.error(formatCliError(err))
|
|
499
602
|
process.exitCode = 1
|
|
500
603
|
}
|
|
501
604
|
return
|
|
@@ -508,7 +611,7 @@ async function main(): Promise<void> {
|
|
|
508
611
|
process.exitCode = 1
|
|
509
612
|
}
|
|
510
613
|
} catch (err) {
|
|
511
|
-
console.error(
|
|
614
|
+
console.error(formatCliError(err))
|
|
512
615
|
process.exitCode = 1
|
|
513
616
|
}
|
|
514
617
|
return
|
|
@@ -533,7 +636,7 @@ async function main(): Promise<void> {
|
|
|
533
636
|
)
|
|
534
637
|
process.exitCode = 1
|
|
535
638
|
} catch (err) {
|
|
536
|
-
console.error(
|
|
639
|
+
console.error(formatCliError(err))
|
|
537
640
|
process.exitCode = 1
|
|
538
641
|
}
|
|
539
642
|
return
|
|
@@ -574,7 +677,7 @@ async function main(): Promise<void> {
|
|
|
574
677
|
throw new Error("[nifra] capabilities needs `snapshot` or `check`")
|
|
575
678
|
}
|
|
576
679
|
} catch (err) {
|
|
577
|
-
console.error(
|
|
680
|
+
console.error(formatCliError(err))
|
|
578
681
|
process.exitCode = 1
|
|
579
682
|
}
|
|
580
683
|
return
|
|
@@ -616,7 +719,7 @@ async function main(): Promise<void> {
|
|
|
616
719
|
throw new Error("[nifra] manifest needs `emit` or `diff`")
|
|
617
720
|
}
|
|
618
721
|
} catch (err) {
|
|
619
|
-
console.error(
|
|
722
|
+
console.error(formatCliError(err))
|
|
620
723
|
process.exitCode = 1
|
|
621
724
|
}
|
|
622
725
|
return
|
|
@@ -647,13 +750,13 @@ async function main(): Promise<void> {
|
|
|
647
750
|
)
|
|
648
751
|
process.exitCode = 1
|
|
649
752
|
} catch (err) {
|
|
650
|
-
console.error(
|
|
753
|
+
console.error(formatCliError(err))
|
|
651
754
|
process.exitCode = 1
|
|
652
755
|
}
|
|
653
756
|
return
|
|
654
757
|
}
|
|
655
758
|
// `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
|
|
656
|
-
// recipe, then verified with `nifra check`. Dispatch before the eager `loadApp`
|
|
759
|
+
// recipe, then verified with `nifra check`. Dispatch before the eager `loadApp` - it must run on any
|
|
657
760
|
// repo (API-only, not built, or mid-upgrade with edits that don't yet typecheck under dry-run).
|
|
658
761
|
if (command === "upgrade") {
|
|
659
762
|
const { runUpgrade } = await import("./upgrade.ts")
|
|
@@ -668,12 +771,12 @@ async function main(): Promise<void> {
|
|
|
668
771
|
})
|
|
669
772
|
if (!ok) process.exitCode = 1
|
|
670
773
|
} catch (err) {
|
|
671
|
-
console.error(
|
|
774
|
+
console.error(formatCliError(err))
|
|
672
775
|
process.exitCode = 1
|
|
673
776
|
}
|
|
674
777
|
return
|
|
675
778
|
}
|
|
676
|
-
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app)
|
|
779
|
+
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
|
|
677
780
|
// `doctor`, dispatch before the eager `loadApp` so it runs on an API-only / not-yet-built project.
|
|
678
781
|
if (command === "port") {
|
|
679
782
|
const { runPort } = await import("./port.ts")
|
|
@@ -693,7 +796,7 @@ async function main(): Promise<void> {
|
|
|
693
796
|
})
|
|
694
797
|
if (!passed) process.exitCode = 1
|
|
695
798
|
} catch (err) {
|
|
696
|
-
console.error(
|
|
799
|
+
console.error(formatCliError(err))
|
|
697
800
|
process.exitCode = 1
|
|
698
801
|
}
|
|
699
802
|
return
|
|
@@ -748,7 +851,7 @@ async function main(): Promise<void> {
|
|
|
748
851
|
// exported `parseFlags`. `import.meta.main` is true only for the process's entry module.
|
|
749
852
|
if (import.meta.main) {
|
|
750
853
|
main().catch((err) => {
|
|
751
|
-
console.error(
|
|
854
|
+
console.error(formatCliError(err))
|
|
752
855
|
process.exitCode = 1
|
|
753
856
|
})
|
|
754
857
|
}
|
package/src/diff-tool.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra snapshot` / `nifra diff`
|
|
2
|
+
* `nifra snapshot` / `nifra diff` - the API breaking-change gate.
|
|
3
3
|
*
|
|
4
4
|
* nifra snapshot [--out api-snapshot.json] Write the backend's route contract as plain JSON.
|
|
5
5
|
* nifra diff <baseline.json> [--json] Re-snapshot and compare against the committed
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* before it ships. Classification lives in `@nifrajs/core/diff` (direction-aware, fails closed);
|
|
11
11
|
* this module only loads `backend.ts` and renders the result.
|
|
12
12
|
*
|
|
13
|
-
* Loads ONLY `backend.ts`
|
|
13
|
+
* Loads ONLY `backend.ts` - the API contract - so it works on an API-only project with no
|
|
14
14
|
* framework config or routes/ directory (unlike the eager `loadApp`).
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
snapshotRoutes,
|
|
25
25
|
} from "@nifrajs/core/diff"
|
|
26
26
|
|
|
27
|
-
/** Snapshot file envelope
|
|
27
|
+
/** Snapshot file envelope - versioned so a future format change can migrate instead of misparse. */
|
|
28
28
|
export interface SnapshotFile {
|
|
29
29
|
readonly nifraSnapshot: 1
|
|
30
30
|
readonly routes: readonly RouteSnapshot[]
|
|
@@ -39,7 +39,7 @@ export function parseSnapshotFile(content: string, sourcePath: string): Snapshot
|
|
|
39
39
|
parsed = JSON.parse(content)
|
|
40
40
|
} catch {
|
|
41
41
|
throw new Error(
|
|
42
|
-
`[nifra] ${sourcePath} is not valid JSON
|
|
42
|
+
`[nifra] ${sourcePath} is not valid JSON - regenerate it with \`nifra snapshot\`.`,
|
|
43
43
|
)
|
|
44
44
|
}
|
|
45
45
|
const record = parsed as Partial<SnapshotFile> | null
|
|
@@ -50,7 +50,7 @@ export function parseSnapshotFile(content: string, sourcePath: string): Snapshot
|
|
|
50
50
|
!Array.isArray(record.routes)
|
|
51
51
|
) {
|
|
52
52
|
throw new Error(
|
|
53
|
-
`[nifra] ${sourcePath} is not a nifra API snapshot
|
|
53
|
+
`[nifra] ${sourcePath} is not a nifra API snapshot - expected { "nifraSnapshot": 1, "routes": [...] }.`,
|
|
54
54
|
)
|
|
55
55
|
}
|
|
56
56
|
return record as SnapshotFile
|
|
@@ -76,7 +76,7 @@ export function formatDiff(diff: RoutesDiff): string {
|
|
|
76
76
|
.map(describeChange)
|
|
77
77
|
const breaking = diff.changes.filter((change) => change.severity === "breaking").length
|
|
78
78
|
const summary = diff.hasBreaking
|
|
79
|
-
? `${breaking} breaking change${breaking === 1 ? "" : "s"}
|
|
79
|
+
? `${breaking} breaking change${breaking === 1 ? "" : "s"} - existing clients will fail.`
|
|
80
80
|
: "No breaking changes."
|
|
81
81
|
return `${lines.join("\n")}\n\n${summary}`
|
|
82
82
|
}
|
|
@@ -86,7 +86,7 @@ async function snapshotBackend(cwd: string): Promise<readonly RouteSnapshot[]> {
|
|
|
86
86
|
const backendPath = resolve(cwd, "backend.ts")
|
|
87
87
|
if (!existsSync(backendPath)) {
|
|
88
88
|
throw new Error(
|
|
89
|
-
`[nifra] no backend.ts in ${cwd}
|
|
89
|
+
`[nifra] no backend.ts in ${cwd} - \`nifra snapshot\`/\`nifra diff\` compare the API contract, which lives in backend.ts.`,
|
|
90
90
|
)
|
|
91
91
|
}
|
|
92
92
|
const backend = ((await import(backendPath)) as { backend?: unknown }).backend
|
|
@@ -115,7 +115,7 @@ export async function runDiff(
|
|
|
115
115
|
const baselineFile = Bun.file(resolved)
|
|
116
116
|
if (!(await baselineFile.exists())) {
|
|
117
117
|
throw new Error(
|
|
118
|
-
`[nifra] baseline not found: ${resolved}
|
|
118
|
+
`[nifra] baseline not found: ${resolved} - create it on your main branch with \`nifra snapshot\`.`,
|
|
119
119
|
)
|
|
120
120
|
}
|
|
121
121
|
const baseline = parseSnapshotFile(await baselineFile.text(), baselinePath)
|
package/src/docs-search.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra_docs`
|
|
2
|
+
* `nifra_docs` - searchable framework documentation for agents. The corpus is the generated
|
|
3
3
|
* `llms-full.txt` (written into this package by `bun run gen:llms`, so it can't drift from the
|
|
4
4
|
* source it's generated from). Instead of dumping the whole ~150 KB into an agent's context, the
|
|
5
|
-
* tool keyword-scores the document's `##` sections and returns only the top matches
|
|
5
|
+
* tool keyword-scores the document's `##` sections and returns only the top matches - or, with no
|
|
6
6
|
* query, just the section index (the cheap discovery call).
|
|
7
7
|
*
|
|
8
8
|
* Deliberately NOT a vector search: the corpus is one curated document with descriptive headings;
|
|
@@ -68,7 +68,7 @@ export function searchSections(
|
|
|
68
68
|
return scored.sort((a, b) => b.score - a.score).slice(0, limit)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** Cap a section body for the tool result
|
|
71
|
+
/** Cap a section body for the tool result - whole sections can be pages long; the agent can ask
|
|
72
72
|
* again with a narrower query if the trim cuts what it needed. */
|
|
73
73
|
const MAX_SECTION_CHARS = 2800
|
|
74
74
|
|
|
@@ -81,7 +81,7 @@ export function renderDocsResult(
|
|
|
81
81
|
const sections = splitSections(markdown)
|
|
82
82
|
if (query === undefined || query.trim() === "") {
|
|
83
83
|
const index = sections.map((s) => `- ${s.heading}`).join("\n")
|
|
84
|
-
return `# nifra docs
|
|
84
|
+
return `# nifra docs - section index (pass \`query\` to fetch matching sections)\n\n${index}`
|
|
85
85
|
}
|
|
86
86
|
const matches = searchSections(sections, query, limit)
|
|
87
87
|
if (matches.length === 0) {
|
|
@@ -91,7 +91,7 @@ export function renderDocsResult(
|
|
|
91
91
|
.map((m) => {
|
|
92
92
|
const body =
|
|
93
93
|
m.body.length > MAX_SECTION_CHARS
|
|
94
|
-
? `${m.body.slice(0, MAX_SECTION_CHARS)}\n…(trimmed
|
|
94
|
+
? `${m.body.slice(0, MAX_SECTION_CHARS)}\n…(trimmed - narrow the query for the rest)`
|
|
95
95
|
: m.body
|
|
96
96
|
return `## ${m.heading}\n\n${body}`
|
|
97
97
|
})
|
|
@@ -99,7 +99,7 @@ export function renderDocsResult(
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** Load the bundled corpus. Resolves relative to this module so it works from `src/` (repo dev)
|
|
102
|
-
* and `dist/` (published) alike
|
|
102
|
+
* and `dist/` (published) alike - `docs/llms-full.txt` sits at the package root next to both. */
|
|
103
103
|
export async function loadDocsCorpus(): Promise<string | undefined> {
|
|
104
104
|
const url = new URL("../docs/llms-full.txt", import.meta.url)
|
|
105
105
|
try {
|
package/src/doctor.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra doctor`
|
|
2
|
+
* `nifra doctor` - catches the Bun-workspace footgun the typed-client lint can't: a package that's
|
|
3
3
|
* IMPORTED in source but NOT declared in `package.json`. Bun resolves it at runtime (via hoisting or a
|
|
4
|
-
* workspace), so tests pass and `bun install` reports "no changes"
|
|
4
|
+
* workspace), so tests pass and `bun install` reports "no changes" - false confidence - yet `tsc` fails
|
|
5
5
|
* and a fresh or standalone install can't resolve it. doctor diffs every bare import specifier against
|
|
6
6
|
* the package's declared dependencies and flags the gap, with a stable `--json` shape for agents/CI.
|
|
7
7
|
*
|
|
8
8
|
* Scope is intentionally per-package: it checks the `package.json` at `cwd`, because a dependency must be
|
|
9
9
|
* declared by the package that imports it (for `tsc` and for that package to install on its own) even
|
|
10
10
|
* when a monorepo would hoist it. Relative paths, runtime builtins (node core, `node:`/`bun:`, `bun`),
|
|
11
|
-
* the package's own name, and tsconfig `paths` aliases are excluded
|
|
11
|
+
* the package's own name, and tsconfig `paths` aliases are excluded - none of them are npm deps.
|
|
12
12
|
*/
|
|
13
13
|
import { realpath, stat } from "node:fs/promises"
|
|
14
14
|
import { builtinModules } from "node:module"
|
|
@@ -25,7 +25,7 @@ const BUILTINS: ReadonlySet<string> = new Set<string>([
|
|
|
25
25
|
|
|
26
26
|
// The four specifier-bearing forms: static import/re-export with a source, side-effect import, dynamic
|
|
27
27
|
// import, and CJS require. Anchored with `(?<![.\w$])` so `myimport`/`.import`/`foorequire` never match.
|
|
28
|
-
// Comments are stripped before these run (see stripComments)
|
|
28
|
+
// Comments are stripped before these run (see stripComments) - else a doc-comment usage example would be
|
|
29
29
|
// flagged as a real import.
|
|
30
30
|
const IMPORT_PATTERNS: readonly RegExp[] = [
|
|
31
31
|
/(?<![.\w$])(?:import|export)\b[^'"]*?\bfrom\s*['"]([^'"]+)['"]/g,
|
|
@@ -56,7 +56,7 @@ export function packageOf(spec: string): string | undefined {
|
|
|
56
56
|
if (spec.startsWith("node:") || spec.startsWith("bun:")) return undefined
|
|
57
57
|
if (spec.startsWith("@")) {
|
|
58
58
|
const [scope, name] = spec.split("/")
|
|
59
|
-
if (!scope || scope === "@" || !name) return undefined // `@/alias`, `@foo`
|
|
59
|
+
if (!scope || scope === "@" || !name) return undefined // `@/alias`, `@foo` - not a real package
|
|
60
60
|
const pkg = `${scope}/${name}`
|
|
61
61
|
return isNpmPackageName(pkg) ? pkg : undefined
|
|
62
62
|
}
|
|
@@ -66,7 +66,7 @@ export function packageOf(spec: string): string | undefined {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** Build a predicate matching tsconfig `paths` aliases (e.g. `@/*`, `~/utils`), which resolve to local
|
|
69
|
-
* source, not npm packages
|
|
69
|
+
* source, not npm packages - so doctor must not flag them. */
|
|
70
70
|
export function aliasMatcher(
|
|
71
71
|
paths: Readonly<Record<string, unknown>> | undefined,
|
|
72
72
|
): (spec: string) => boolean {
|
|
@@ -113,13 +113,13 @@ export function scanUndeclaredImports(
|
|
|
113
113
|
export interface DoctorFinding {
|
|
114
114
|
readonly file: string
|
|
115
115
|
readonly line: number
|
|
116
|
-
/** The undeclared package name
|
|
116
|
+
/** The undeclared package name - add it to `package.json` dependencies. */
|
|
117
117
|
readonly package: string
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export interface DoctorResult {
|
|
121
121
|
readonly ok: boolean
|
|
122
|
-
/** `false` when no `package.json` was found at cwd
|
|
122
|
+
/** `false` when no `package.json` was found at cwd - doctor can't run (reported, not a crash). */
|
|
123
123
|
readonly ran: boolean
|
|
124
124
|
readonly findings: readonly DoctorFinding[]
|
|
125
125
|
/** Packages that resolve to more than one physical install across this workspace. */
|
|
@@ -622,7 +622,7 @@ export async function runDoctor(
|
|
|
622
622
|
}
|
|
623
623
|
console.log("nifra doctor\n")
|
|
624
624
|
if (!result.ran) {
|
|
625
|
-
console.log("• no package.json at this directory
|
|
625
|
+
console.log("• no package.json at this directory - nothing to check")
|
|
626
626
|
return true
|
|
627
627
|
}
|
|
628
628
|
if (result.fixed && result.fixed.length > 0) {
|
|
@@ -635,7 +635,7 @@ export async function runDoctor(
|
|
|
635
635
|
if (result.skippedFixes && result.skippedFixes.length > 0) {
|
|
636
636
|
console.log("• not auto-fixed:")
|
|
637
637
|
for (const f of result.skippedFixes) {
|
|
638
|
-
console.log(` ${f.package}
|
|
638
|
+
console.log(` ${f.package} - ${f.reason}; run \`${f.command.join(" ")}\``)
|
|
639
639
|
}
|
|
640
640
|
console.log("")
|
|
641
641
|
}
|
package/src/examples.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra_example`
|
|
2
|
+
* `nifra_example` - verified, copy-pasteable code examples for agents. The corpus is `examples.json`
|
|
3
3
|
* (written into this package by `bun run gen:llms`): the same doc snippets `check:docs` typechecks
|
|
4
4
|
* against the live `@nifrajs/*` API. So every example this returns is GUARANTEED to compile against the
|
|
5
|
-
* installed version
|
|
5
|
+
* installed version - the antidote to an agent hallucinating a framework API from stale training.
|
|
6
6
|
*
|
|
7
7
|
* Keyword-scored over each example's name/topic/slug (curated identifiers, heavily weighted) and its
|
|
8
8
|
* code body. No query → a grouped index of what's available (the cheap discovery call).
|
|
@@ -12,9 +12,9 @@ import { countBodyHits, queryTermGroups, tokenize, tokenSetHas } from "./search-
|
|
|
12
12
|
|
|
13
13
|
export interface Example {
|
|
14
14
|
readonly name: string
|
|
15
|
-
/** The doc page's title, e.g. "Nifra
|
|
15
|
+
/** The doc page's title, e.g. "Nifra - Auth & sessions". */
|
|
16
16
|
readonly topic: string
|
|
17
|
-
/** The doc page slug, e.g. "auth"
|
|
17
|
+
/** The doc page slug, e.g. "auth" - a short, matchable tag. */
|
|
18
18
|
readonly slug: string
|
|
19
19
|
readonly code: string
|
|
20
20
|
}
|
|
@@ -63,19 +63,19 @@ export function renderExamplesResult(
|
|
|
63
63
|
const index = [...byTopic.entries()]
|
|
64
64
|
.map(([topic, names]) => `- **${topic}**: ${names.join(", ")}`)
|
|
65
65
|
.join("\n")
|
|
66
|
-
return `# nifra examples
|
|
66
|
+
return `# nifra examples - index (${examples.length} verified snippets; pass \`query\` to fetch code)\n\nEvery snippet is typechecked against the installed nifra version - prefer these over recalling an API.\n\n${index}`
|
|
67
67
|
}
|
|
68
68
|
const matches = searchExamples(examples, query, limit)
|
|
69
69
|
if (matches.length === 0) {
|
|
70
70
|
return `No examples matched ${JSON.stringify(query)}. Call without a query for the index, or try a broader term (e.g. "auth", "upload", "isr", "client").`
|
|
71
71
|
}
|
|
72
72
|
return matches
|
|
73
|
-
.map((m) => `## ${m.name}
|
|
73
|
+
.map((m) => `## ${m.name} - ${m.topic}\n\n\`\`\`ts\n${m.code}\n\`\`\``)
|
|
74
74
|
.join("\n\n---\n\n")
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** Load the bundled example corpus. Resolves relative to this module so it works from `src/` (repo dev)
|
|
78
|
-
* and `dist/` (published) alike
|
|
78
|
+
* and `dist/` (published) alike - `docs/examples.json` sits at the package root next to both. */
|
|
79
79
|
export async function loadExamplesCorpus(): Promise<Example[] | undefined> {
|
|
80
80
|
const url = new URL("../docs/examples.json", import.meta.url)
|
|
81
81
|
try {
|