@nifrajs/cli 2.12.1 → 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 +235 -47
- 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 +96 -406
- 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 +18 -11
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +30 -230
- 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/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/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 +28 -15
- package/package.json +9 -9
- package/src/capabilities-tool.ts +229 -21
- package/src/check.ts +286 -64
- package/src/cli.ts +106 -399
- package/src/command-catalog.ts +1415 -0
- package/src/contracts.ts +28 -7
- package/src/doctor.ts +56 -264
- 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/release-verification.ts +377 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.1",
|
|
4
4
|
"description": "The nifra CLI - zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -66,14 +66,14 @@
|
|
|
66
66
|
"access": "public"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@nifrajs/client": "2.
|
|
70
|
-
"@nifrajs/core": "2.
|
|
71
|
-
"@nifrajs/mcp": "2.
|
|
72
|
-
"@nifrajs/runner": "2.
|
|
73
|
-
"@nifrajs/schema": "2.
|
|
74
|
-
"@nifrajs/testing": "2.
|
|
75
|
-
"@nifrajs/web": "2.
|
|
76
|
-
"create-nifra": "2.
|
|
69
|
+
"@nifrajs/client": "2.14.1",
|
|
70
|
+
"@nifrajs/core": "2.14.1",
|
|
71
|
+
"@nifrajs/mcp": "2.14.1",
|
|
72
|
+
"@nifrajs/runner": "2.14.1",
|
|
73
|
+
"@nifrajs/schema": "2.14.1",
|
|
74
|
+
"@nifrajs/testing": "2.14.1",
|
|
75
|
+
"@nifrajs/web": "2.14.1",
|
|
76
|
+
"create-nifra": "2.14.1"
|
|
77
77
|
},
|
|
78
78
|
"repository": {
|
|
79
79
|
"type": "git",
|
package/src/capabilities-tool.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { existsSync, readFileSync } from "node:fs"
|
|
|
4
4
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path"
|
|
5
5
|
import { matchesAssuranceSelector } from "@nifrajs/core/assurance"
|
|
6
6
|
import {
|
|
7
|
+
type AssuredCapabilityRoute,
|
|
7
8
|
type CapabilityAssuranceReport,
|
|
9
|
+
type CapabilityDefinition,
|
|
10
|
+
type CapabilityFinding,
|
|
8
11
|
type CapabilityPolicy,
|
|
9
12
|
type CapabilitySnapshot,
|
|
10
13
|
type CapabilitySnapshotRoute,
|
|
@@ -110,6 +113,64 @@ export interface CapabilityProvenanceTruncation {
|
|
|
110
113
|
readonly chain: readonly string[]
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
export interface CapabilityRouteExplanation {
|
|
117
|
+
readonly ok: boolean
|
|
118
|
+
readonly method: string
|
|
119
|
+
readonly path: string
|
|
120
|
+
readonly route: AssuredCapabilityRoute
|
|
121
|
+
readonly definitions: readonly CapabilityDefinition[]
|
|
122
|
+
readonly findings: readonly CapabilityFinding[]
|
|
123
|
+
readonly violations: readonly CapabilityImportViolation[]
|
|
124
|
+
readonly truncations: readonly CapabilityProvenanceTruncation[]
|
|
125
|
+
/** This command explains declarations and static evidence; it does not infer handler-internal branches. */
|
|
126
|
+
readonly note: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Explain one reflected route using only declared capability tokens and collected static evidence. */
|
|
130
|
+
export function explainCapabilityRoute(
|
|
131
|
+
policy: CapabilityPolicy,
|
|
132
|
+
project: CapabilityProjectReport,
|
|
133
|
+
methodInput: string,
|
|
134
|
+
path: string,
|
|
135
|
+
): CapabilityRouteExplanation | undefined {
|
|
136
|
+
const method = methodInput.toUpperCase()
|
|
137
|
+
const route = project.report.routes.find(
|
|
138
|
+
(candidate) => candidate.method === method && candidate.path === path,
|
|
139
|
+
)
|
|
140
|
+
if (route === undefined) return undefined
|
|
141
|
+
|
|
142
|
+
const findings = project.report.findings.filter(
|
|
143
|
+
(finding) =>
|
|
144
|
+
(finding.method === method && finding.path === path) ||
|
|
145
|
+
(finding.method === "*" && finding.path === "*"),
|
|
146
|
+
)
|
|
147
|
+
const violations = project.violations.filter(
|
|
148
|
+
(violation) => violation.method === method && violation.path === path,
|
|
149
|
+
)
|
|
150
|
+
const truncations = project.truncations.filter(
|
|
151
|
+
(truncation) => truncation.method === method && truncation.path === path,
|
|
152
|
+
)
|
|
153
|
+
const ids = new Set<string>([
|
|
154
|
+
...route.declared,
|
|
155
|
+
...route.evidence.map((evidence) => evidence.id),
|
|
156
|
+
...findings.flatMap((finding) =>
|
|
157
|
+
finding.capability === undefined ? [] : [finding.capability],
|
|
158
|
+
),
|
|
159
|
+
])
|
|
160
|
+
const definitions = policy.definitions.filter((definition) => ids.has(definition.id))
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
ok: findings.length === 0 && violations.length === 0 && truncations.length === 0,
|
|
163
|
+
method,
|
|
164
|
+
path,
|
|
165
|
+
route,
|
|
166
|
+
definitions: Object.freeze(definitions),
|
|
167
|
+
findings: Object.freeze(findings),
|
|
168
|
+
violations: Object.freeze(violations),
|
|
169
|
+
truncations: Object.freeze(truncations),
|
|
170
|
+
note: "Nifra explains declarations and static provenance only; it does not guess handler-internal branches or runtime data flow.",
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
113
174
|
function resolveLocalModule(
|
|
114
175
|
cwd: string,
|
|
115
176
|
from: string,
|
|
@@ -492,44 +553,122 @@ function unsafeProject(project: CapabilityProjectReport): boolean {
|
|
|
492
553
|
return !project.report.ok || project.violations.length > 0 || project.truncations.length > 0
|
|
493
554
|
}
|
|
494
555
|
|
|
495
|
-
|
|
496
|
-
|
|
556
|
+
export interface CapabilitySnapshotCommandResult {
|
|
557
|
+
readonly ok: boolean
|
|
558
|
+
readonly path: string
|
|
559
|
+
readonly snapshot?: CapabilitySnapshot
|
|
560
|
+
readonly report: CapabilityAssuranceReport
|
|
561
|
+
readonly violations: readonly CapabilityImportViolation[]
|
|
562
|
+
readonly truncations: readonly CapabilityProvenanceTruncation[]
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export interface CapabilityCheckCommandResult {
|
|
566
|
+
readonly ok: boolean
|
|
567
|
+
readonly report: CapabilityAssuranceReport
|
|
568
|
+
readonly violations: readonly CapabilityImportViolation[]
|
|
569
|
+
readonly truncations: readonly CapabilityProvenanceTruncation[]
|
|
570
|
+
readonly changes: readonly string[]
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Collect the snapshot result without printing. The CLI and command catalog use this pure face. */
|
|
574
|
+
export async function collectCapabilitySnapshot(
|
|
497
575
|
cwd: string,
|
|
498
576
|
options: { readonly config?: string; readonly out?: string } = {},
|
|
499
|
-
): Promise<
|
|
577
|
+
): Promise<CapabilitySnapshotCommandResult> {
|
|
500
578
|
const { policy, project } = await currentProject(cwd, options.config)
|
|
579
|
+
const path = resolve(cwd, options.out ?? policy.lockfile ?? "capabilities.lock.json")
|
|
501
580
|
if (unsafeProject(project)) {
|
|
502
|
-
|
|
503
|
-
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
path,
|
|
584
|
+
report: project.report,
|
|
585
|
+
violations: project.violations,
|
|
586
|
+
truncations: project.truncations,
|
|
587
|
+
}
|
|
504
588
|
}
|
|
505
589
|
const snapshot = snapshotCapabilities(project.report)
|
|
506
|
-
const path = resolve(cwd, options.out ?? policy.lockfile ?? "capabilities.lock.json")
|
|
507
590
|
await Bun.write(path, `${JSON.stringify(snapshot, null, 2)}\n`)
|
|
508
|
-
|
|
509
|
-
|
|
591
|
+
return {
|
|
592
|
+
ok: true,
|
|
593
|
+
path,
|
|
594
|
+
snapshot,
|
|
595
|
+
report: project.report,
|
|
596
|
+
violations: project.violations,
|
|
597
|
+
truncations: project.truncations,
|
|
598
|
+
}
|
|
510
599
|
}
|
|
511
600
|
|
|
512
|
-
/**
|
|
513
|
-
export async function
|
|
601
|
+
/** Collect the lockfile gate without printing. */
|
|
602
|
+
export async function collectCapabilityCheck(
|
|
514
603
|
cwd: string,
|
|
515
|
-
options: { readonly config?: string; readonly lockfile?: string
|
|
516
|
-
): Promise<
|
|
604
|
+
options: { readonly config?: string; readonly lockfile?: string } = {},
|
|
605
|
+
): Promise<CapabilityCheckCommandResult> {
|
|
517
606
|
const { policy, project } = await currentProject(cwd, options.config)
|
|
518
607
|
const path = resolve(cwd, options.lockfile ?? policy.lockfile ?? "capabilities.lock.json")
|
|
519
608
|
if (!existsSync(path)) throw new Error(`[nifra] capability lockfile not found: ${path}`)
|
|
520
609
|
const baseline = parseCapabilityLockfile(await Bun.file(path).text(), path)
|
|
521
610
|
const current = snapshotCapabilities(project.report)
|
|
522
611
|
const changes = diffCapabilitySnapshots(baseline, current)
|
|
523
|
-
|
|
612
|
+
return {
|
|
613
|
+
ok: !unsafeProject(project) && changes.length === 0,
|
|
614
|
+
report: project.report,
|
|
615
|
+
violations: project.violations,
|
|
616
|
+
truncations: project.truncations,
|
|
617
|
+
changes,
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export type CapabilityExplainCommandResult =
|
|
622
|
+
| CapabilityRouteExplanation
|
|
623
|
+
| { readonly ok: false; readonly error: string; readonly routes: readonly string[] }
|
|
624
|
+
|
|
625
|
+
/** Collect one route explanation without printing. */
|
|
626
|
+
export async function collectCapabilityExplanation(
|
|
627
|
+
cwd: string,
|
|
628
|
+
method: string,
|
|
629
|
+
path: string,
|
|
630
|
+
options: { readonly config?: string } = {},
|
|
631
|
+
): Promise<CapabilityExplainCommandResult> {
|
|
632
|
+
const { policy, project } = await currentProject(cwd, options.config)
|
|
633
|
+
const explanation = explainCapabilityRoute(policy, project, method, path)
|
|
634
|
+
if (explanation !== undefined) return explanation
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
error: `[nifra] capability route not found: ${method.toUpperCase()} ${path}`,
|
|
638
|
+
routes: project.report.routes.map((route) => `${route.method} ${route.path}`),
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Write a lockfile only from a clean, fully-covered project report. */
|
|
643
|
+
export async function runCapabilitySnapshot(
|
|
644
|
+
cwd: string,
|
|
645
|
+
options: { readonly config?: string; readonly out?: string } = {},
|
|
646
|
+
): Promise<boolean> {
|
|
647
|
+
const result = await collectCapabilitySnapshot(cwd, options)
|
|
648
|
+
if (!result.ok) {
|
|
649
|
+
console.error("[nifra] refusing to snapshot failing capability assurance")
|
|
650
|
+
return false
|
|
651
|
+
}
|
|
652
|
+
console.log(`[nifra] wrote capability lockfile to ${result.path}`)
|
|
653
|
+
return true
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Fail on assurance violations or any drift from the checked-in capability lockfile. */
|
|
657
|
+
export async function runCapabilityCheck(
|
|
658
|
+
cwd: string,
|
|
659
|
+
options: { readonly config?: string; readonly lockfile?: string; readonly json?: boolean } = {},
|
|
660
|
+
): Promise<boolean> {
|
|
661
|
+
const result = await collectCapabilityCheck(cwd, options)
|
|
662
|
+
const ok = result.ok
|
|
524
663
|
if (options.json === true) {
|
|
525
664
|
console.log(
|
|
526
665
|
JSON.stringify(
|
|
527
666
|
{
|
|
528
667
|
ok,
|
|
529
|
-
report:
|
|
530
|
-
violations:
|
|
531
|
-
truncations:
|
|
532
|
-
changes,
|
|
668
|
+
report: result.report,
|
|
669
|
+
violations: result.violations,
|
|
670
|
+
truncations: result.truncations,
|
|
671
|
+
changes: result.changes,
|
|
533
672
|
},
|
|
534
673
|
null,
|
|
535
674
|
2,
|
|
@@ -538,16 +677,85 @@ export async function runCapabilityCheck(
|
|
|
538
677
|
} else if (ok) {
|
|
539
678
|
console.log("✓ capability assurance and lockfile are current")
|
|
540
679
|
} else {
|
|
541
|
-
for (const finding of
|
|
542
|
-
for (const violation of
|
|
680
|
+
for (const finding of result.report.findings) console.log(`✖ ${finding.message}`)
|
|
681
|
+
for (const violation of result.violations)
|
|
543
682
|
console.log(
|
|
544
683
|
`✖ ${violation.method} ${violation.path}: ${violation.chain.join(" → ")} - ${violation.reason}`,
|
|
545
684
|
)
|
|
546
|
-
for (const truncation of
|
|
685
|
+
for (const truncation of result.truncations)
|
|
547
686
|
console.log(
|
|
548
687
|
`✖ ${truncation.method} ${truncation.path}: ${truncation.chain.join(" → ")} - ${truncation.reason}`,
|
|
549
688
|
)
|
|
550
|
-
for (const change of changes) console.log(`✖ capability lock drift: ${change}`)
|
|
689
|
+
for (const change of result.changes) console.log(`✖ capability lock drift: ${change}`)
|
|
551
690
|
}
|
|
552
691
|
return ok
|
|
553
692
|
}
|
|
693
|
+
|
|
694
|
+
function formatCapabilityRouteExplanation(explanation: CapabilityRouteExplanation): string {
|
|
695
|
+
const lines = [
|
|
696
|
+
`${explanation.method} ${explanation.path}`,
|
|
697
|
+
`status: ${explanation.ok ? "assured" : "needs attention"}`,
|
|
698
|
+
`provenance coverage: ${explanation.route.covered ? "covered" : "uncovered"}`,
|
|
699
|
+
`declared: ${explanation.route.declared.length > 0 ? explanation.route.declared.join(", ") : "none"}`,
|
|
700
|
+
`evidence: ${
|
|
701
|
+
explanation.route.evidence.length > 0
|
|
702
|
+
? explanation.route.evidence
|
|
703
|
+
.map((item) => `${item.id} [${item.kind}] from ${item.source}`)
|
|
704
|
+
.join(", ")
|
|
705
|
+
: "none"
|
|
706
|
+
}`,
|
|
707
|
+
`unproven: ${explanation.route.unproven.length > 0 ? explanation.route.unproven.join(", ") : "none"}`,
|
|
708
|
+
]
|
|
709
|
+
if (explanation.definitions.length > 0) {
|
|
710
|
+
lines.push("definitions:")
|
|
711
|
+
for (const definition of explanation.definitions) {
|
|
712
|
+
lines.push(
|
|
713
|
+
` ${definition.id}: zone=${definition.zone}, access=${definition.access}, idempotency=${definition.idempotency ?? "none"}`,
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (explanation.findings.length > 0) {
|
|
718
|
+
lines.push("findings:")
|
|
719
|
+
for (const finding of explanation.findings) lines.push(` ✖ ${finding.message}`)
|
|
720
|
+
}
|
|
721
|
+
for (const violation of explanation.violations) {
|
|
722
|
+
lines.push(` ✖ ${violation.chain.join(" → ")} - ${violation.reason}`)
|
|
723
|
+
}
|
|
724
|
+
for (const truncation of explanation.truncations) {
|
|
725
|
+
lines.push(` ✖ ${truncation.chain.join(" → ")} - ${truncation.reason}`)
|
|
726
|
+
}
|
|
727
|
+
lines.push(`note: ${explanation.note}`)
|
|
728
|
+
return lines.join("\n")
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Explain one route's token-only capability declaration and static provenance. */
|
|
732
|
+
export async function runCapabilityExplain(
|
|
733
|
+
cwd: string,
|
|
734
|
+
method: string,
|
|
735
|
+
path: string,
|
|
736
|
+
options: { readonly config?: string; readonly json?: boolean } = {},
|
|
737
|
+
): Promise<boolean> {
|
|
738
|
+
const explanation = await collectCapabilityExplanation(cwd, method, path, options)
|
|
739
|
+
if ("error" in explanation) {
|
|
740
|
+
const message = explanation.error
|
|
741
|
+
if (options.json === true) {
|
|
742
|
+
console.log(
|
|
743
|
+
JSON.stringify(
|
|
744
|
+
{
|
|
745
|
+
ok: false,
|
|
746
|
+
error: message,
|
|
747
|
+
routes: explanation.routes,
|
|
748
|
+
},
|
|
749
|
+
null,
|
|
750
|
+
2,
|
|
751
|
+
),
|
|
752
|
+
)
|
|
753
|
+
} else {
|
|
754
|
+
console.error(message)
|
|
755
|
+
}
|
|
756
|
+
return false
|
|
757
|
+
}
|
|
758
|
+
if (options.json === true) console.log(JSON.stringify(explanation, null, 2))
|
|
759
|
+
else console.log(formatCapabilityRouteExplanation(explanation))
|
|
760
|
+
return explanation.ok
|
|
761
|
+
}
|