@nifrajs/cli 2.12.1 → 2.13.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/dist/check.d.ts.map +1 -1
- package/dist/check.js +2 -5
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +5 -11
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +12 -228
- package/dist/doctor.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/release-verification.d.ts +45 -0
- package/dist/release-verification.d.ts.map +1 -0
- package/dist/release-verification.js +303 -0
- package/dist/release-verification.js.map +1 -0
- package/docs/types.json +3 -3
- package/package.json +9 -9
- package/src/check.ts +2 -6
- package/src/cli.ts +12 -1
- package/src/doctor.ts +21 -260
- package/src/mcp-http.ts +1 -1
- package/src/release-verification.ts +377 -0
package/src/cli.ts
CHANGED
|
@@ -104,6 +104,8 @@ Usage:
|
|
|
104
104
|
nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
|
|
105
105
|
server-only imports in routes/). Run as "done"; --json for agents;
|
|
106
106
|
--lints-only skips tsc for a near-instant inner-loop pass.
|
|
107
|
+
nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
|
|
108
|
+
full build, test, coverage, corpus, consumer, and cross-runtime gate.
|
|
107
109
|
nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
|
|
108
110
|
structured diagnostics.
|
|
109
111
|
nifra sync-manifest Regenerate a committed web server-manifest.ts from routes/ WITHOUT
|
|
@@ -175,7 +177,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
|
|
|
175
177
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
|
|
176
178
|
|
|
177
179
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
178
|
-
const CLI_VERSION = "2.
|
|
180
|
+
const CLI_VERSION = "2.13.0"
|
|
179
181
|
|
|
180
182
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
181
183
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
@@ -649,6 +651,15 @@ async function main(): Promise<void> {
|
|
|
649
651
|
process.exitCode = 1
|
|
650
652
|
return
|
|
651
653
|
}
|
|
654
|
+
if (command === "verify") {
|
|
655
|
+
const { runReleaseVerification } = await import("./release-verification.ts")
|
|
656
|
+
const ok = await runReleaseVerification(process.cwd(), {
|
|
657
|
+
mode: argv.includes("--release") ? "release" : "default",
|
|
658
|
+
json: argv.includes("--json"),
|
|
659
|
+
})
|
|
660
|
+
if (!ok) process.exitCode = 1
|
|
661
|
+
return
|
|
662
|
+
}
|
|
652
663
|
if (command === "fix") {
|
|
653
664
|
const codeIndex = argv.indexOf("--code")
|
|
654
665
|
const code = codeIndex === -1 ? undefined : argv[codeIndex + 1]
|
package/src/doctor.ts
CHANGED
|
@@ -17,9 +17,16 @@
|
|
|
17
17
|
* undeclared import there fails a clean `bun install --frozen-lockfile` build just the same.
|
|
18
18
|
*/
|
|
19
19
|
import type { Dirent } from "node:fs"
|
|
20
|
-
import {
|
|
20
|
+
import { readdir, stat } from "node:fs/promises"
|
|
21
21
|
import { builtinModules } from "node:module"
|
|
22
22
|
import { dirname, join, relative, sep } from "node:path"
|
|
23
|
+
import {
|
|
24
|
+
collectIdentityParity,
|
|
25
|
+
displayPath,
|
|
26
|
+
type IdentityParityCopy,
|
|
27
|
+
type IdentityParityFinding,
|
|
28
|
+
resolvedInstalledCopy,
|
|
29
|
+
} from "@nifrajs/web/internal/parity"
|
|
23
30
|
import { codePositionMask, type SourceFinding, stripComments, walkSource } from "./check.ts"
|
|
24
31
|
import { collectPipelineReport, type PipelineReport } from "./pipeline-report.ts"
|
|
25
32
|
import { type ResolvedTarget, resolveTarget } from "./port.ts"
|
|
@@ -178,17 +185,8 @@ export interface DoctorReadiness {
|
|
|
178
185
|
readonly items: readonly DoctorReadinessItem[]
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
export interface DuplicateInstallCopy {
|
|
182
|
-
|
|
183
|
-
readonly version: string
|
|
184
|
-
/** Physical package directory, relative to the doctor root when possible. */
|
|
185
|
-
readonly path: string
|
|
186
|
-
/** Workspace package roots whose normal Node/Bun resolution selects this copy. */
|
|
187
|
-
readonly importers: readonly string[]
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export interface DuplicateInstallFinding {
|
|
191
|
-
readonly package: string
|
|
188
|
+
export interface DuplicateInstallCopy extends IdentityParityCopy {}
|
|
189
|
+
export interface DuplicateInstallFinding extends Omit<IdentityParityFinding, "copies"> {
|
|
192
190
|
readonly copies: readonly DuplicateInstallCopy[]
|
|
193
191
|
}
|
|
194
192
|
|
|
@@ -229,8 +227,6 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
229
227
|
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
230
228
|
|
|
231
229
|
const MAX_WORKSPACE_IMPORTERS = 2_048
|
|
232
|
-
const IDENTITY_SENSITIVE_PACKAGES = new Set(["@nifrajs/core", "react", "react-dom"])
|
|
233
|
-
|
|
234
230
|
function workspacePatterns(pkg: Record<string, unknown>): string[] {
|
|
235
231
|
const raw = pkg.workspaces
|
|
236
232
|
const entries = Array.isArray(raw)
|
|
@@ -343,251 +339,25 @@ const scopeForFile = (scopes: readonly DoctorPackageScope[], file: string): Doct
|
|
|
343
339
|
(file === scope.relativeRoot || file.startsWith(`${scope.relativeRoot}/`)),
|
|
344
340
|
) ?? (scopes.find((scope) => scope.relativeRoot === "") as DoctorPackageScope)
|
|
345
341
|
|
|
346
|
-
|
|
347
|
-
const targets = new Set<string>()
|
|
348
|
-
for (const field of DEPENDENCY_FIELDS) {
|
|
349
|
-
for (const name of depNames(pkg, field)) {
|
|
350
|
-
if (name.startsWith("@nifrajs/") || IDENTITY_SENSITIVE_PACKAGES.has(name)) targets.add(name)
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return [...targets].sort()
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
async function resolvedInstalledCopy(
|
|
357
|
-
importer: string,
|
|
358
|
-
boundary: string,
|
|
359
|
-
name: string,
|
|
360
|
-
): Promise<{ path: string; version: string } | undefined> {
|
|
361
|
-
const parts = name.split("/")
|
|
362
|
-
for (let dir = importer; ; dir = dirname(dir)) {
|
|
363
|
-
const packageDir = join(dir, "node_modules", ...parts)
|
|
364
|
-
const meta = await readJson(join(packageDir, "package.json"))
|
|
365
|
-
if (meta !== undefined) {
|
|
366
|
-
try {
|
|
367
|
-
return {
|
|
368
|
-
path: await realpath(packageDir),
|
|
369
|
-
version:
|
|
370
|
-
typeof meta.version === "string" && meta.version.length > 0 ? meta.version : "unknown",
|
|
371
|
-
}
|
|
372
|
-
} catch {
|
|
373
|
-
return undefined
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
if (dir === boundary) return undefined
|
|
377
|
-
const parent = dirname(dir)
|
|
378
|
-
if (parent === dir) return undefined
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const MAX_LINKED_PACKAGES = 64
|
|
383
|
-
const MAX_LINK_PROBES = 4_096
|
|
384
|
-
|
|
385
|
-
/** Is `path` inside `root` (or `root` itself)? Both must already be real paths. */
|
|
386
|
-
const isInside = (root: string, path: string): boolean =>
|
|
387
|
-
path === root || path.startsWith(root.endsWith(sep) ? root : `${root}${sep}`)
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* The repo boundary for a linked package: the nearest ancestor holding a `.git`, so the upward
|
|
391
|
-
* `node_modules` walk inside a sibling checkout stops where that checkout does instead of climbing
|
|
392
|
-
* into a shared parent directory. Falls back to the package directory itself.
|
|
393
|
-
*/
|
|
394
|
-
async function linkedRepoBoundary(packageRoot: string): Promise<string> {
|
|
395
|
-
let dir = packageRoot
|
|
396
|
-
for (let depth = 0; depth < 8; depth++) {
|
|
397
|
-
if (await pathExists(join(dir, ".git"))) return dir
|
|
398
|
-
const parent = dirname(dir)
|
|
399
|
-
if (parent === dir) break
|
|
400
|
-
dir = parent
|
|
401
|
-
}
|
|
402
|
-
return packageRoot
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Dependency roots that are symlinks OUT of the scanned workspace - a sibling checkout linked in with
|
|
407
|
-
* `link:`/`file:`/`npm link`. Their own `node_modules` is a second physical install root, so a shared
|
|
408
|
-
* package (`@nifrajs/core`, React) resolves to a different directory there than it does here. That is
|
|
409
|
-
* a real duplicate: module identity is path-based, so registries, symbols, and hooks do not match
|
|
410
|
-
* across the two copies - the exact split that reads as "my types collapsed for no reason".
|
|
411
|
-
*
|
|
412
|
-
* One level deep and bounded: links inside a linked package are not followed.
|
|
413
|
-
*/
|
|
414
|
-
async function linkedPackageRoots(
|
|
415
|
-
importerRoots: readonly string[],
|
|
416
|
-
manifests: readonly Record<string, unknown>[],
|
|
417
|
-
scanRoot: string,
|
|
418
|
-
): Promise<string[]> {
|
|
419
|
-
const realScanRoot = await realpath(scanRoot).catch(() => scanRoot)
|
|
420
|
-
const found = new Map<string, true>()
|
|
421
|
-
let probes = 0
|
|
422
|
-
for (const [index, importerRoot] of importerRoots.entries()) {
|
|
423
|
-
const pkg = manifests[index]
|
|
424
|
-
if (pkg === undefined) continue
|
|
425
|
-
const names = new Set<string>()
|
|
426
|
-
for (const field of DEPENDENCY_FIELDS) for (const name of depNames(pkg, field)) names.add(name)
|
|
427
|
-
for (const name of names) {
|
|
428
|
-
if (probes++ >= MAX_LINK_PROBES || found.size >= MAX_LINKED_PACKAGES) return [...found.keys()]
|
|
429
|
-
const candidate = join(importerRoot, "node_modules", ...name.split("/"))
|
|
430
|
-
// `lstat` first: on macOS `realpath` also rewrites symlinked ANCESTORS (`/tmp` ->
|
|
431
|
-
// `/private/tmp`), which would read as a link out of the workspace for every dependency.
|
|
432
|
-
const link = await lstat(candidate).catch(() => undefined)
|
|
433
|
-
if (link === undefined || !link.isSymbolicLink()) continue
|
|
434
|
-
const resolved = await realpath(candidate).catch(() => undefined)
|
|
435
|
-
if (resolved === undefined || isInside(realScanRoot, resolved)) continue
|
|
436
|
-
found.set(resolved, true)
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return [...found.keys()]
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const displayPath = (cwd: string, path: string): string => {
|
|
443
|
-
const rel = relative(cwd, path)
|
|
444
|
-
return rel === "" ? "." : rel
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Resolve the workspace root that actually governs `cwd`, or `undefined` when there is none.
|
|
449
|
-
*
|
|
450
|
-
* Everything below is anchored to a package that declares `workspaces`. In a monorepo you run
|
|
451
|
-
* `nifra check` from the app - `apps/web` - and THAT manifest declares nothing, so the scan collapses
|
|
452
|
-
* to the app itself and the sibling package holding the second copy is never probed. Running from an
|
|
453
|
-
* app subdirectory is the normal case, which made this the configuration the check was blind in.
|
|
454
|
-
*
|
|
455
|
-
* An ancestor is adopted only when its `workspaces` patterns genuinely match `cwd`. A parent
|
|
456
|
-
* directory that happens to contain a manifest is not automatically this project's root, and adopting
|
|
457
|
-
* one would silently widen the scan to an unrelated tree. The walk also stops at a `.git` boundary
|
|
458
|
-
* (checked after the manifest, so a repo root that IS the workspace root still counts) and at the
|
|
459
|
-
* filesystem root.
|
|
460
|
-
*/
|
|
461
|
-
/** Whether `path` exists at all. `stat`, not a file read: a `.git` is a directory in a normal
|
|
462
|
-
* checkout and a FILE in a worktree or submodule, and both mark the same boundary. */
|
|
463
|
-
const pathExists = (path: string): Promise<boolean> =>
|
|
464
|
-
stat(path).then(
|
|
465
|
-
() => true,
|
|
466
|
-
() => false,
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
async function findWorkspaceRoot(
|
|
470
|
-
cwd: string,
|
|
471
|
-
): Promise<{ root: string; package: Record<string, unknown> } | undefined> {
|
|
472
|
-
let dir = dirname(cwd)
|
|
473
|
-
for (;;) {
|
|
474
|
-
const manifestPath = join(dir, "package.json")
|
|
475
|
-
const manifest = await readJson(manifestPath)
|
|
476
|
-
if (manifest !== undefined) {
|
|
477
|
-
const patterns = workspacePatterns(manifest)
|
|
478
|
-
if (patterns.length > 0 && (await patternsCover(dir, patterns, cwd))) {
|
|
479
|
-
return { root: dir, package: manifest }
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
// A `.git` here means we have reached this project's boundary; anything above belongs to a
|
|
483
|
-
// different repo (or a parent checkout) and must not be adopted.
|
|
484
|
-
if (await pathExists(join(dir, ".git"))) return undefined
|
|
485
|
-
const parent = dirname(dir)
|
|
486
|
-
if (parent === dir) return undefined
|
|
487
|
-
dir = parent
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/** Whether any of `root`'s workspace patterns resolves to a manifest inside `target`'s package. */
|
|
492
|
-
async function patternsCover(root: string, patterns: string[], target: string): Promise<boolean> {
|
|
493
|
-
for (const pattern of patterns) {
|
|
494
|
-
const packagePattern = `${pattern.replace(/\/$/, "")}/package.json`
|
|
495
|
-
for await (const rel of new Bun.Glob(packagePattern).scan({ cwd: root, dot: false })) {
|
|
496
|
-
if (rel.split(/[\\/]/).includes("node_modules")) continue
|
|
497
|
-
if (dirname(join(root, rel)) === target) return true
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return false
|
|
501
|
-
}
|
|
342
|
+
/* Identity-sensitive install resolution is owned by @nifrajs/web/internal/parity. */
|
|
502
343
|
|
|
503
344
|
/** Find identity-sensitive dependencies that resolve to multiple physical directories. Two copies at
|
|
504
345
|
* the same version still fail: module identity (React hooks, Nifra symbols/registries) is path-based.
|
|
505
346
|
*
|
|
506
347
|
* `cwd` is where the user ran the check; the scan is anchored at the workspace root above it (see
|
|
507
|
-
*
|
|
508
|
-
* to `cwd` so the findings read the way the user's terminal does. */
|
|
348
|
+
* The shared parity seam resolves the governing workspace root, while paths are still REPORTED
|
|
349
|
+
* relative to `cwd` so the findings read the way the user's terminal does. */
|
|
509
350
|
export async function collectDuplicateInstalls(
|
|
510
351
|
cwd: string,
|
|
511
352
|
rootPackage: Record<string, unknown>,
|
|
512
353
|
): Promise<DuplicateInstallFinding[]> {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
copy: { path: string; version: string },
|
|
520
|
-
importer: string,
|
|
521
|
-
): void => {
|
|
522
|
-
let copies = byPackage.get(name)
|
|
523
|
-
if (copies === undefined) {
|
|
524
|
-
copies = new Map()
|
|
525
|
-
byPackage.set(name, copies)
|
|
526
|
-
}
|
|
527
|
-
const entry = copies.get(copy.path) ?? { version: copy.version, importers: new Set<string>() }
|
|
528
|
-
entry.importers.add(importer)
|
|
529
|
-
copies.set(copy.path, entry)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const targets = new Set<string>()
|
|
533
|
-
for (const importer of importers) {
|
|
534
|
-
for (const name of duplicateTargets(importer.package)) {
|
|
535
|
-
if (importer.package.name === name) continue
|
|
536
|
-
targets.add(name)
|
|
537
|
-
// Boundary is the SCAN root, not `cwd`: an importer can now sit beside or above the directory
|
|
538
|
-
// the user ran from, and stopping the upward walk at `cwd` would abandon it before reaching the
|
|
539
|
-
// hoisted copy it actually resolves.
|
|
540
|
-
const copy = await resolvedInstalledCopy(importer.root, scanRoot, name)
|
|
541
|
-
if (copy !== undefined) record(name, copy, displayPath(cwd, importer.root))
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Probe the workspace root for every target too, even when the root manifest doesn't declare it.
|
|
546
|
-
// A hoisted root copy is reachable from any package that lacks its own nested one, so leaving it
|
|
547
|
-
// out hides the very split it should catch: every declaring package nested onto one copy while the
|
|
548
|
-
// root resolved a different one, which reads as a single copy when only declarers are consulted.
|
|
549
|
-
for (const name of targets) {
|
|
550
|
-
const copy = await resolvedInstalledCopy(scanRoot, scanRoot, name)
|
|
551
|
-
if (copy !== undefined) record(name, copy, displayPath(cwd, scanRoot))
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Sibling checkouts linked in (`link:`/`file:`/`npm link`) install their own copies. Their
|
|
555
|
-
// resolution is what the runtime and `tsc` actually use for code that crosses the link, so a split
|
|
556
|
-
// there is as real as one inside this workspace - and invisible to a scan bounded by the workspace.
|
|
557
|
-
const linkedRoots = await linkedPackageRoots(
|
|
558
|
-
[...importers.map((importer) => importer.root), scanRoot],
|
|
559
|
-
[...importers.map((importer) => importer.package), workspace?.package ?? rootPackage],
|
|
560
|
-
scanRoot,
|
|
354
|
+
const result = await collectIdentityParity(cwd, rootPackage, { useWorkspaceRoot: true })
|
|
355
|
+
return result.findings.map(
|
|
356
|
+
(finding): DuplicateInstallFinding => ({
|
|
357
|
+
...finding,
|
|
358
|
+
copies: finding.copies.map((copy): DuplicateInstallCopy => ({ ...copy })),
|
|
359
|
+
}),
|
|
561
360
|
)
|
|
562
|
-
for (const linkedRoot of linkedRoots) {
|
|
563
|
-
const linkedPackage = await readJson(join(linkedRoot, "package.json"))
|
|
564
|
-
const boundary = await linkedRepoBoundary(linkedRoot)
|
|
565
|
-
const linkedTargets = new Set([
|
|
566
|
-
...targets,
|
|
567
|
-
...(linkedPackage === undefined ? [] : duplicateTargets(linkedPackage)),
|
|
568
|
-
])
|
|
569
|
-
for (const name of linkedTargets) {
|
|
570
|
-
if (linkedPackage?.name === name) continue
|
|
571
|
-
const copy = await resolvedInstalledCopy(linkedRoot, boundary, name)
|
|
572
|
-
if (copy !== undefined) record(name, copy, displayPath(cwd, linkedRoot))
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const findings: DuplicateInstallFinding[] = []
|
|
577
|
-
for (const [name, copies] of [...byPackage.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
578
|
-
if (copies.size < 2) continue
|
|
579
|
-
findings.push({
|
|
580
|
-
package: name,
|
|
581
|
-
copies: [...copies.entries()]
|
|
582
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
583
|
-
.map(([path, copy]) => ({
|
|
584
|
-
version: copy.version,
|
|
585
|
-
path: displayPath(cwd, path),
|
|
586
|
-
importers: [...copy.importers].sort(),
|
|
587
|
-
})),
|
|
588
|
-
})
|
|
589
|
-
}
|
|
590
|
-
return findings
|
|
591
361
|
}
|
|
592
362
|
|
|
593
363
|
/** A workspace-linked dependency whose build artifact is older than its source. */
|
|
@@ -1131,18 +901,9 @@ export async function runDoctor(
|
|
|
1131
901
|
for (const copy of finding.copies) {
|
|
1132
902
|
console.log(` ${copy.version} at ${copy.path} ← ${copy.importers.join(", ")}`)
|
|
1133
903
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
" ↳ a second @nifrajs/core breaks module identity: `typeof backend` collapses to `any`",
|
|
1137
|
-
)
|
|
1138
|
-
console.log(
|
|
1139
|
-
" at `.merge()` and route-type inference silently degrades - usually that error's root cause.",
|
|
1140
|
-
)
|
|
1141
|
-
}
|
|
904
|
+
console.log(` ${finding.explanation}`)
|
|
905
|
+
console.log(` fix: ${finding.remediation}`)
|
|
1142
906
|
}
|
|
1143
|
-
console.log(
|
|
1144
|
-
"\n Align dependency ranges and reinstall from the workspace root; doctor never deletes installs.",
|
|
1145
|
-
)
|
|
1146
907
|
}
|
|
1147
908
|
if (result.readiness?.strict === true && result.readiness.ok === false) {
|
|
1148
909
|
console.log("\n✗ production readiness is incomplete under --strict")
|
package/src/mcp-http.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
|
|
|
29
29
|
export type { TypeEntry } from "./types-search.ts"
|
|
30
30
|
|
|
31
31
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
32
|
-
const VERSION = "2.
|
|
32
|
+
const VERSION = "2.13.0"
|
|
33
33
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION }
|
|
34
34
|
// Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
|
|
35
35
|
const docsHealth = (tools: McpTool[]): string =>
|