@nifrajs/cli 2.13.0 → 3.1.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.
Files changed (114) hide show
  1. package/dist/capabilities-tool.d.ts +54 -1
  2. package/dist/capabilities-tool.d.ts.map +1 -1
  3. package/dist/capabilities-tool.js +146 -18
  4. package/dist/capabilities-tool.js.map +1 -1
  5. package/dist/check.d.ts +11 -11
  6. package/dist/check.d.ts.map +1 -1
  7. package/dist/check.js +260 -49
  8. package/dist/check.js.map +1 -1
  9. package/dist/cli.d.ts +11 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +130 -414
  12. package/dist/cli.js.map +1 -1
  13. package/dist/command-catalog.d.ts +250 -0
  14. package/dist/command-catalog.d.ts.map +1 -0
  15. package/dist/command-catalog.js +1032 -0
  16. package/dist/command-catalog.js.map +1 -0
  17. package/dist/contracts.d.ts +9 -0
  18. package/dist/contracts.d.ts.map +1 -1
  19. package/dist/contracts.js +22 -6
  20. package/dist/contracts.js.map +1 -1
  21. package/dist/doctor.d.ts +55 -0
  22. package/dist/doctor.d.ts.map +1 -1
  23. package/dist/doctor.js +82 -9
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/env-file.d.ts +17 -0
  26. package/dist/env-file.d.ts.map +1 -0
  27. package/dist/env-file.js +113 -0
  28. package/dist/env-file.js.map +1 -0
  29. package/dist/fix-recipes.d.ts.map +1 -1
  30. package/dist/fix-recipes.js +91 -15
  31. package/dist/fix-recipes.js.map +1 -1
  32. package/dist/internal/reserved-segment-codemod.d.ts +36 -0
  33. package/dist/internal/reserved-segment-codemod.d.ts.map +1 -0
  34. package/dist/internal/reserved-segment-codemod.js +134 -0
  35. package/dist/internal/reserved-segment-codemod.js.map +1 -0
  36. package/dist/internal/typescript-import.d.ts +1 -1
  37. package/dist/internal/typescript-import.d.ts.map +1 -1
  38. package/dist/internal/typescript-import.js +62 -11
  39. package/dist/internal/typescript-import.js.map +1 -1
  40. package/dist/introspect.d.ts +26 -1
  41. package/dist/introspect.d.ts.map +1 -1
  42. package/dist/introspect.js +74 -24
  43. package/dist/introspect.js.map +1 -1
  44. package/dist/load.d.ts.map +1 -1
  45. package/dist/load.js +22 -5
  46. package/dist/load.js.map +1 -1
  47. package/dist/manifest-tool.d.ts +15 -1
  48. package/dist/manifest-tool.d.ts.map +1 -1
  49. package/dist/manifest-tool.js +23 -9
  50. package/dist/manifest-tool.js.map +1 -1
  51. package/dist/mcp-http.js +1 -1
  52. package/dist/mcp-http.js.map +1 -1
  53. package/dist/mcp.d.ts +16 -0
  54. package/dist/mcp.d.ts.map +1 -1
  55. package/dist/mcp.js +127 -355
  56. package/dist/mcp.js.map +1 -1
  57. package/dist/project-facts.d.ts +40 -0
  58. package/dist/project-facts.d.ts.map +1 -0
  59. package/dist/project-facts.js +36 -0
  60. package/dist/project-facts.js.map +1 -0
  61. package/dist/recipes/3.0.0.d.ts +10 -0
  62. package/dist/recipes/3.0.0.d.ts.map +1 -0
  63. package/dist/recipes/3.0.0.js +23 -0
  64. package/dist/recipes/3.0.0.js.map +1 -0
  65. package/dist/recipes/index.d.ts.map +1 -1
  66. package/dist/recipes/index.js +2 -0
  67. package/dist/recipes/index.js.map +1 -1
  68. package/dist/release-verification.d.ts.map +1 -1
  69. package/dist/release-verification.js +15 -0
  70. package/dist/release-verification.js.map +1 -1
  71. package/dist/rules/comment-markers.d.ts +8 -0
  72. package/dist/rules/comment-markers.d.ts.map +1 -1
  73. package/dist/rules/comment-markers.js +36 -0
  74. package/dist/rules/comment-markers.js.map +1 -1
  75. package/dist/rules/index.d.ts +2 -3
  76. package/dist/rules/index.d.ts.map +1 -1
  77. package/dist/rules/index.js.map +1 -1
  78. package/dist/rules/routes.d.ts.map +1 -1
  79. package/dist/rules/routes.js +29 -44
  80. package/dist/rules/routes.js.map +1 -1
  81. package/dist/rules/security.js +14 -14
  82. package/dist/rules/security.js.map +1 -1
  83. package/dist/workspace-link.d.ts +37 -0
  84. package/dist/workspace-link.d.ts.map +1 -0
  85. package/dist/workspace-link.js +72 -0
  86. package/dist/workspace-link.js.map +1 -0
  87. package/docs/examples.json +1 -1
  88. package/docs/llms-full.txt +362 -20
  89. package/docs/types.json +956 -56
  90. package/package.json +9 -9
  91. package/src/capabilities-tool.ts +229 -21
  92. package/src/check.ts +312 -65
  93. package/src/cli.ts +140 -405
  94. package/src/command-catalog.ts +1435 -0
  95. package/src/contracts.ts +28 -7
  96. package/src/doctor.ts +134 -12
  97. package/src/env-file.ts +121 -0
  98. package/src/fix-recipes.ts +101 -15
  99. package/src/internal/reserved-segment-codemod.ts +150 -0
  100. package/src/internal/typescript-import.ts +61 -11
  101. package/src/introspect.ts +82 -21
  102. package/src/load.ts +25 -6
  103. package/src/manifest-tool.ts +43 -12
  104. package/src/mcp-http.ts +1 -1
  105. package/src/mcp.ts +163 -420
  106. package/src/project-facts.ts +86 -0
  107. package/src/recipes/3.0.0.ts +24 -0
  108. package/src/recipes/index.ts +2 -0
  109. package/src/release-verification.ts +17 -0
  110. package/src/rules/comment-markers.ts +36 -0
  111. package/src/rules/index.ts +3 -4
  112. package/src/rules/routes.ts +30 -44
  113. package/src/rules/security.ts +14 -14
  114. package/src/workspace-link.ts +80 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "2.13.0",
3
+ "version": "3.1.0",
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.13.0",
70
- "@nifrajs/core": "2.13.0",
71
- "@nifrajs/mcp": "2.13.0",
72
- "@nifrajs/runner": "2.13.0",
73
- "@nifrajs/schema": "2.13.0",
74
- "@nifrajs/testing": "2.13.0",
75
- "@nifrajs/web": "2.13.0",
76
- "create-nifra": "2.13.0"
69
+ "@nifrajs/client": "3.1.0",
70
+ "@nifrajs/core": "3.1.0",
71
+ "@nifrajs/mcp": "3.1.0",
72
+ "@nifrajs/runner": "3.1.0",
73
+ "@nifrajs/schema": "3.1.0",
74
+ "@nifrajs/testing": "3.1.0",
75
+ "@nifrajs/web": "3.1.0",
76
+ "create-nifra": "3.1.0"
77
77
  },
78
78
  "repository": {
79
79
  "type": "git",
@@ -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
- /** Write a lockfile only from a clean, fully-covered project report. */
496
- export async function runCapabilitySnapshot(
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<boolean> {
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
- console.error("[nifra] refusing to snapshot failing capability assurance")
503
- return false
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
- console.log(`[nifra] wrote capability lockfile to ${path}`)
509
- return true
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
- /** Fail on assurance violations or any drift from the checked-in capability lockfile. */
513
- export async function runCapabilityCheck(
601
+ /** Collect the lockfile gate without printing. */
602
+ export async function collectCapabilityCheck(
514
603
  cwd: string,
515
- options: { readonly config?: string; readonly lockfile?: string; readonly json?: boolean } = {},
516
- ): Promise<boolean> {
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
- const ok = !unsafeProject(project) && changes.length === 0
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: project.report,
530
- violations: project.violations,
531
- truncations: project.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 project.report.findings) console.log(`✖ ${finding.message}`)
542
- for (const violation of project.violations)
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 project.truncations)
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
+ }