@nifrajs/cli 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/check.d.ts +100 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +690 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +208 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs-search.d.ts +30 -0
- package/dist/docs-search.d.ts.map +1 -0
- package/dist/docs-search.js +94 -0
- package/dist/docs-search.js.map +1 -0
- package/dist/doctor.d.ts +52 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +291 -0
- package/dist/doctor.js.map +1 -0
- package/dist/examples.d.ts +30 -0
- package/dist/examples.d.ts.map +1 -0
- package/dist/examples.js +70 -0
- package/dist/examples.js.map +1 -0
- package/dist/introspect.d.ts +64 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +243 -0
- package/dist/introspect.js.map +1 -0
- package/dist/load.d.ts +51 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +70 -0
- package/dist/load.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts +13 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -0
- package/dist/mcp-docs-tools.js +63 -0
- package/dist/mcp-docs-tools.js.map +1 -0
- package/dist/mcp-http.d.ts +43 -0
- package/dist/mcp-http.d.ts.map +1 -0
- package/dist/mcp-http.js +136 -0
- package/dist/mcp-http.js.map +1 -0
- package/dist/mcp-protocol.d.ts +97 -0
- package/dist/mcp-protocol.d.ts.map +1 -0
- package/dist/mcp-protocol.js +248 -0
- package/dist/mcp-protocol.js.map +1 -0
- package/dist/mcp-render.d.ts +17 -0
- package/dist/mcp-render.d.ts.map +1 -0
- package/dist/mcp-render.js +73 -0
- package/dist/mcp-render.js.map +1 -0
- package/dist/mcp-run.d.ts +24 -0
- package/dist/mcp-run.d.ts.map +1 -0
- package/dist/mcp-run.js +142 -0
- package/dist/mcp-run.js.map +1 -0
- package/dist/mcp-ws.d.ts +26 -0
- package/dist/mcp-ws.d.ts.map +1 -0
- package/dist/mcp-ws.js +258 -0
- package/dist/mcp-ws.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +712 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openapi-tool.d.ts +6 -0
- package/dist/openapi-tool.d.ts.map +1 -0
- package/dist/openapi-tool.js +48 -0
- package/dist/openapi-tool.js.map +1 -0
- package/dist/scaffold.d.ts +35 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +140 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/search-terms.d.ts +17 -0
- package/dist/search-terms.d.ts.map +1 -0
- package/dist/search-terms.js +82 -0
- package/dist/search-terms.js.map +1 -0
- package/dist/test-tool.d.ts +29 -0
- package/dist/test-tool.d.ts.map +1 -0
- package/dist/test-tool.js +150 -0
- package/dist/test-tool.js.map +1 -0
- package/docs/examples.json +314 -0
- package/docs/llms-full.txt +4325 -0
- package/package.json +58 -0
- package/src/check.ts +790 -0
- package/src/cli.ts +237 -0
- package/src/docs-search.ts +111 -0
- package/src/doctor.ts +349 -0
- package/src/examples.ts +89 -0
- package/src/introspect.ts +284 -0
- package/src/load.ts +129 -0
- package/src/mcp-docs-tools.ts +72 -0
- package/src/mcp-http.ts +155 -0
- package/src/mcp-protocol.ts +326 -0
- package/src/mcp-render.ts +78 -0
- package/src/mcp-run.ts +153 -0
- package/src/mcp-ws.ts +328 -0
- package/src/mcp.ts +863 -0
- package/src/openapi-tool.ts +51 -0
- package/src/scaffold.ts +164 -0
- package/src/search-terms.ts +89 -0
- package/src/test-tool.ts +181 -0
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra doctor` — catches the Bun-workspace footgun the typed-client lint can't: a package that's
|
|
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" — false confidence — yet `tsc` fails
|
|
5
|
+
* and a fresh or standalone install can't resolve it. doctor diffs every bare import specifier against
|
|
6
|
+
* the package's declared dependencies and flags the gap, with a stable `--json` shape for agents/CI.
|
|
7
|
+
*
|
|
8
|
+
* Scope is intentionally per-package: it checks the `package.json` at `cwd`, because a dependency must be
|
|
9
|
+
* declared by the package that imports it (for `tsc` and for that package to install on its own) even
|
|
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 — none of them are npm deps.
|
|
12
|
+
*/
|
|
13
|
+
import { builtinModules } from "node:module"
|
|
14
|
+
import { dirname, join } from "node:path"
|
|
15
|
+
import { type SourceFinding, stripComments, walkSource } from "./check.ts"
|
|
16
|
+
|
|
17
|
+
// Runtime-provided modules that are never an npm dependency: Node core (bare + `node:` form) and Bun's
|
|
18
|
+
// own `bun` module. `node:`/`bun:`-prefixed specifiers are filtered in packageOf by prefix.
|
|
19
|
+
const BUILTINS: ReadonlySet<string> = new Set<string>([
|
|
20
|
+
...builtinModules,
|
|
21
|
+
...builtinModules.map((m) => `node:${m}`),
|
|
22
|
+
"bun",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
// The four specifier-bearing forms: static import/re-export with a source, side-effect import, dynamic
|
|
26
|
+
// import, and CJS require. Anchored with `(?<![.\w$])` so `myimport`/`.import`/`foorequire` never match.
|
|
27
|
+
// Comments are stripped before these run (see stripComments) — else a doc-comment usage example would be
|
|
28
|
+
// flagged as a real import.
|
|
29
|
+
const IMPORT_PATTERNS: readonly RegExp[] = [
|
|
30
|
+
/(?<![.\w$])(?:import|export)\b[^'"]*?\bfrom\s*['"]([^'"]+)['"]/g,
|
|
31
|
+
/(?<![.\w$])import\s+['"]([^'"]+)['"]/g,
|
|
32
|
+
/(?<![.\w$])import\s*\(\s*['"]([^'"]+)['"]/g,
|
|
33
|
+
/(?<![.\w$])require\s*\(\s*['"]([^'"]+)['"]/g,
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const NPM_PACKAGE_NAME = /^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/
|
|
37
|
+
const DEPENDENCY_FIELDS = [
|
|
38
|
+
"dependencies",
|
|
39
|
+
"devDependencies",
|
|
40
|
+
"peerDependencies",
|
|
41
|
+
"optionalDependencies",
|
|
42
|
+
] as const
|
|
43
|
+
|
|
44
|
+
const isNpmPackageName = (name: string): boolean =>
|
|
45
|
+
NPM_PACKAGE_NAME.test(name) &&
|
|
46
|
+
name.split("/").every((part) => part !== "" && part !== "." && part !== "..")
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve an import specifier to the npm package name it would install as, or `undefined` when it isn't
|
|
50
|
+
* an npm dependency at all (a relative/absolute path, a runtime builtin, or a malformed scope like the
|
|
51
|
+
* `@/…` path-alias convention). `@scope/name/sub` → `@scope/name`; `name/sub` → `name`.
|
|
52
|
+
*/
|
|
53
|
+
export function packageOf(spec: string): string | undefined {
|
|
54
|
+
if (spec === "" || spec.startsWith(".") || spec.startsWith("/")) return undefined
|
|
55
|
+
if (spec.startsWith("node:") || spec.startsWith("bun:")) return undefined
|
|
56
|
+
if (spec.startsWith("@")) {
|
|
57
|
+
const [scope, name] = spec.split("/")
|
|
58
|
+
if (!scope || scope === "@" || !name) return undefined // `@/alias`, `@foo` — not a real package
|
|
59
|
+
const pkg = `${scope}/${name}`
|
|
60
|
+
return isNpmPackageName(pkg) ? pkg : undefined
|
|
61
|
+
}
|
|
62
|
+
const name = spec.split("/")[0] as string
|
|
63
|
+
if (!isNpmPackageName(name)) return undefined
|
|
64
|
+
return BUILTINS.has(name) ? undefined : name
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build a predicate matching tsconfig `paths` aliases (e.g. `@/*`, `~/utils`), which resolve to local
|
|
68
|
+
* source, not npm packages — so doctor must not flag them. */
|
|
69
|
+
export function aliasMatcher(
|
|
70
|
+
paths: Readonly<Record<string, unknown>> | undefined,
|
|
71
|
+
): (spec: string) => boolean {
|
|
72
|
+
const prefixes = Object.keys(paths ?? {})
|
|
73
|
+
.map((k) => k.replace(/\/?\*$/, ""))
|
|
74
|
+
.filter((p) => p !== "")
|
|
75
|
+
return (spec) => prefixes.some((p) => spec === p || spec.startsWith(`${p}/`))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Scan one file for bare imports whose resolved package is neither `declared` nor a path `alias`. Pure +
|
|
80
|
+
* line-accurate. Deduped per (package, line): the `snippet` carries the undeclared package name.
|
|
81
|
+
*/
|
|
82
|
+
export function scanUndeclaredImports(
|
|
83
|
+
file: string,
|
|
84
|
+
content: string,
|
|
85
|
+
declared: ReadonlySet<string>,
|
|
86
|
+
isAlias: (spec: string) => boolean,
|
|
87
|
+
): SourceFinding[] {
|
|
88
|
+
const out: SourceFinding[] = []
|
|
89
|
+
const seen = new Set<string>()
|
|
90
|
+
const code = stripComments(content) // so doc-comment usage examples aren't read as real imports
|
|
91
|
+
for (const rx of IMPORT_PATTERNS) {
|
|
92
|
+
rx.lastIndex = 0
|
|
93
|
+
for (let m = rx.exec(code); m !== null; m = rx.exec(code)) {
|
|
94
|
+
const spec = m[1] ?? ""
|
|
95
|
+
if (isAlias(spec)) continue
|
|
96
|
+
const pkg = packageOf(spec)
|
|
97
|
+
if (pkg === undefined || declared.has(pkg)) continue
|
|
98
|
+
const line = content.slice(0, m.index).split("\n").length
|
|
99
|
+
const key = `${pkg}@${line}`
|
|
100
|
+
if (seen.has(key)) continue
|
|
101
|
+
seen.add(key)
|
|
102
|
+
out.push({ file, line, snippet: pkg })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out.sort((a, b) => a.line - b.line || a.snippet.localeCompare(b.snippet))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface DoctorFinding {
|
|
109
|
+
readonly file: string
|
|
110
|
+
readonly line: number
|
|
111
|
+
/** The undeclared package name — add it to `package.json` dependencies. */
|
|
112
|
+
readonly package: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DoctorResult {
|
|
116
|
+
readonly ok: boolean
|
|
117
|
+
/** `false` when no `package.json` was found at cwd — doctor can't run (reported, not a crash). */
|
|
118
|
+
readonly ran: boolean
|
|
119
|
+
readonly findings: readonly DoctorFinding[]
|
|
120
|
+
/** Dependencies written by `--auto-fix` / MCP `autoFix:true`. */
|
|
121
|
+
readonly fixed?: readonly DoctorAppliedFix[]
|
|
122
|
+
/** Findings that were safe to report but not safe to write automatically. */
|
|
123
|
+
readonly skippedFixes?: readonly DoctorSkippedFix[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DoctorAppliedFix {
|
|
127
|
+
readonly package: string
|
|
128
|
+
readonly field: "dependencies"
|
|
129
|
+
readonly version: string
|
|
130
|
+
readonly source: "ancestor-package-json" | "installed-package-json"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface DoctorSkippedFix {
|
|
134
|
+
readonly package: string
|
|
135
|
+
readonly reason: string
|
|
136
|
+
readonly command: readonly string[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Read + parse a JSON file, or `undefined` if it's missing/unparseable (doctor degrades, never throws). */
|
|
140
|
+
async function readJson(path: string): Promise<Record<string, unknown> | undefined> {
|
|
141
|
+
try {
|
|
142
|
+
const text = await Bun.file(path).text()
|
|
143
|
+
const parsed = JSON.parse(text) as unknown
|
|
144
|
+
return typeof parsed === "object" && parsed !== null
|
|
145
|
+
? (parsed as Record<string, unknown>)
|
|
146
|
+
: undefined
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const depNames = (pkg: Record<string, unknown>, field: string): string[] => {
|
|
153
|
+
const deps = pkg[field]
|
|
154
|
+
return typeof deps === "object" && deps !== null
|
|
155
|
+
? Object.keys(deps as Record<string, unknown>)
|
|
156
|
+
: []
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
160
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
161
|
+
|
|
162
|
+
function depRecord(
|
|
163
|
+
pkg: Record<string, unknown>,
|
|
164
|
+
field: string,
|
|
165
|
+
): Record<string, unknown> | undefined {
|
|
166
|
+
const deps = pkg[field]
|
|
167
|
+
return isRecord(deps) ? deps : undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function dependencySpec(pkg: Record<string, unknown>, name: string): string | undefined {
|
|
171
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
172
|
+
const spec = depRecord(pkg, field)?.[name]
|
|
173
|
+
if (typeof spec === "string" && spec.length > 0) return spec
|
|
174
|
+
}
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function ancestorDependencySpec(cwd: string, name: string): Promise<string | undefined> {
|
|
179
|
+
for (let dir = dirname(cwd); ; dir = dirname(dir)) {
|
|
180
|
+
const pkg = await readJson(join(dir, "package.json"))
|
|
181
|
+
if (pkg !== undefined) {
|
|
182
|
+
const spec = dependencySpec(pkg, name)
|
|
183
|
+
if (spec !== undefined) return spec
|
|
184
|
+
}
|
|
185
|
+
const parent = dirname(dir)
|
|
186
|
+
if (parent === dir) return undefined
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function installedPackageSpec(cwd: string, name: string): Promise<string | undefined> {
|
|
191
|
+
if (!isNpmPackageName(name)) return undefined
|
|
192
|
+
const parts = name.split("/")
|
|
193
|
+
for (let dir = cwd; ; dir = dirname(dir)) {
|
|
194
|
+
const meta = await readJson(join(dir, "node_modules", ...parts, "package.json"))
|
|
195
|
+
if (typeof meta?.version === "string" && meta.version.length > 0) {
|
|
196
|
+
return `^${meta.version}`
|
|
197
|
+
}
|
|
198
|
+
const parent = dirname(dir)
|
|
199
|
+
if (parent === dir) return undefined
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function inferDependencyFix(
|
|
204
|
+
cwd: string,
|
|
205
|
+
name: string,
|
|
206
|
+
): Promise<Omit<DoctorAppliedFix, "package" | "field"> | undefined> {
|
|
207
|
+
const ancestorSpec = await ancestorDependencySpec(cwd, name)
|
|
208
|
+
if (ancestorSpec !== undefined) {
|
|
209
|
+
return { version: ancestorSpec, source: "ancestor-package-json" }
|
|
210
|
+
}
|
|
211
|
+
const installedSpec = await installedPackageSpec(cwd, name)
|
|
212
|
+
return installedSpec === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: { version: installedSpec, source: "installed-package-json" }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Run doctor against the project at `cwd`: diff source imports vs declared deps. */
|
|
218
|
+
export async function collectDoctorResult(cwd: string): Promise<DoctorResult> {
|
|
219
|
+
const pkg = await readJson(join(cwd, "package.json"))
|
|
220
|
+
if (pkg === undefined) return { ok: true, ran: false, findings: [] }
|
|
221
|
+
|
|
222
|
+
const declared = new Set<string>()
|
|
223
|
+
if (typeof pkg.name === "string") declared.add(pkg.name) // a package may import its own name (exports map)
|
|
224
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
225
|
+
for (const name of depNames(pkg, field)) declared.add(name)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const tsconfig = await readJson(join(cwd, "tsconfig.json"))
|
|
229
|
+
const compilerOptions = tsconfig?.compilerOptions as
|
|
230
|
+
| { paths?: Record<string, unknown> }
|
|
231
|
+
| undefined
|
|
232
|
+
const isAlias = aliasMatcher(compilerOptions?.paths)
|
|
233
|
+
|
|
234
|
+
const findings: DoctorFinding[] = []
|
|
235
|
+
await walkSource(cwd, (rel, content) => {
|
|
236
|
+
for (const f of scanUndeclaredImports(rel, content, declared, isAlias)) {
|
|
237
|
+
findings.push({ file: f.file, line: f.line, package: f.snippet })
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
241
|
+
return { ok: findings.length === 0, ran: true, findings }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Safely add undeclared imports to package.json when the version can be inferred without network I/O. */
|
|
245
|
+
export async function applyDoctorAutoFix(cwd: string): Promise<DoctorResult> {
|
|
246
|
+
const before = await collectDoctorResult(cwd)
|
|
247
|
+
if (!before.ran || before.findings.length === 0) return before
|
|
248
|
+
|
|
249
|
+
const pkgPath = join(cwd, "package.json")
|
|
250
|
+
const pkg = await readJson(pkgPath)
|
|
251
|
+
if (pkg === undefined) return before
|
|
252
|
+
const dependencies = pkg.dependencies
|
|
253
|
+
if (dependencies !== undefined && !isRecord(dependencies)) {
|
|
254
|
+
const skippedFixes = [...new Set(before.findings.map((f) => f.package))].sort().map(
|
|
255
|
+
(name): DoctorSkippedFix => ({
|
|
256
|
+
package: name,
|
|
257
|
+
reason: "`dependencies` exists but is not an object; refusing to rewrite it automatically",
|
|
258
|
+
command: ["bun", "add", name],
|
|
259
|
+
}),
|
|
260
|
+
)
|
|
261
|
+
return { ...before, skippedFixes }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const deps = (dependencies ?? {}) as Record<string, unknown>
|
|
265
|
+
if (dependencies === undefined) pkg.dependencies = deps
|
|
266
|
+
|
|
267
|
+
const fixed: DoctorAppliedFix[] = []
|
|
268
|
+
const skippedFixes: DoctorSkippedFix[] = []
|
|
269
|
+
for (const name of [...new Set(before.findings.map((f) => f.package))].sort()) {
|
|
270
|
+
if (!isNpmPackageName(name)) {
|
|
271
|
+
skippedFixes.push({
|
|
272
|
+
package: name,
|
|
273
|
+
reason: "package name did not match npm package-name syntax",
|
|
274
|
+
command: ["bun", "add", name],
|
|
275
|
+
})
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
const inferred = await inferDependencyFix(cwd, name)
|
|
279
|
+
if (inferred === undefined) {
|
|
280
|
+
skippedFixes.push({
|
|
281
|
+
package: name,
|
|
282
|
+
reason: "no declared ancestor version or installed package metadata was found locally",
|
|
283
|
+
command: ["bun", "add", name],
|
|
284
|
+
})
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
deps[name] = inferred.version
|
|
288
|
+
fixed.push({ package: name, field: "dependencies", ...inferred })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (fixed.length > 0) await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
292
|
+
const after = fixed.length > 0 ? await collectDoctorResult(cwd) : before
|
|
293
|
+
return {
|
|
294
|
+
...after,
|
|
295
|
+
...(fixed.length > 0 ? { fixed } : {}),
|
|
296
|
+
...(skippedFixes.length > 0 ? { skippedFixes } : {}),
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Run doctor; print a report (`--json` for machine output) and return whether it passed. */
|
|
301
|
+
export async function runDoctor(
|
|
302
|
+
cwd: string,
|
|
303
|
+
opts: { readonly json?: boolean; readonly autoFix?: boolean } = {},
|
|
304
|
+
): Promise<boolean> {
|
|
305
|
+
const result = opts.autoFix ? await applyDoctorAutoFix(cwd) : await collectDoctorResult(cwd)
|
|
306
|
+
if (opts.json) {
|
|
307
|
+
console.log(JSON.stringify(result, null, 2))
|
|
308
|
+
return result.ok
|
|
309
|
+
}
|
|
310
|
+
console.log("nifra doctor\n")
|
|
311
|
+
if (!result.ran) {
|
|
312
|
+
console.log("• no package.json at this directory — nothing to check")
|
|
313
|
+
return true
|
|
314
|
+
}
|
|
315
|
+
if (result.fixed && result.fixed.length > 0) {
|
|
316
|
+
console.log("✓ updated package.json dependencies:")
|
|
317
|
+
for (const f of result.fixed) {
|
|
318
|
+
console.log(` ${f.package}@${f.version} (${f.source})`)
|
|
319
|
+
}
|
|
320
|
+
console.log("")
|
|
321
|
+
}
|
|
322
|
+
if (result.skippedFixes && result.skippedFixes.length > 0) {
|
|
323
|
+
console.log("• not auto-fixed:")
|
|
324
|
+
for (const f of result.skippedFixes) {
|
|
325
|
+
console.log(` ${f.package} — ${f.reason}; run \`${f.command.join(" ")}\``)
|
|
326
|
+
}
|
|
327
|
+
console.log("")
|
|
328
|
+
}
|
|
329
|
+
if (result.ok) {
|
|
330
|
+
console.log("✓ every imported package is declared in package.json")
|
|
331
|
+
return true
|
|
332
|
+
}
|
|
333
|
+
// Group by package so the fix ("add X to dependencies") is stated once with its import sites.
|
|
334
|
+
const byPkg = new Map<string, DoctorFinding[]>()
|
|
335
|
+
for (const f of result.findings) {
|
|
336
|
+
const list = byPkg.get(f.package) ?? []
|
|
337
|
+
list.push(f)
|
|
338
|
+
byPkg.set(f.package, list)
|
|
339
|
+
}
|
|
340
|
+
console.log(`✗ ${byPkg.size} package(s) imported but not declared in package.json:\n`)
|
|
341
|
+
for (const [pkg, sites] of [...byPkg.entries()].sort()) {
|
|
342
|
+
console.log(` ${pkg} — add to dependencies (\`bun add ${pkg}\`)`)
|
|
343
|
+
for (const s of sites) console.log(` ${s.file}:${s.line}`)
|
|
344
|
+
}
|
|
345
|
+
console.log(
|
|
346
|
+
"\nThese resolve at Bun runtime via hoisting/workspace but break `tsc` and a standalone install.",
|
|
347
|
+
)
|
|
348
|
+
return false
|
|
349
|
+
}
|
package/src/examples.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra_example` — verified, copy-pasteable code examples for agents. The corpus is `examples.json`
|
|
3
|
+
* (written into this package by `bun run gen:llms`): the same doc snippets `check:docs` typechecks
|
|
4
|
+
* against the live `@nifrajs/*` API. So every example this returns is GUARANTEED to compile against the
|
|
5
|
+
* installed version — the antidote to an agent hallucinating a framework API from stale training.
|
|
6
|
+
*
|
|
7
|
+
* Keyword-scored over each example's name/topic/slug (curated identifiers, heavily weighted) and its
|
|
8
|
+
* code body. No query → a grouped index of what's available (the cheap discovery call).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { countBodyHits, queryTermGroups, tokenize, tokenSetHas } from "./search-terms.ts"
|
|
12
|
+
|
|
13
|
+
export interface Example {
|
|
14
|
+
readonly name: string
|
|
15
|
+
/** The doc page's title, e.g. "Nifra — Auth & sessions". */
|
|
16
|
+
readonly topic: string
|
|
17
|
+
/** The doc page slug, e.g. "auth" — a short, matchable tag. */
|
|
18
|
+
readonly slug: string
|
|
19
|
+
readonly code: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ScoredExample extends Example {
|
|
23
|
+
readonly score: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Score examples against the query: a term in the slug/topic/name (the curated, searchable labels)
|
|
27
|
+
* is worth far more than a mention in the code body; body hits cap per term so one long snippet can't
|
|
28
|
+
* outweigh a precisely-tagged one. */
|
|
29
|
+
export function searchExamples(
|
|
30
|
+
examples: readonly Example[],
|
|
31
|
+
query: string,
|
|
32
|
+
limit: number,
|
|
33
|
+
): ScoredExample[] {
|
|
34
|
+
const terms = queryTermGroups(query)
|
|
35
|
+
if (terms.length === 0) return []
|
|
36
|
+
const scored: ScoredExample[] = []
|
|
37
|
+
for (const ex of examples) {
|
|
38
|
+
const labelTokens = new Set([...tokenize(ex.slug), ...tokenize(ex.topic), ...tokenize(ex.name)])
|
|
39
|
+
const codeLower = ex.code.toLowerCase()
|
|
40
|
+
let score = 0
|
|
41
|
+
for (const term of terms) {
|
|
42
|
+
if (tokenSetHas(labelTokens, term)) score += 8
|
|
43
|
+
score += countBodyHits(codeLower, term, 5)
|
|
44
|
+
}
|
|
45
|
+
if (score > 0) scored.push({ ...ex, score })
|
|
46
|
+
}
|
|
47
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Render the tool result: matched examples as fenced code blocks, or the grouped index when no query. */
|
|
51
|
+
export function renderExamplesResult(
|
|
52
|
+
examples: readonly Example[],
|
|
53
|
+
query: string | undefined,
|
|
54
|
+
limit: number,
|
|
55
|
+
): string {
|
|
56
|
+
if (query === undefined || query.trim() === "") {
|
|
57
|
+
const byTopic = new Map<string, string[]>()
|
|
58
|
+
for (const ex of examples) {
|
|
59
|
+
const list = byTopic.get(ex.topic) ?? []
|
|
60
|
+
list.push(ex.name)
|
|
61
|
+
byTopic.set(ex.topic, list)
|
|
62
|
+
}
|
|
63
|
+
const index = [...byTopic.entries()]
|
|
64
|
+
.map(([topic, names]) => `- **${topic}**: ${names.join(", ")}`)
|
|
65
|
+
.join("\n")
|
|
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
|
+
}
|
|
68
|
+
const matches = searchExamples(examples, query, limit)
|
|
69
|
+
if (matches.length === 0) {
|
|
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
|
+
}
|
|
72
|
+
return matches
|
|
73
|
+
.map((m) => `## ${m.name} — ${m.topic}\n\n\`\`\`ts\n${m.code}\n\`\`\``)
|
|
74
|
+
.join("\n\n---\n\n")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Load the bundled example corpus. Resolves relative to this module so it works from `src/` (repo dev)
|
|
78
|
+
* and `dist/` (published) alike — `docs/examples.json` sits at the package root next to both. */
|
|
79
|
+
export async function loadExamplesCorpus(): Promise<Example[] | undefined> {
|
|
80
|
+
const url = new URL("../docs/examples.json", import.meta.url)
|
|
81
|
+
try {
|
|
82
|
+
const text = await Bun.file(url).text()
|
|
83
|
+
if (text.length === 0) return undefined
|
|
84
|
+
const parsed = JSON.parse(text) as Example[]
|
|
85
|
+
return Array.isArray(parsed) && parsed.length > 0 ? parsed : undefined
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
}
|