@nifrajs/cli 2.13.0 → 2.14.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/dist/capabilities-tool.d.ts +54 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +146 -18
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +11 -11
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +234 -43
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -413
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +244 -0
- package/dist/command-catalog.d.ts.map +1 -0
- package/dist/command-catalog.js +1014 -0
- package/dist/command-catalog.js.map +1 -0
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +22 -6
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +18 -2
- package/dist/doctor.js.map +1 -1
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +22 -5
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +15 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +23 -9
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +59 -349
- package/dist/mcp.js.map +1 -1
- package/dist/project-facts.d.ts +40 -0
- package/dist/project-facts.d.ts.map +1 -0
- package/dist/project-facts.js +36 -0
- package/dist/project-facts.js.map +1 -0
- package/dist/rules/comment-markers.d.ts +8 -0
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +36 -0
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/index.d.ts +2 -3
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +12 -14
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/security.js +14 -14
- package/dist/rules/security.js.map +1 -1
- package/docs/types.json +27 -14
- package/package.json +9 -9
- package/src/capabilities-tool.ts +229 -21
- package/src/check.ts +285 -59
- package/src/cli.ts +100 -404
- package/src/command-catalog.ts +1415 -0
- package/src/contracts.ts +28 -7
- package/src/doctor.ts +35 -4
- package/src/load.ts +25 -6
- package/src/manifest-tool.ts +43 -12
- package/src/mcp-http.ts +1 -1
- package/src/mcp.ts +82 -414
- package/src/project-facts.ts +86 -0
- package/src/rules/comment-markers.ts +36 -0
- package/src/rules/index.ts +3 -4
- package/src/rules/routes.ts +13 -14
- package/src/rules/security.ts +14 -14
package/src/contracts.ts
CHANGED
|
@@ -41,8 +41,7 @@ async function sha256(value: string): Promise<string> {
|
|
|
41
41
|
return [...digest].map((item) => item.toString(16).padStart(2, "0")).join("")
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function schemaValue(
|
|
45
|
-
const schema = route.schema
|
|
44
|
+
function schemaValue(schema: ReflectedRoute["schema"], side: "request" | "response"): unknown {
|
|
46
45
|
if (schema === undefined) return {}
|
|
47
46
|
if (side === "request") {
|
|
48
47
|
return {
|
|
@@ -64,11 +63,28 @@ function schemaValue(route: ReflectedRoute, side: "request" | "response"): unkno
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
export async function digestRoute(route: ReflectedRoute): Promise<ContractDigest> {
|
|
67
|
-
const request = JSON.stringify(canonical(schemaValue(route, "request")))
|
|
68
|
-
const response = JSON.stringify(canonical(schemaValue(route, "response")))
|
|
66
|
+
const request = JSON.stringify(canonical(schemaValue(route.schema, "request")))
|
|
67
|
+
const response = JSON.stringify(canonical(schemaValue(route.schema, "response")))
|
|
69
68
|
return { request: await sha256(request), response: await sha256(response) }
|
|
70
69
|
}
|
|
71
70
|
|
|
71
|
+
/** The digest a route with NO declared schema produces on both sides - `sha256("{}")`. A lock whose
|
|
72
|
+
* every route hashes to this is guarding nothing: drift detection can only ever compare empty schema to
|
|
73
|
+
* empty schema. Computed through the same canonical/stringify path as {@link digestRoute} so it tracks
|
|
74
|
+
* that path rather than a hard-coded literal. */
|
|
75
|
+
export async function emptySchemaDigest(): Promise<string> {
|
|
76
|
+
return sha256(JSON.stringify(canonical(schemaValue(undefined, "request"))))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Whether every route in `lock` hashes to the empty-schema digest (so no route declares a schema). An
|
|
80
|
+
* empty `routes` map is NOT vacuous in this sense - there is no route whose contract went unguarded. */
|
|
81
|
+
export async function isVacuousLock(lock: ContractsLock): Promise<boolean> {
|
|
82
|
+
const digests = Object.values(lock.routes)
|
|
83
|
+
if (digests.length === 0) return false
|
|
84
|
+
const empty = await emptySchemaDigest()
|
|
85
|
+
return digests.every((d) => d.request === empty && d.response === empty)
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
export async function buildContractsLock(source: unknown): Promise<ContractsLock> {
|
|
73
89
|
const routes = Object.fromEntries(
|
|
74
90
|
await Promise.all(
|
|
@@ -131,11 +147,16 @@ export function parseContractsLock(value: unknown): ContractsLock {
|
|
|
131
147
|
export async function checkContractsLock(
|
|
132
148
|
cwd: string,
|
|
133
149
|
file = DEFAULT_CONTRACTS_LOCK,
|
|
134
|
-
): Promise<{
|
|
150
|
+
): Promise<{
|
|
151
|
+
present: boolean
|
|
152
|
+
vacuous: boolean
|
|
153
|
+
diagnostics: Array<{ route?: string; message: string }>
|
|
154
|
+
}> {
|
|
135
155
|
const path = resolve(cwd, file)
|
|
136
|
-
if (!existsSync(path)) return { present: false, diagnostics: [] }
|
|
156
|
+
if (!existsSync(path)) return { present: false, vacuous: false, diagnostics: [] }
|
|
137
157
|
const parsed = parseContractsLock(JSON.parse(await Bun.file(path).text()))
|
|
138
158
|
const current = await buildContractsLock(await loadBackend(cwd))
|
|
159
|
+
const vacuous = await isVacuousLock(current)
|
|
139
160
|
const diagnostics: Array<{ route?: string; message: string }> = []
|
|
140
161
|
const keys = new Set([...Object.keys(parsed.routes), ...Object.keys(current.routes)])
|
|
141
162
|
for (const key of [...keys].sort()) {
|
|
@@ -153,5 +174,5 @@ export async function checkContractsLock(
|
|
|
153
174
|
})
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
|
-
return { present: true, diagnostics }
|
|
177
|
+
return { present: true, vacuous, diagnostics }
|
|
157
178
|
}
|
package/src/doctor.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolvedInstalledCopy,
|
|
29
29
|
} from "@nifrajs/web/internal/parity"
|
|
30
30
|
import { codePositionMask, type SourceFinding, stripComments, walkSource } from "./check.ts"
|
|
31
|
+
import { detectToolingDrift, type ToolingDrift } from "./mcp-root.ts"
|
|
31
32
|
import { collectPipelineReport, type PipelineReport } from "./pipeline-report.ts"
|
|
32
33
|
import { type ResolvedTarget, resolveTarget } from "./port.ts"
|
|
33
34
|
|
|
@@ -144,6 +145,14 @@ export interface DoctorResult {
|
|
|
144
145
|
* Advisory (never folded into `ok`): while actively editing a linked package its dist is always
|
|
145
146
|
* momentarily behind - the finding matters when a dev server starts against it. */
|
|
146
147
|
readonly staleDists: readonly StaleDistFinding[]
|
|
148
|
+
/**
|
|
149
|
+
* The running CLI is a different feature version than the `@nifrajs/cli` (or `@nifrajs/core`) the
|
|
150
|
+
* project installs. Advisory (never folded into `ok`): a stale global/bunx binary answering about a
|
|
151
|
+
* project it does not match is an environment problem, not a defect in the project - but every
|
|
152
|
+
* answer it gives (types, checks, docs) reads as authoritative. Present only when `cliVersion` was
|
|
153
|
+
* supplied and the feature versions disagree. Computed by {@link detectToolingDrift}.
|
|
154
|
+
*/
|
|
155
|
+
readonly toolingDrift?: ToolingDrift
|
|
147
156
|
/** Static production-readiness evidence for the selected deploy target. */
|
|
148
157
|
readonly readiness?: DoctorReadiness
|
|
149
158
|
/**
|
|
@@ -672,7 +681,7 @@ async function inferDependencyFix(
|
|
|
672
681
|
/** Run doctor against the project at `cwd`: diff source imports vs declared deps. */
|
|
673
682
|
export async function collectDoctorResult(
|
|
674
683
|
cwd: string,
|
|
675
|
-
opts: { readonly target?: string; readonly strict?: boolean } = {},
|
|
684
|
+
opts: { readonly target?: string; readonly strict?: boolean; readonly cliVersion?: string } = {},
|
|
676
685
|
): Promise<DoctorResult> {
|
|
677
686
|
const pkg = await readJson(join(cwd, "package.json"))
|
|
678
687
|
if (pkg === undefined)
|
|
@@ -700,7 +709,12 @@ export async function collectDoctorResult(
|
|
|
700
709
|
const pipeline = await collectPipelineReport(cwd)
|
|
701
710
|
const pipelineErrors = pipeline.findings.filter((f) => f.severity === "error")
|
|
702
711
|
const readiness = await collectDoctorReadiness(cwd, opts)
|
|
703
|
-
//
|
|
712
|
+
// A stale binary answering about a project it does not match is an environment condition, so like
|
|
713
|
+
// `staleDists` it is reported but never folded into `ok`. Skipped entirely when the caller did not
|
|
714
|
+
// pass its own version (e.g. the MCP server, which already annotates every result with the drift).
|
|
715
|
+
const toolingDrift =
|
|
716
|
+
opts.cliVersion !== undefined ? await detectToolingDrift(cwd, opts.cliVersion) : undefined
|
|
717
|
+
// `staleDists` and `toolingDrift` are advisory (see DoctorResult): they never fail `ok`.
|
|
704
718
|
return {
|
|
705
719
|
ok:
|
|
706
720
|
findings.length === 0 &&
|
|
@@ -712,6 +726,7 @@ export async function collectDoctorResult(
|
|
|
712
726
|
duplicateInstalls,
|
|
713
727
|
staleDists,
|
|
714
728
|
readiness,
|
|
729
|
+
...(toolingDrift !== undefined ? { toolingDrift } : {}),
|
|
715
730
|
...(pipeline.ran ? { pipeline } : {}),
|
|
716
731
|
}
|
|
717
732
|
}
|
|
@@ -719,7 +734,7 @@ export async function collectDoctorResult(
|
|
|
719
734
|
/** Safely add undeclared imports to package.json when the version can be inferred without network I/O. */
|
|
720
735
|
export async function applyDoctorAutoFix(
|
|
721
736
|
cwd: string,
|
|
722
|
-
opts: { readonly target?: string; readonly strict?: boolean } = {},
|
|
737
|
+
opts: { readonly target?: string; readonly strict?: boolean; readonly cliVersion?: string } = {},
|
|
723
738
|
): Promise<DoctorResult> {
|
|
724
739
|
const before = await collectDoctorResult(cwd, opts)
|
|
725
740
|
if (!before.ran || before.findings.length === 0) return before
|
|
@@ -798,6 +813,8 @@ export async function runDoctor(
|
|
|
798
813
|
readonly autoFix?: boolean
|
|
799
814
|
readonly strict?: boolean
|
|
800
815
|
readonly target?: string
|
|
816
|
+
/** The running CLI's own version, so doctor can flag a binary that mismatches the project. */
|
|
817
|
+
readonly cliVersion?: string
|
|
801
818
|
} = {},
|
|
802
819
|
): Promise<boolean> {
|
|
803
820
|
const result = opts.autoFix
|
|
@@ -872,6 +889,19 @@ export async function runDoctor(
|
|
|
872
889
|
}
|
|
873
890
|
console.log("")
|
|
874
891
|
}
|
|
892
|
+
// Advisory, printed regardless of ok: the binary answering is a different feature version than the
|
|
893
|
+
// one the project builds with, so its types/checks/docs describe a surface the code does not have.
|
|
894
|
+
if (result.toolingDrift !== undefined) {
|
|
895
|
+
const drift = result.toolingDrift
|
|
896
|
+
console.log(
|
|
897
|
+
`⚠ this CLI is nifra ${drift.cli}, but the project installs ${drift.package} ${drift.project} - ` +
|
|
898
|
+
"its types, checks, and docs may describe a different version than your code builds with.",
|
|
899
|
+
)
|
|
900
|
+
console.log(
|
|
901
|
+
" fix: run the project's own CLI (`bunx --bun nifra doctor` from the project directory, " +
|
|
902
|
+
"or ./node_modules/.bin/nifra)\n",
|
|
903
|
+
)
|
|
904
|
+
}
|
|
875
905
|
if (result.ok) {
|
|
876
906
|
console.log(
|
|
877
907
|
"✓ every imported package is declared and identity-sensitive installs are deduplicated",
|
|
@@ -897,7 +927,8 @@ export async function runDoctor(
|
|
|
897
927
|
`${byPkg.size > 0 ? "\n" : ""}✗ identity-sensitive packages resolve to multiple physical copies:\n`,
|
|
898
928
|
)
|
|
899
929
|
for (const finding of result.duplicateInstalls) {
|
|
900
|
-
console.log(` ${finding.package}`)
|
|
930
|
+
console.log(` ${finding.package} [${finding.cause}]`)
|
|
931
|
+
console.log(` versions: ${finding.versions.join(", ")}`)
|
|
901
932
|
for (const copy of finding.copies) {
|
|
902
933
|
console.log(` ${copy.version} at ${copy.path} ← ${copy.importers.join(", ")}`)
|
|
903
934
|
}
|
package/src/load.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* reach the edge bundle. A simple single-target app with no edge build can put it all in `framework.ts`.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from "node:fs"
|
|
16
|
-
import { resolve } from "node:path"
|
|
16
|
+
import { isAbsolute, resolve } from "node:path"
|
|
17
17
|
import { checkPipelineSeparation } from "./pipeline-guard.ts"
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -210,11 +210,30 @@ export async function loadApp(
|
|
|
210
210
|
// resolved to absolute here. It is embedded verbatim as an import specifier in the generated client
|
|
211
211
|
// entry, and `nifra dev` and `nifra build` write that entry into DIFFERENT directories - so a relative
|
|
212
212
|
// specifier would resolve against different bases and load in one phase but not the other. A bare or
|
|
213
|
-
// package specifier (`@nifrajs/web-react/client`) is location-independent
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
// package specifier (`@nifrajs/web-react/client`) or an already-absolute path is location-independent
|
|
214
|
+
// and passes through unchanged.
|
|
215
|
+
let clientModule: string
|
|
216
|
+
if (fw.clientModule.startsWith("./") || fw.clientModule.startsWith("../")) {
|
|
217
|
+
clientModule = resolve(cwd, fw.clientModule)
|
|
218
|
+
} else {
|
|
219
|
+
// A specifier that is neither `./`-relative, scoped (`@…`), nor absolute is read as a BARE PACKAGE
|
|
220
|
+
// specifier - resolved against `node_modules`, not `cwd`. If a real local file sits at that path
|
|
221
|
+
// (a `./` was forgotten: `src/client.tsx` instead of `./src/client.tsx`), the bundler would ignore
|
|
222
|
+
// the file, fail to find a package, and surface an opaque "cannot resolve" deep in the build. Reject
|
|
223
|
+
// it here with the one-character fix rather than letting it fail silently.
|
|
224
|
+
if (
|
|
225
|
+
!isAbsolute(fw.clientModule) &&
|
|
226
|
+
!fw.clientModule.startsWith("@") &&
|
|
227
|
+
existsSync(resolve(cwd, fw.clientModule))
|
|
228
|
+
) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`[nifra] clientModule "${fw.clientModule}" has no "./" prefix, so it is read as a bare package ` +
|
|
231
|
+
`specifier - but a local file exists at that path. Prefix it with "./" (e.g. ` +
|
|
232
|
+
`"./${fw.clientModule}") so \`nifra dev\` and \`nifra build\` resolve the same local entry.`,
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
clientModule = fw.clientModule
|
|
236
|
+
}
|
|
218
237
|
const [vitePlugins, clientPlugins, serverPlugins] = await Promise.all([
|
|
219
238
|
resolvePlugins(fw.vitePlugins),
|
|
220
239
|
resolvePlugins(fw.clientPlugins),
|
package/src/manifest-tool.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { resolve } from "node:path"
|
|
|
5
5
|
import {
|
|
6
6
|
buildNifraManifest,
|
|
7
7
|
diffNifraManifests,
|
|
8
|
+
type NifraManifest,
|
|
8
9
|
type NifraManifestDiff,
|
|
9
10
|
parseNifraManifest,
|
|
10
11
|
serializeNifraManifest,
|
|
@@ -38,11 +39,18 @@ export function formatManifestDiff(diff: NifraManifestDiff): string {
|
|
|
38
39
|
return lines.join("\n")
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
export interface ManifestEmitCommandResult {
|
|
43
|
+
readonly ok: boolean
|
|
44
|
+
readonly path: string
|
|
45
|
+
readonly manifest?: NifraManifest
|
|
46
|
+
readonly signaturePath?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Emit the manifest without printing. This is the execution seam used by all adapters. */
|
|
50
|
+
export async function collectManifestEmit(
|
|
43
51
|
cwd: string,
|
|
44
52
|
options: { readonly config?: string; readonly out?: string; readonly sign?: string } = {},
|
|
45
|
-
): Promise<
|
|
53
|
+
): Promise<ManifestEmitCommandResult> {
|
|
46
54
|
const verification = await collectProjectVerification(cwd, {
|
|
47
55
|
...(options.config !== undefined ? { config: options.config } : {}),
|
|
48
56
|
})
|
|
@@ -58,9 +66,9 @@ export async function runManifestEmit(
|
|
|
58
66
|
if (assurance === undefined) {
|
|
59
67
|
throw new Error("[nifra] assurance evaluation was unavailable for manifest emission")
|
|
60
68
|
}
|
|
69
|
+
const path = resolve(cwd, options.out ?? config.manifest?.path ?? DEFAULT_MANIFEST_FILE)
|
|
61
70
|
if (!assurance.ok || (capabilityProject !== undefined && !capabilityProject.report.ok)) {
|
|
62
|
-
|
|
63
|
-
return false
|
|
71
|
+
return { ok: false, path }
|
|
64
72
|
}
|
|
65
73
|
const manifest = await buildNifraManifest({
|
|
66
74
|
source: config.source,
|
|
@@ -68,9 +76,9 @@ export async function runManifestEmit(
|
|
|
68
76
|
assurance,
|
|
69
77
|
...(capabilityProject !== undefined ? { capabilities: capabilityProject.report } : {}),
|
|
70
78
|
})
|
|
71
|
-
const path = resolve(cwd, options.out ?? config.manifest?.path ?? DEFAULT_MANIFEST_FILE)
|
|
72
79
|
await Bun.write(path, `${serializeNifraManifest(manifest)}\n`)
|
|
73
80
|
|
|
81
|
+
let signaturePath: string | undefined
|
|
74
82
|
if (options.sign !== undefined) {
|
|
75
83
|
if (config.manifest?.signer === undefined) {
|
|
76
84
|
throw new Error(
|
|
@@ -79,29 +87,52 @@ export async function runManifestEmit(
|
|
|
79
87
|
}
|
|
80
88
|
const signer = await config.manifest.signer(options.sign)
|
|
81
89
|
const signature = await signNifraManifest(manifest, signer)
|
|
82
|
-
|
|
90
|
+
signaturePath = `${path}.sig`
|
|
91
|
+
await Bun.write(signaturePath, `${serializeNifraManifestSignature(signature)}\n`)
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, path, manifest, ...(signaturePath === undefined ? {} : { signaturePath }) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Build only after both assurance layers pass, then optionally create an operator-signed sidecar. */
|
|
97
|
+
export async function runManifestEmit(
|
|
98
|
+
cwd: string,
|
|
99
|
+
options: { readonly config?: string; readonly out?: string; readonly sign?: string } = {},
|
|
100
|
+
): Promise<boolean> {
|
|
101
|
+
const result = await collectManifestEmit(cwd, options)
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
console.error("[nifra] refusing to emit a manifest from failing assurance")
|
|
104
|
+
return false
|
|
83
105
|
}
|
|
84
106
|
console.log(
|
|
85
|
-
`[nifra] wrote manifest to ${path}${
|
|
107
|
+
`[nifra] wrote manifest to ${result.path}${result.signaturePath === undefined ? "" : ` and ${result.signaturePath}`}`,
|
|
86
108
|
)
|
|
87
109
|
return true
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
/** Diff two already-emitted, hash-verified artifacts. Suitable for deploy promotion gates. */
|
|
91
|
-
export async function
|
|
113
|
+
export async function collectManifestDiff(
|
|
92
114
|
cwd: string,
|
|
93
115
|
beforePath: string,
|
|
94
116
|
afterPath: string,
|
|
95
|
-
|
|
96
|
-
): Promise<boolean> {
|
|
117
|
+
): Promise<NifraManifestDiff> {
|
|
97
118
|
const before = resolve(cwd, beforePath)
|
|
98
119
|
const after = resolve(cwd, afterPath)
|
|
99
120
|
if (!existsSync(before)) throw new Error(`[nifra] manifest not found: ${before}`)
|
|
100
121
|
if (!existsSync(after)) throw new Error(`[nifra] manifest not found: ${after}`)
|
|
101
|
-
|
|
122
|
+
return diffNifraManifests(
|
|
102
123
|
await parseNifraManifest(await Bun.file(before).text(), before),
|
|
103
124
|
await parseNifraManifest(await Bun.file(after).text(), after),
|
|
104
125
|
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Diff two already-emitted, hash-verified artifacts and print the result. */
|
|
129
|
+
export async function runManifestDiff(
|
|
130
|
+
cwd: string,
|
|
131
|
+
beforePath: string,
|
|
132
|
+
afterPath: string,
|
|
133
|
+
options: { readonly json?: boolean } = {},
|
|
134
|
+
): Promise<boolean> {
|
|
135
|
+
const diff = await collectManifestDiff(cwd, beforePath, afterPath)
|
|
105
136
|
console.log(
|
|
106
137
|
options.json === true
|
|
107
138
|
? JSON.stringify({ hasBreaking: diff.hasBreaking, changes: diff.changes }, null, 2)
|
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.14.1"
|
|
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 =>
|