yuho 5.0.0__py3-none-any.whl

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 (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,656 @@
1
+ """
2
+ Library subcommand implementations for Yuho CLI.
3
+
4
+ Provides search, install, uninstall, list, update, publish, and info commands
5
+ for managing Yuho statute packages.
6
+ """
7
+
8
+ from typing import Optional, List
9
+ from pathlib import Path
10
+ import json
11
+ import click
12
+
13
+
14
+ def run_library_search(
15
+ query: str,
16
+ jurisdiction: Optional[str] = None,
17
+ tags: Optional[List[str]] = None,
18
+ limit: int = 20,
19
+ json_output: bool = False,
20
+ verbose: bool = False,
21
+ ) -> None:
22
+ """
23
+ Search the library for packages matching query.
24
+
25
+ Args:
26
+ query: Search query (keyword, section number, or title)
27
+ jurisdiction: Filter by jurisdiction
28
+ tags: Filter by tags
29
+ limit: Maximum results to return
30
+ json_output: Output as JSON
31
+ verbose: Verbose output
32
+ """
33
+ from yuho.library import search_library
34
+
35
+ results = search_library(
36
+ keyword=query,
37
+ jurisdiction=jurisdiction,
38
+ tags=tags,
39
+ )[:limit]
40
+
41
+ if json_output:
42
+ click.echo(json.dumps(results, indent=2))
43
+ return
44
+
45
+ if not results:
46
+ click.echo(f"No packages found for '{query}'")
47
+ return
48
+
49
+ click.echo(f"Found {len(results)} package(s):\n")
50
+
51
+ for pkg in results:
52
+ section = pkg.get("section_number", "")
53
+ title = pkg.get("title", "")
54
+ version = pkg.get("version", "")
55
+ jurisdiction_val = pkg.get("jurisdiction", "")
56
+ description = pkg.get("description", "")
57
+
58
+ click.echo(click.style(f" {section}", fg="cyan", bold=True) +
59
+ click.style(f" v{version}", fg="yellow"))
60
+ click.echo(f" {title}")
61
+ if jurisdiction_val:
62
+ click.echo(f" Jurisdiction: {jurisdiction_val}")
63
+ if description and verbose:
64
+ click.echo(f" {description[:80]}...")
65
+ click.echo()
66
+
67
+
68
+ def run_library_install(
69
+ package: str,
70
+ force: bool = False,
71
+ no_deps: bool = False,
72
+ json_output: bool = False,
73
+ verbose: bool = False,
74
+ ) -> None:
75
+ """
76
+ Install a package from registry or local path.
77
+
78
+ Args:
79
+ package: Package section number or path to .yhpkg
80
+ force: Overwrite existing package
81
+ no_deps: Don't install dependencies
82
+ json_output: Output as JSON
83
+ verbose: Verbose output
84
+ """
85
+ from yuho.library import install_package, download_package
86
+
87
+ path = Path(package)
88
+
89
+ if path.exists():
90
+ # Local install
91
+ success, message = install_package(str(path), force=force)
92
+ else:
93
+ # Registry install
94
+ success, message = download_package(package)
95
+
96
+ result = {"success": success, "message": message}
97
+
98
+ if json_output:
99
+ click.echo(json.dumps(result))
100
+ elif success:
101
+ click.echo(click.style("✓ ", fg="green") + message)
102
+ else:
103
+ click.echo(click.style("✗ ", fg="red") + message)
104
+ raise SystemExit(1)
105
+
106
+
107
+ def run_library_uninstall(
108
+ package: str,
109
+ dry_run: bool = False,
110
+ json_output: bool = False,
111
+ verbose: bool = False,
112
+ ) -> None:
113
+ """
114
+ Uninstall an installed package.
115
+
116
+ Args:
117
+ package: Package section number
118
+ dry_run: Show what would be done without doing it
119
+ json_output: Output as JSON
120
+ verbose: Verbose output
121
+ """
122
+ from yuho.library import uninstall_package, list_installed
123
+
124
+ if dry_run:
125
+ # Check if package is installed
126
+ installed = list_installed()
127
+ pkg_info = next((p for p in installed if p.get("section_number") == package), None)
128
+
129
+ if pkg_info:
130
+ result = {
131
+ "dry_run": True,
132
+ "would_uninstall": package,
133
+ "version": pkg_info.get("version", "unknown"),
134
+ "title": pkg_info.get("title", ""),
135
+ }
136
+ if json_output:
137
+ click.echo(json.dumps(result, indent=2))
138
+ else:
139
+ click.echo(click.style("[DRY RUN] ", fg="yellow") +
140
+ f"Would uninstall {package} v{pkg_info.get('version', '?')}")
141
+ if verbose:
142
+ click.echo(f" Title: {pkg_info.get('title', 'N/A')}")
143
+ else:
144
+ result = {"dry_run": True, "error": f"Package not found: {package}"}
145
+ if json_output:
146
+ click.echo(json.dumps(result))
147
+ else:
148
+ click.echo(click.style("[DRY RUN] ", fg="yellow") +
149
+ f"Package not installed: {package}")
150
+ return
151
+
152
+ success, message = uninstall_package(package)
153
+
154
+ result = {"success": success, "message": message}
155
+
156
+ if json_output:
157
+ click.echo(json.dumps(result))
158
+ elif success:
159
+ click.echo(click.style("✓ ", fg="green") + message)
160
+ else:
161
+ click.echo(click.style("✗ ", fg="red") + message)
162
+ raise SystemExit(1)
163
+
164
+
165
+ def run_library_list(
166
+ json_output: bool = False,
167
+ verbose: bool = False,
168
+ ) -> None:
169
+ """
170
+ List all installed packages.
171
+
172
+ Args:
173
+ json_output: Output as JSON
174
+ verbose: Verbose output
175
+ """
176
+ from yuho.library import list_installed
177
+
178
+ packages = list_installed()
179
+
180
+ if json_output:
181
+ click.echo(json.dumps(packages, indent=2))
182
+ return
183
+
184
+ if not packages:
185
+ click.echo("No packages installed")
186
+ return
187
+
188
+ click.echo(f"Installed packages ({len(packages)}):\n")
189
+
190
+ for pkg in packages:
191
+ section = pkg.get("section_number", "")
192
+ title = pkg.get("title", "")
193
+ version = pkg.get("version", "")
194
+
195
+ click.echo(f" {click.style(section, fg='cyan', bold=True)} " +
196
+ f"{click.style(f'v{version}', fg='yellow')}")
197
+ if verbose:
198
+ click.echo(f" {title}")
199
+
200
+
201
+ def run_library_update(
202
+ package: Optional[str] = None,
203
+ all_packages: bool = False,
204
+ json_output: bool = False,
205
+ verbose: bool = False,
206
+ ) -> None:
207
+ """
208
+ Update one or all packages.
209
+
210
+ Args:
211
+ package: Specific package to update (None for all)
212
+ all_packages: Update all packages
213
+ json_output: Output as JSON
214
+ verbose: Verbose output
215
+ """
216
+ from yuho.library import update_all_packages, check_updates, download_package
217
+
218
+ if package:
219
+ # Update single package
220
+ success, message = download_package(package)
221
+ results = [(package, success, message)]
222
+ else:
223
+ # Check for updates first
224
+ updates = check_updates()
225
+
226
+ if not updates:
227
+ if json_output:
228
+ click.echo(json.dumps({"updates": []}))
229
+ else:
230
+ click.echo("All packages are up to date")
231
+ return
232
+
233
+ if not all_packages:
234
+ # Just show available updates
235
+ if json_output:
236
+ click.echo(json.dumps({"updates": updates}))
237
+ else:
238
+ click.echo("Updates available:\n")
239
+ for u in updates:
240
+ section = u["section_number"]
241
+ current = u["current_version"]
242
+ available = u["available_version"]
243
+ click.echo(f" {section}: {current} -> {available}")
244
+ click.echo("\nRun 'yuho library update --all' to update all")
245
+ return
246
+
247
+ # Update all
248
+ results = update_all_packages()
249
+
250
+ if json_output:
251
+ click.echo(json.dumps([
252
+ {"package": r[0], "success": r[1], "message": r[2]}
253
+ for r in results
254
+ ], indent=2))
255
+ else:
256
+ for section, success, message in results:
257
+ if success:
258
+ click.echo(click.style("✓ ", fg="green") + message)
259
+ else:
260
+ click.echo(click.style("✗ ", fg="red") + f"{section}: {message}")
261
+
262
+
263
+ def run_library_publish(
264
+ path: str,
265
+ registry: Optional[str] = None,
266
+ token: Optional[str] = None,
267
+ dry_run: bool = False,
268
+ json_output: bool = False,
269
+ verbose: bool = False,
270
+ ) -> None:
271
+ """
272
+ Publish a package to the registry.
273
+
274
+ Args:
275
+ path: Path to package directory or .yhpkg
276
+ registry: Registry URL
277
+ token: Auth token
278
+ dry_run: Validate package without actually publishing
279
+ json_output: Output as JSON
280
+ verbose: Verbose output
281
+ """
282
+ from yuho.library import publish_package, Package
283
+ from pathlib import Path as PathLib
284
+
285
+ registry_url = registry or "https://registry.yuho.dev"
286
+ pkg_path = PathLib(path)
287
+
288
+ if dry_run:
289
+ # Validate the package without publishing
290
+ errors = []
291
+ warnings = []
292
+ pkg_info = {}
293
+
294
+ try:
295
+ # Try to load and validate the package
296
+ if pkg_path.is_file() and pkg_path.suffix == ".yhpkg":
297
+ pkg = Package.from_yhpkg(pkg_path)
298
+ elif pkg_path.is_dir():
299
+ # Look for metadata.toml
300
+ meta_file = pkg_path / "metadata.toml"
301
+ if not meta_file.exists():
302
+ errors.append("Missing metadata.toml")
303
+ else:
304
+ import tomllib
305
+ with open(meta_file, "rb") as f:
306
+ pkg_info = tomllib.load(f)
307
+
308
+ # Check for statute.yh
309
+ statute_file = pkg_path / "statute.yh"
310
+ if not statute_file.exists():
311
+ errors.append("Missing statute.yh")
312
+
313
+ # Validate with parser
314
+ if statute_file.exists():
315
+ from yuho.parser import Parser
316
+ parser = Parser()
317
+ result = parser.parse_file(statute_file)
318
+ if result.errors:
319
+ errors.extend(f"Parse error: {e.message}" for e in result.errors)
320
+ else:
321
+ errors.append(f"Invalid package path: {path}")
322
+
323
+ if not token and not dry_run:
324
+ warnings.append("No auth token provided")
325
+
326
+ except Exception as e:
327
+ errors.append(f"Validation error: {e}")
328
+
329
+ result = {
330
+ "dry_run": True,
331
+ "path": str(pkg_path),
332
+ "registry": registry_url,
333
+ "valid": len(errors) == 0,
334
+ "errors": errors,
335
+ "warnings": warnings,
336
+ "package_info": pkg_info,
337
+ }
338
+
339
+ if json_output:
340
+ click.echo(json.dumps(result, indent=2))
341
+ else:
342
+ click.echo(click.style("[DRY RUN] ", fg="yellow") + f"Validating {path}")
343
+ if pkg_info.get("section_number"):
344
+ click.echo(f" Section: {pkg_info.get('section_number')}")
345
+ if pkg_info.get("title"):
346
+ click.echo(f" Title: {pkg_info.get('title')}")
347
+ if pkg_info.get("version"):
348
+ click.echo(f" Version: {pkg_info.get('version')}")
349
+ click.echo(f" Registry: {registry_url}")
350
+
351
+ if errors:
352
+ click.echo(click.style("\nErrors:", fg="red"))
353
+ for err in errors:
354
+ click.echo(f" - {err}")
355
+ if warnings:
356
+ click.echo(click.style("\nWarnings:", fg="yellow"))
357
+ for warn in warnings:
358
+ click.echo(f" - {warn}")
359
+
360
+ if not errors:
361
+ click.echo(click.style("\n✓ Package is valid and ready to publish", fg="green"))
362
+ else:
363
+ click.echo(click.style("\n✗ Package validation failed", fg="red"))
364
+ raise SystemExit(1)
365
+ return
366
+
367
+ success, message = publish_package(
368
+ source=path,
369
+ registry_url=registry_url,
370
+ auth_token=token,
371
+ )
372
+
373
+ result = {"success": success, "message": message}
374
+
375
+ if json_output:
376
+ click.echo(json.dumps(result))
377
+ elif success:
378
+ click.echo(click.style("✓ ", fg="green") + message)
379
+ else:
380
+ click.echo(click.style("✗ ", fg="red") + message)
381
+ raise SystemExit(1)
382
+
383
+
384
+ def run_library_outdated(
385
+ json_output: bool = False,
386
+ verbose: bool = False,
387
+ ) -> None:
388
+ """
389
+ Show packages with updates available.
390
+
391
+ Args:
392
+ json_output: Output as JSON
393
+ verbose: Verbose output including deprecation info
394
+ """
395
+ from yuho.library import check_updates, list_installed, LibraryIndex
396
+ from yuho.library.resolver import Version
397
+
398
+ # Get update info from registry
399
+ updates = check_updates()
400
+ installed = list_installed()
401
+
402
+ # Build installed lookup
403
+ installed_map = {
404
+ pkg.get("section_number"): pkg
405
+ for pkg in installed
406
+ }
407
+
408
+ # Check for deprecated packages
409
+ index = LibraryIndex()
410
+ deprecated_warnings = []
411
+ for pkg in installed:
412
+ section = pkg.get("section_number")
413
+ entry = index.get(section)
414
+ if entry and hasattr(entry, 'deprecation') and getattr(entry, 'is_deprecated', False):
415
+ deprecated_warnings.append({
416
+ "section_number": section,
417
+ "message": f"Package {section} is deprecated",
418
+ })
419
+
420
+ if json_output:
421
+ result = {
422
+ "outdated": updates,
423
+ "deprecated": deprecated_warnings,
424
+ "total_installed": len(installed),
425
+ "total_outdated": len(updates),
426
+ }
427
+ click.echo(json.dumps(result, indent=2))
428
+ return
429
+
430
+ if not updates and not deprecated_warnings:
431
+ click.echo(click.style("All packages are up to date!", fg="green"))
432
+ return
433
+
434
+ if updates:
435
+ click.echo(click.style(f"\nOutdated packages ({len(updates)}):\n", bold=True))
436
+
437
+ for u in updates:
438
+ section = u["section_number"]
439
+ current = u["current_version"]
440
+ available = u["available_version"]
441
+
442
+ # Determine upgrade type
443
+ try:
444
+ curr_v = Version.parse(current)
445
+ avail_v = Version.parse(available)
446
+ if avail_v.major > curr_v.major:
447
+ change_type = click.style("MAJOR", fg="red", bold=True)
448
+ elif avail_v.minor > curr_v.minor:
449
+ change_type = click.style("minor", fg="yellow")
450
+ else:
451
+ change_type = click.style("patch", fg="green")
452
+ except Exception:
453
+ change_type = ""
454
+
455
+ click.echo(
456
+ f" {click.style(section, fg='cyan', bold=True)} "
457
+ f"{click.style(current, fg='yellow')} -> "
458
+ f"{click.style(available, fg='green')} {change_type}"
459
+ )
460
+
461
+ if verbose:
462
+ pkg = installed_map.get(section, {})
463
+ title = pkg.get("title", "")
464
+ if title:
465
+ click.echo(f" {title}")
466
+
467
+ click.echo()
468
+
469
+ if deprecated_warnings:
470
+ click.echo(click.style(f"\nDeprecated packages ({len(deprecated_warnings)}):\n", fg="yellow", bold=True))
471
+ for d in deprecated_warnings:
472
+ click.echo(f" {click.style('⚠', fg='yellow')} {d['message']}")
473
+ click.echo()
474
+
475
+ click.echo(f"Run 'yuho library update --all' to update outdated packages")
476
+
477
+
478
+ def run_library_tree(
479
+ package: Optional[str] = None,
480
+ depth: int = 10,
481
+ json_output: bool = False,
482
+ verbose: bool = False,
483
+ ) -> None:
484
+ """
485
+ Show dependency tree for packages.
486
+
487
+ Args:
488
+ package: Specific package to show tree for (None for all installed)
489
+ depth: Maximum depth to display
490
+ json_output: Output as JSON
491
+ verbose: Verbose output including versions
492
+ """
493
+ from yuho.library import list_installed, LibraryIndex, Package
494
+ from yuho.library.resolver import Dependency
495
+ from pathlib import Path
496
+
497
+ index = LibraryIndex()
498
+
499
+ def get_dependencies(section: str) -> List[str]:
500
+ """Get dependencies for a package from its metadata."""
501
+ entry = index.get(section)
502
+ if not entry:
503
+ return []
504
+ # Load package to get dependencies
505
+ pkg_path = Path(entry.package_path) if hasattr(entry, 'package_path') else None
506
+ if pkg_path and pkg_path.exists():
507
+ try:
508
+ pkg = Package.from_yhpkg(pkg_path)
509
+ return pkg.metadata.dependencies
510
+ except Exception:
511
+ pass
512
+ return []
513
+
514
+ def build_tree(section: str, seen: set, current_depth: int) -> dict:
515
+ """Build dependency tree recursively."""
516
+ if current_depth > depth or section in seen:
517
+ return {"section": section, "circular": section in seen, "children": []}
518
+
519
+ seen = seen | {section}
520
+ entry = index.get(section)
521
+ version = entry.version if entry else "?"
522
+
523
+ deps = get_dependencies(section)
524
+ children = []
525
+
526
+ for dep_str in deps:
527
+ try:
528
+ dep = Dependency.parse(dep_str)
529
+ child_tree = build_tree(dep.package, seen, current_depth + 1)
530
+ child_tree["constraint"] = str(dep.constraint)
531
+ children.append(child_tree)
532
+ except Exception:
533
+ children.append({"section": dep_str, "error": True, "children": []})
534
+
535
+ return {
536
+ "section": section,
537
+ "version": version,
538
+ "children": children,
539
+ }
540
+
541
+ def print_tree(node: dict, prefix: str = "", is_last: bool = True) -> None:
542
+ """Print tree with ASCII art."""
543
+ connector = "└── " if is_last else "├── "
544
+ section = node.get("section", "?")
545
+ version = node.get("version", "")
546
+ constraint = node.get("constraint", "")
547
+
548
+ # Format the node
549
+ label = click.style(section, fg="cyan", bold=True)
550
+ if verbose and version:
551
+ label += click.style(f" v{version}", fg="yellow")
552
+ if constraint:
553
+ label += click.style(f" ({constraint})", fg="white", dim=True)
554
+ if node.get("circular"):
555
+ label += click.style(" (circular)", fg="red")
556
+ if node.get("error"):
557
+ label += click.style(" (not found)", fg="red")
558
+
559
+ click.echo(prefix + connector + label)
560
+
561
+ children = node.get("children", [])
562
+ child_prefix = prefix + (" " if is_last else "│ ")
563
+
564
+ for i, child in enumerate(children):
565
+ print_tree(child, child_prefix, i == len(children) - 1)
566
+
567
+ # Get packages to display
568
+ if package:
569
+ packages = [package]
570
+ else:
571
+ installed = list_installed()
572
+ packages = [pkg.get("section_number") for pkg in installed if pkg.get("section_number")]
573
+
574
+ if not packages:
575
+ if json_output:
576
+ click.echo(json.dumps({"trees": [], "message": "No packages installed"}))
577
+ else:
578
+ click.echo("No packages installed")
579
+ return
580
+
581
+ # Build trees
582
+ trees = []
583
+ for pkg in packages:
584
+ tree = build_tree(pkg, set(), 0)
585
+ trees.append(tree)
586
+
587
+ if json_output:
588
+ click.echo(json.dumps({"trees": trees}, indent=2))
589
+ return
590
+
591
+ click.echo()
592
+ for i, tree in enumerate(trees):
593
+ section = tree.get("section", "?")
594
+ version = tree.get("version", "")
595
+ children = tree.get("children", [])
596
+
597
+ # Root node
598
+ root_label = click.style(section, fg="cyan", bold=True)
599
+ if verbose and version:
600
+ root_label += click.style(f" v{version}", fg="yellow")
601
+
602
+ if not children:
603
+ click.echo(f"{root_label} (no dependencies)")
604
+ else:
605
+ click.echo(root_label)
606
+ for j, child in enumerate(children):
607
+ print_tree(child, "", j == len(children) - 1)
608
+
609
+ if i < len(trees) - 1:
610
+ click.echo()
611
+
612
+
613
+ def run_library_info(
614
+ package: str,
615
+ json_output: bool = False,
616
+ verbose: bool = False,
617
+ ) -> None:
618
+ """
619
+ Show detailed package information.
620
+
621
+ Args:
622
+ package: Package section number
623
+ json_output: Output as JSON
624
+ verbose: Verbose output
625
+ """
626
+ from yuho.library import LibraryIndex
627
+
628
+ index = LibraryIndex()
629
+ entry = index.get(package)
630
+
631
+ if not entry:
632
+ if json_output:
633
+ click.echo(json.dumps({"error": f"Package not found: {package}"}))
634
+ else:
635
+ click.echo(f"Package not found: {package}")
636
+ raise SystemExit(1)
637
+
638
+ info = entry.to_dict()
639
+
640
+ if json_output:
641
+ click.echo(json.dumps(info, indent=2))
642
+ return
643
+
644
+ click.echo(click.style(f"\n{info['section_number']}", fg="cyan", bold=True) +
645
+ click.style(f" v{info['version']}", fg="yellow"))
646
+ click.echo(f"\n Title: {info['title']}")
647
+ click.echo(f" Jurisdiction: {info['jurisdiction']}")
648
+ click.echo(f" Contributor: {info['contributor']}")
649
+
650
+ if info.get("description"):
651
+ click.echo(f" Description: {info['description']}")
652
+
653
+ if info.get("tags"):
654
+ click.echo(f" Tags: {', '.join(info['tags'])}")
655
+
656
+ click.echo()