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
yuho/cli/main.py ADDED
@@ -0,0 +1,1064 @@
1
+ """
2
+ Main CLI entry point using Click.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional, List
9
+
10
+ import click
11
+
12
+ from yuho import __version__
13
+
14
+
15
+ def _detect_color_support() -> bool:
16
+ """
17
+ Auto-detect terminal color support.
18
+
19
+ Checks for:
20
+ - TTY attached to stdout
21
+ - NO_COLOR environment variable (force disable)
22
+ - FORCE_COLOR environment variable (force enable)
23
+ - TERM environment variable
24
+ """
25
+ # NO_COLOR spec: https://no-color.org/
26
+ if os.environ.get("NO_COLOR"):
27
+ return False
28
+
29
+ # FORCE_COLOR always wins
30
+ if os.environ.get("FORCE_COLOR"):
31
+ return True
32
+
33
+ # Check if stdout is a TTY
34
+ if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
35
+ return False
36
+
37
+ # Check TERM variable
38
+ term = os.environ.get("TERM", "")
39
+ if term == "dumb":
40
+ return False
41
+
42
+ return True
43
+
44
+
45
+ # Click context settings
46
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
47
+
48
+
49
+ @click.group(context_settings=CONTEXT_SETTINGS)
50
+ @click.version_option(version=__version__, prog_name="yuho")
51
+ @click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
52
+ @click.option(
53
+ "--color/--no-color",
54
+ "use_color",
55
+ default=None,
56
+ help="Force color output on/off (auto-detected by default)"
57
+ )
58
+ @click.option(
59
+ "-q", "--quiet",
60
+ is_flag=True,
61
+ help="Suppress non-error output"
62
+ )
63
+ @click.pass_context
64
+ def cli(ctx: click.Context, verbose: bool, use_color: Optional[bool], quiet: bool) -> None:
65
+ """
66
+ Yuho - A domain-specific language for encoding legal statutes.
67
+
68
+ Use 'yuho <command> --help' for command-specific help.
69
+ """
70
+ ctx.ensure_object(dict)
71
+ ctx.obj["verbose"] = verbose and not quiet # Quiet overrides verbose
72
+ ctx.obj["quiet"] = quiet
73
+
74
+ # Determine color setting
75
+ if use_color is None:
76
+ # Auto-detect
77
+ ctx.obj["color"] = _detect_color_support()
78
+ else:
79
+ ctx.obj["color"] = use_color
80
+
81
+ # Set global color state for error_formatter
82
+ from yuho.cli import error_formatter
83
+ error_formatter.COLOR_ENABLED = ctx.obj["color"]
84
+
85
+
86
+ # =============================================================================
87
+ # Check command
88
+ # =============================================================================
89
+
90
+
91
+ @cli.command()
92
+ @click.argument("file", type=click.Path(exists=True))
93
+ @click.option("--json", "json_output", is_flag=True, help="Output errors as JSON")
94
+ @click.option("--explain-error", "explain_errors", is_flag=True,
95
+ help="Show detailed explanations for errors with common causes and fixes")
96
+ @click.pass_context
97
+ def check(ctx: click.Context, file: str, json_output: bool, explain_errors: bool) -> None:
98
+ """
99
+ Parse and validate a Yuho source file.
100
+
101
+ Runs syntax checking and semantic analysis, reporting any errors found.
102
+ """
103
+ from yuho.cli.commands.check import run_check
104
+ run_check(file, json_output=json_output, verbose=ctx.obj["verbose"], explain_errors=explain_errors)
105
+
106
+
107
+ # =============================================================================
108
+ # AST command
109
+ # =============================================================================
110
+
111
+
112
+ @cli.command()
113
+ @click.argument("file", type=click.Path(exists=True))
114
+ @click.option("-o", "--output", type=click.Path(), help="Output file path")
115
+ @click.option("--tree", "show_tree", is_flag=True, default=True, help="Show tree visualization (default)")
116
+ @click.option("--stats", is_flag=True, help="Show AST statistics")
117
+ @click.option("--depth", type=int, default=0, help="Max depth (0 = unlimited)")
118
+ @click.option("--ascii", "no_unicode", is_flag=True, help="Use ASCII-only characters")
119
+ @click.pass_context
120
+ def ast(
121
+ ctx: click.Context,
122
+ file: str,
123
+ output: Optional[str],
124
+ show_tree: bool,
125
+ stats: bool,
126
+ depth: int,
127
+ no_unicode: bool,
128
+ ) -> None:
129
+ """
130
+ Visualize AST structure as tree.
131
+
132
+ Displays the abstract syntax tree of a Yuho statute using
133
+ tree-like ASCII art in the terminal.
134
+
135
+ Examples:
136
+ yuho ast statute.yh
137
+ yuho ast statute.yh --stats
138
+ yuho ast statute.yh --ascii -o tree.txt
139
+ """
140
+ from yuho.cli.commands.ast_viz import run_ast_viz
141
+ run_ast_viz(
142
+ file=file,
143
+ output=output,
144
+ stats=stats,
145
+ depth=depth,
146
+ no_unicode=no_unicode,
147
+ verbose=ctx.obj["verbose"],
148
+ color=ctx.obj["color"],
149
+ )
150
+
151
+
152
+ # =============================================================================
153
+ # Transpile command
154
+ # =============================================================================
155
+
156
+
157
+ @cli.command()
158
+ @click.argument("file", type=click.Path(exists=True))
159
+ @click.option(
160
+ "-t", "--target",
161
+ type=click.Choice(["json", "jsonld", "english", "latex", "mermaid", "alloy", "graphql", "blocks"], case_sensitive=False),
162
+ default="json",
163
+ help="Transpilation target format"
164
+ )
165
+ @click.option("-o", "--output", type=click.Path(), help="Output file path")
166
+ @click.option("--dir", "output_dir", type=click.Path(), help="Output directory for multiple files")
167
+ @click.option("--all", "all_targets", is_flag=True, help="Generate all targets")
168
+ @click.option("--json", "json_output", is_flag=True, help="Output metadata as JSON")
169
+ @click.pass_context
170
+ def transpile(
171
+ ctx: click.Context,
172
+ file: str,
173
+ target: str,
174
+ output: Optional[str],
175
+ output_dir: Optional[str],
176
+ all_targets: bool,
177
+ json_output: bool
178
+ ) -> None:
179
+ """
180
+ Transpile a Yuho source file to another format.
181
+
182
+ Supported targets: json, jsonld, english, latex, mermaid, alloy, graphql, blocks
183
+ """
184
+ from yuho.cli.commands.transpile import run_transpile
185
+ run_transpile(
186
+ file,
187
+ target=target,
188
+ output=output,
189
+ output_dir=output_dir,
190
+ all_targets=all_targets,
191
+ json_output=json_output,
192
+ verbose=ctx.obj["verbose"]
193
+ )
194
+
195
+
196
+ # =============================================================================
197
+ # Preview command
198
+ # =============================================================================
199
+
200
+
201
+ @cli.command()
202
+ @click.argument("file", type=click.Path(exists=True))
203
+ @click.option(
204
+ "-t", "--target",
205
+ type=click.Choice(["english", "mermaid"], case_sensitive=False),
206
+ default="english",
207
+ help="Preview format (english or mermaid)"
208
+ )
209
+ @click.option("-p", "--port", type=int, default=8000, help="Server port")
210
+ @click.option("--no-browser", is_flag=True, help="Don't auto-open browser")
211
+ @click.pass_context
212
+ def preview(
213
+ ctx: click.Context,
214
+ file: str,
215
+ target: str,
216
+ port: int,
217
+ no_browser: bool,
218
+ ) -> None:
219
+ """
220
+ Live preview with auto-reload on file changes.
221
+
222
+ Watches the file for changes, transpiles to the target format,
223
+ and serves a live-reloading preview in your browser.
224
+
225
+ Examples:
226
+ yuho preview statute.yh
227
+ yuho preview statute.yh --target mermaid
228
+ yuho preview statute.yh --port 3000 --no-browser
229
+ """
230
+ from yuho.cli.commands.preview import run_preview
231
+ run_preview(
232
+ file=file,
233
+ target=target.lower(),
234
+ port=port,
235
+ no_browser=no_browser,
236
+ verbose=ctx.obj["verbose"],
237
+ color=ctx.obj["color"],
238
+ )
239
+
240
+
241
+ # =============================================================================
242
+ # REPL command
243
+ # =============================================================================
244
+
245
+
246
+ @cli.command()
247
+ @click.pass_context
248
+ def repl(ctx: click.Context) -> None:
249
+ """
250
+ Start interactive REPL for statute experimentation.
251
+
252
+ The REPL provides an interactive environment for:
253
+ - Parsing and validating Yuho code snippets
254
+ - Transpiling to various targets (json, english, mermaid, etc.)
255
+ - Exploring statute definitions and AST structure
256
+ - Testing legal logic interactively
257
+
258
+ Type 'help' within the REPL for available commands.
259
+ """
260
+ from yuho.cli.commands.repl import run_repl
261
+ sys.exit(run_repl(color=ctx.obj["color"], verbose=ctx.obj["verbose"]))
262
+
263
+
264
+ # =============================================================================
265
+ # Explain command
266
+ # =============================================================================
267
+
268
+
269
+ @cli.command()
270
+ @click.argument("file", type=click.Path(exists=True))
271
+ @click.option("-s", "--section", help="Explain specific section only")
272
+ @click.option("-i", "--interactive", is_flag=True, help="Interactive REPL mode")
273
+ @click.option("--provider", type=click.Choice(["ollama", "huggingface", "openai", "anthropic"]),
274
+ help="LLM provider to use")
275
+ @click.option("--model", help="Model name to use")
276
+ @click.option("--stream/--no-stream", "stream", default=True,
277
+ help="Enable/disable streaming output for real-time response")
278
+ @click.pass_context
279
+ def explain(
280
+ ctx: click.Context,
281
+ file: str,
282
+ section: Optional[str],
283
+ interactive: bool,
284
+ provider: Optional[str],
285
+ model: Optional[str],
286
+ stream: bool,
287
+ ) -> None:
288
+ """
289
+ Generate natural language explanation of a Yuho file.
290
+
291
+ Uses LLM to explain statutes in plain language.
292
+ """
293
+ from yuho.cli.commands.explain import run_explain
294
+ run_explain(
295
+ file,
296
+ section=section,
297
+ interactive=interactive,
298
+ provider=provider,
299
+ model=model,
300
+ verbose=ctx.obj["verbose"],
301
+ stream=stream,
302
+ )
303
+
304
+
305
+ # =============================================================================
306
+ # Diff command
307
+ # =============================================================================
308
+
309
+
310
+ @cli.command()
311
+ @click.argument("file1", type=click.Path(exists=True))
312
+ @click.argument("file2", type=click.Path(exists=True))
313
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
314
+ @click.pass_context
315
+ def diff(ctx: click.Context, file1: str, file2: str, json_output: bool) -> None:
316
+ """
317
+ Compare two Yuho files and show semantic differences.
318
+
319
+ Shows added, removed, and modified statutes, definitions, elements,
320
+ penalties, and illustrations between two versions.
321
+
322
+ Examples:
323
+ yuho diff old.yh new.yh
324
+ yuho diff v1/statute.yh v2/statute.yh --json
325
+ """
326
+ from yuho.cli.commands.diff import run_diff
327
+ run_diff(
328
+ file1,
329
+ file2,
330
+ json_output=json_output,
331
+ verbose=ctx.obj["verbose"],
332
+ color=ctx.obj["color"],
333
+ )
334
+
335
+
336
+ # =============================================================================
337
+ # Graph command
338
+ # =============================================================================
339
+
340
+
341
+ @cli.command()
342
+ @click.argument("file", type=click.Path(exists=True))
343
+ @click.option(
344
+ "-f", "--format",
345
+ type=click.Choice(["dot", "mermaid"], case_sensitive=False),
346
+ default="mermaid",
347
+ help="Output format"
348
+ )
349
+ @click.option("-o", "--output", type=click.Path(), help="Output file path")
350
+ @click.pass_context
351
+ def graph(ctx: click.Context, file: str, format: str, output: Optional[str]) -> None:
352
+ """
353
+ Visualize statute dependencies as a graph.
354
+
355
+ Generates a dependency graph showing:
356
+ - Statute cross-references
357
+ - Import relationships
358
+ - Type and function definitions
359
+
360
+ Examples:
361
+ yuho graph statute.yh
362
+ yuho graph statute.yh --format dot -o deps.dot
363
+ yuho graph statute.yh --format mermaid > deps.md
364
+ """
365
+ from yuho.cli.commands.graph import run_graph
366
+ run_graph(
367
+ file,
368
+ format=format,
369
+ output=output,
370
+ verbose=ctx.obj["verbose"],
371
+ color=ctx.obj["color"],
372
+ )
373
+
374
+
375
+ # =============================================================================
376
+ # Lint command
377
+ # =============================================================================
378
+
379
+
380
+ @cli.command()
381
+ @click.argument("files", nargs=-1, type=click.Path(exists=True), required=True)
382
+ @click.option("--rule", "-r", "rules", multiple=True, help="Specific rules to run")
383
+ @click.option("--exclude", "-e", "exclude_rules", multiple=True, help="Rules to exclude")
384
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
385
+ @click.option("--fix", is_flag=True, help="Auto-fix issues where possible")
386
+ @click.pass_context
387
+ def lint(
388
+ ctx: click.Context,
389
+ files: tuple,
390
+ rules: tuple,
391
+ exclude_rules: tuple,
392
+ json_output: bool,
393
+ fix: bool,
394
+ ) -> None:
395
+ """
396
+ Check Yuho files for style and best practice issues.
397
+
398
+ Analyzes files for:
399
+ - Missing statute titles, elements, or penalties
400
+ - Naming convention violations
401
+ - Unused definitions
402
+ - Duplicate section numbers
403
+
404
+ Examples:
405
+ yuho lint statute.yh
406
+ yuho lint *.yh --exclude missing-penalty
407
+ yuho lint src/ --json
408
+ """
409
+ from yuho.cli.commands.lint import run_lint
410
+ run_lint(
411
+ list(files),
412
+ rules=list(rules) if rules else None,
413
+ exclude_rules=list(exclude_rules) if exclude_rules else None,
414
+ json_output=json_output,
415
+ verbose=ctx.obj["verbose"],
416
+ color=ctx.obj["color"],
417
+ fix=fix,
418
+ )
419
+
420
+
421
+ # =============================================================================
422
+ # API command
423
+ # =============================================================================
424
+
425
+
426
+ @cli.command()
427
+ @click.option("-p", "--port", type=int, default=8080, help="Port to listen on")
428
+ @click.option("--host", default="127.0.0.1", help="Host to bind to")
429
+ @click.pass_context
430
+ def api(ctx: click.Context, port: int, host: str) -> None:
431
+ """
432
+ Start the REST API server for remote operations.
433
+
434
+ Provides HTTP endpoints for:
435
+ - Parsing and validating Yuho source code
436
+ - Transpiling to various formats
437
+ - Running lint checks
438
+
439
+ Examples:
440
+ yuho api
441
+ yuho api --port 3000 --host 0.0.0.0
442
+ """
443
+ from yuho.cli.commands.api import run_api
444
+ run_api(
445
+ host=host,
446
+ port=port,
447
+ verbose=ctx.obj["verbose"],
448
+ color=ctx.obj["color"],
449
+ )
450
+
451
+
452
+ # =============================================================================
453
+ # Generate command
454
+ # =============================================================================
455
+
456
+
457
+ @cli.command()
458
+ @click.argument("section")
459
+ @click.option("-t", "--title", required=True, help="Statute title")
460
+ @click.option("-o", "--output", type=click.Path(), help="Output file path")
461
+ @click.option(
462
+ "--template",
463
+ type=click.Choice(["standard", "minimal", "full"], case_sensitive=False),
464
+ default="standard",
465
+ help="Scaffold template type"
466
+ )
467
+ @click.option("--no-definitions", is_flag=True, help="Skip definitions block")
468
+ @click.option("--no-penalty", is_flag=True, help="Skip penalty block")
469
+ @click.option("--no-illustrations", is_flag=True, help="Skip illustrations block")
470
+ @click.option("-f", "--force", is_flag=True, help="Overwrite existing file")
471
+ @click.pass_context
472
+ def generate(
473
+ ctx: click.Context,
474
+ section: str,
475
+ title: str,
476
+ output: Optional[str],
477
+ template: str,
478
+ no_definitions: bool,
479
+ no_penalty: bool,
480
+ no_illustrations: bool,
481
+ force: bool,
482
+ ) -> None:
483
+ """
484
+ Generate statute scaffold with proper structure.
485
+
486
+ Creates a boilerplate .yh file with the specified section number
487
+ and title. Fill in the TODO markers to complete your statute.
488
+
489
+ Examples:
490
+ yuho generate 500 --title "Theft"
491
+ yuho generate s299 --title "Culpable Homicide" --template full
492
+ yuho generate 420 -t "Cheating" -o cheating.yh
493
+ """
494
+ from yuho.cli.commands.generate import run_generate
495
+ run_generate(
496
+ section=section,
497
+ title=title,
498
+ output=output,
499
+ template=template.lower(),
500
+ no_definitions=no_definitions,
501
+ no_penalty=no_penalty,
502
+ no_illustrations=no_illustrations,
503
+ force=force,
504
+ verbose=ctx.obj["verbose"],
505
+ color=ctx.obj["color"],
506
+ )
507
+
508
+
509
+ # =============================================================================
510
+ # Wizard command
511
+ # =============================================================================
512
+
513
+
514
+ @cli.command()
515
+ @click.option("-o", "--output", type=click.Path(), help="Output file path")
516
+ @click.option("--section", help="Pre-set section number")
517
+ @click.option("--title", help="Pre-set statute title")
518
+ @click.pass_context
519
+ def wizard(
520
+ ctx: click.Context,
521
+ output: Optional[str],
522
+ section: Optional[str],
523
+ title: Optional[str],
524
+ ) -> None:
525
+ """
526
+ Interactive wizard for statute creation.
527
+
528
+ Guides you through building a Yuho statute step-by-step using
529
+ inquirer-style prompts. Generates valid .yh source files.
530
+
531
+ Examples:
532
+ yuho wizard
533
+ yuho wizard -o my_statute.yh
534
+ yuho wizard --section 299 --title "Culpable Homicide"
535
+ """
536
+ from yuho.cli.commands.wizard import run_wizard
537
+ run_wizard(
538
+ output=output,
539
+ section=section,
540
+ title=title,
541
+ verbose=ctx.obj["verbose"],
542
+ color=ctx.obj["color"],
543
+ )
544
+
545
+
546
+ # =============================================================================
547
+ # Serve command
548
+ # =============================================================================
549
+
550
+
551
+ @cli.command()
552
+ @click.option("-p", "--port", type=int, default=8080, help="Port to listen on")
553
+ @click.option("--host", default="127.0.0.1", help="Host to bind to")
554
+ @click.option("--stdio", is_flag=True, help="Use stdio transport (for editor integration)")
555
+ @click.pass_context
556
+ def serve(ctx: click.Context, port: int, host: str, stdio: bool) -> None:
557
+ """
558
+ Start the MCP (Model Context Protocol) server.
559
+
560
+ Exposes Yuho functionality to AI assistants and editors.
561
+ """
562
+ from yuho.cli.commands.serve import run_serve
563
+ run_serve(port=port, host=host, stdio=stdio, verbose=ctx.obj["verbose"])
564
+
565
+
566
+ # =============================================================================
567
+ # Contribute command
568
+ # =============================================================================
569
+
570
+
571
+ @cli.command()
572
+ @click.argument("file", type=click.Path(exists=True))
573
+ @click.option("--package", is_flag=True, help="Create distributable .yhpkg archive")
574
+ @click.option("-o", "--output", type=click.Path(), help="Output path for package")
575
+ @click.pass_context
576
+ def contribute(ctx: click.Context, file: str, package: bool, output: Optional[str]) -> None:
577
+ """
578
+ Validate a statute file for contribution to the library.
579
+
580
+ Checks that the file parses correctly and has associated tests.
581
+ """
582
+ from yuho.cli.commands.contribute import run_contribute
583
+ run_contribute(file, package=package, output=output, verbose=ctx.obj["verbose"])
584
+
585
+
586
+ # =============================================================================
587
+ # Init command
588
+ # =============================================================================
589
+
590
+
591
+ @cli.command()
592
+ @click.argument("name", required=False)
593
+ @click.option("-d", "--dir", "directory", type=click.Path(), help="Directory to create project in")
594
+ @click.pass_context
595
+ def init(ctx: click.Context, name: Optional[str], directory: Optional[str]) -> None:
596
+ """
597
+ Initialize a new Yuho statute project.
598
+
599
+ Creates a directory structure with template files.
600
+ """
601
+ from yuho.cli.commands.init import run_init
602
+ run_init(name=name, directory=directory, verbose=ctx.obj["verbose"])
603
+
604
+
605
+ # =============================================================================
606
+ # Format command
607
+ # =============================================================================
608
+
609
+
610
+ @cli.command()
611
+ @click.argument("file", type=click.Path(exists=True))
612
+ @click.option("-i", "--in-place", is_flag=True, help="Format file in place")
613
+ @click.option("--check", is_flag=True, help="Check if file is formatted (exit 1 if not)")
614
+ @click.pass_context
615
+ def fmt(ctx: click.Context, file: str, in_place: bool, check: bool) -> None:
616
+ """
617
+ Format a Yuho source file.
618
+
619
+ Applies canonical formatting to the file.
620
+ """
621
+ from yuho.cli.commands.fmt import run_fmt
622
+ run_fmt(file, in_place=in_place, check=check, verbose=ctx.obj["verbose"])
623
+
624
+
625
+ # =============================================================================
626
+ # Test command
627
+ # =============================================================================
628
+
629
+
630
+ @cli.command()
631
+ @click.argument("file", type=click.Path(exists=True), required=False)
632
+ @click.option("--all", "run_all", is_flag=True, help="Run all tests in directory")
633
+ @click.option("--json", "json_output", is_flag=True, help="Output results as JSON")
634
+ @click.option("--coverage", is_flag=True, help="Enable coverage tracking")
635
+ @click.option("--coverage-html", type=click.Path(), help="Generate HTML coverage report")
636
+ @click.pass_context
637
+ def test(
638
+ ctx: click.Context,
639
+ file: Optional[str],
640
+ run_all: bool,
641
+ json_output: bool,
642
+ coverage: bool,
643
+ coverage_html: Optional[str],
644
+ ) -> None:
645
+ """
646
+ Run tests for a Yuho statute file.
647
+
648
+ Looks for test_<filename>.yh or tests/<filename>_test.yh
649
+
650
+ Examples:
651
+ yuho test statute.yh
652
+ yuho test --all
653
+ yuho test --all --coverage
654
+ yuho test --all --coverage-html coverage.html
655
+ """
656
+ from yuho.cli.commands.test import run_test
657
+ run_test(
658
+ file,
659
+ run_all=run_all,
660
+ json_output=json_output,
661
+ verbose=ctx.obj["verbose"],
662
+ coverage=coverage or bool(coverage_html),
663
+ coverage_html=coverage_html,
664
+ )
665
+
666
+
667
+ # =============================================================================
668
+ # LSP command
669
+ # =============================================================================
670
+
671
+
672
+ @cli.command()
673
+ @click.option("--tcp", type=int, help="Start LSP server on TCP port")
674
+ @click.pass_context
675
+ def lsp(ctx: click.Context, tcp: Optional[int]) -> None:
676
+ """
677
+ Start the Language Server Protocol server.
678
+
679
+ For editor integration (VS Code, Neovim, etc.).
680
+ """
681
+ from yuho.cli.commands.lsp import run_lsp
682
+ run_lsp(tcp=tcp, verbose=ctx.obj["verbose"])
683
+
684
+
685
+ # =============================================================================
686
+ # Completion command
687
+ # =============================================================================
688
+
689
+
690
+ @cli.command()
691
+ @click.argument(
692
+ "shell",
693
+ type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False),
694
+ )
695
+ @click.option(
696
+ "--install", "show_install",
697
+ is_flag=True,
698
+ help="Show installation instructions"
699
+ )
700
+ @click.pass_context
701
+ def completion(ctx: click.Context, shell: str, show_install: bool) -> None:
702
+ """
703
+ Generate shell completion script.
704
+
705
+ Outputs a completion script for the specified shell that can be
706
+ sourced or saved to the appropriate location.
707
+
708
+ Examples:
709
+ # Bash: Add to ~/.bashrc
710
+ eval "$(yuho completion bash)"
711
+
712
+ # Zsh: Add to ~/.zshrc
713
+ eval "$(yuho completion zsh)"
714
+
715
+ # Fish: Save to completions directory
716
+ yuho completion fish > ~/.config/fish/completions/yuho.fish
717
+
718
+ Use --install to see detailed installation instructions.
719
+ """
720
+ from yuho.cli.completions import get_completion_script, get_install_instructions
721
+
722
+ shell_lower = shell.lower()
723
+
724
+ if show_install:
725
+ click.echo(get_install_instructions(shell_lower))
726
+ else:
727
+ click.echo(get_completion_script(shell_lower))
728
+
729
+
730
+ # =============================================================================
731
+ # Config command
732
+ # =============================================================================
733
+
734
+
735
+ @cli.group()
736
+ @click.pass_context
737
+ def config(ctx: click.Context) -> None:
738
+ """
739
+ Manage Yuho configuration.
740
+
741
+ View and modify configuration settings for all Yuho components.
742
+ """
743
+ pass
744
+
745
+
746
+ @config.command("show")
747
+ @click.option("-s", "--section", type=click.Choice(["llm", "transpile", "lsp", "mcp"]),
748
+ help="Show only specific section")
749
+ @click.option("-f", "--format", "fmt", type=click.Choice(["toml", "json"]),
750
+ default="toml", help="Output format")
751
+ @click.pass_context
752
+ def config_show(ctx: click.Context, section: Optional[str], fmt: str) -> None:
753
+ """
754
+ Display current configuration.
755
+
756
+ Shows all configuration values from file and environment.
757
+
758
+ Examples:
759
+ yuho config show
760
+ yuho config show -s llm
761
+ yuho config show -f json
762
+ """
763
+ from yuho.cli.commands.config import run_config_show
764
+ run_config_show(section=section, format=fmt, verbose=ctx.obj["verbose"])
765
+
766
+
767
+ @config.command("set")
768
+ @click.argument("key")
769
+ @click.argument("value")
770
+ @click.pass_context
771
+ def config_set(ctx: click.Context, key: str, value: str) -> None:
772
+ """
773
+ Set a configuration value.
774
+
775
+ KEY must be in format 'section.key' (e.g., 'llm.provider').
776
+
777
+ Examples:
778
+ yuho config set llm.provider ollama
779
+ yuho config set llm.model llama3
780
+ yuho config set mcp.port 9000
781
+ """
782
+ from yuho.cli.commands.config import run_config_set
783
+ run_config_set(key, value, verbose=ctx.obj["verbose"])
784
+
785
+
786
+ @config.command("init")
787
+ @click.option("--force", is_flag=True, help="Overwrite existing config file")
788
+ @click.pass_context
789
+ def config_init(ctx: click.Context, force: bool) -> None:
790
+ """
791
+ Create a default configuration file.
792
+
793
+ Creates ~/.config/yuho/config.toml with sensible defaults.
794
+ """
795
+ from yuho.cli.commands.config import run_config_init
796
+ run_config_init(force=force, verbose=ctx.obj["verbose"])
797
+
798
+
799
+ # =============================================================================
800
+ # Library command
801
+ # =============================================================================
802
+
803
+
804
+ @cli.group()
805
+ @click.pass_context
806
+ def library(ctx: click.Context) -> None:
807
+ """
808
+ Manage Yuho statute packages.
809
+
810
+ Search, install, update, and manage statute packages from the library.
811
+ """
812
+ pass
813
+
814
+
815
+ @library.command("search")
816
+ @click.argument("query")
817
+ @click.option("-j", "--jurisdiction", help="Filter by jurisdiction")
818
+ @click.option("-t", "--tag", "tags", multiple=True, help="Filter by tag")
819
+ @click.option("-n", "--limit", type=int, default=20, help="Max results")
820
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
821
+ @click.pass_context
822
+ def library_search(
823
+ ctx: click.Context,
824
+ query: str,
825
+ jurisdiction: Optional[str],
826
+ tags: tuple,
827
+ limit: int,
828
+ json_output: bool,
829
+ ) -> None:
830
+ """
831
+ Search for packages in the library.
832
+
833
+ Examples:
834
+ yuho library search theft
835
+ yuho library search S403 --jurisdiction singapore
836
+ """
837
+ from yuho.cli.commands.library import run_library_search
838
+ run_library_search(
839
+ query,
840
+ jurisdiction=jurisdiction,
841
+ tags=list(tags) if tags else None,
842
+ limit=limit,
843
+ json_output=json_output,
844
+ verbose=ctx.obj["verbose"],
845
+ )
846
+
847
+
848
+ @library.command("install")
849
+ @click.argument("package")
850
+ @click.option("-f", "--force", is_flag=True, help="Overwrite existing")
851
+ @click.option("--no-deps", is_flag=True, help="Skip dependencies")
852
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
853
+ @click.pass_context
854
+ def library_install(
855
+ ctx: click.Context,
856
+ package: str,
857
+ force: bool,
858
+ no_deps: bool,
859
+ json_output: bool,
860
+ ) -> None:
861
+ """
862
+ Install a package from registry or local path.
863
+
864
+ Examples:
865
+ yuho library install S403
866
+ yuho library install ./my-statute.yhpkg
867
+ """
868
+ from yuho.cli.commands.library import run_library_install
869
+ run_library_install(
870
+ package,
871
+ force=force,
872
+ no_deps=no_deps,
873
+ json_output=json_output,
874
+ verbose=ctx.obj["verbose"],
875
+ )
876
+
877
+
878
+ @library.command("uninstall")
879
+ @click.argument("package")
880
+ @click.option("--dry-run", is_flag=True, help="Show what would be uninstalled without doing it")
881
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
882
+ @click.pass_context
883
+ def library_uninstall(
884
+ ctx: click.Context,
885
+ package: str,
886
+ dry_run: bool,
887
+ json_output: bool,
888
+ ) -> None:
889
+ """
890
+ Uninstall an installed package.
891
+
892
+ Examples:
893
+ yuho library uninstall S403
894
+ yuho library uninstall S403 --dry-run
895
+ """
896
+ from yuho.cli.commands.library import run_library_uninstall
897
+ run_library_uninstall(
898
+ package,
899
+ dry_run=dry_run,
900
+ json_output=json_output,
901
+ verbose=ctx.obj["verbose"],
902
+ )
903
+
904
+
905
+ @library.command("list")
906
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
907
+ @click.pass_context
908
+ def library_list(ctx: click.Context, json_output: bool) -> None:
909
+ """
910
+ List all installed packages.
911
+ """
912
+ from yuho.cli.commands.library import run_library_list
913
+ run_library_list(json_output=json_output, verbose=ctx.obj["verbose"])
914
+
915
+
916
+ @library.command("update")
917
+ @click.argument("package", required=False)
918
+ @click.option("--all", "all_packages", is_flag=True, help="Update all packages")
919
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
920
+ @click.pass_context
921
+ def library_update(
922
+ ctx: click.Context,
923
+ package: Optional[str],
924
+ all_packages: bool,
925
+ json_output: bool,
926
+ ) -> None:
927
+ """
928
+ Update one or all packages.
929
+
930
+ Without arguments, shows available updates.
931
+ Use --all to apply all updates.
932
+
933
+ Examples:
934
+ yuho library update # Show updates
935
+ yuho library update --all # Apply all updates
936
+ yuho library update S403 # Update specific package
937
+ """
938
+ from yuho.cli.commands.library import run_library_update
939
+ run_library_update(
940
+ package,
941
+ all_packages=all_packages,
942
+ json_output=json_output,
943
+ verbose=ctx.obj["verbose"],
944
+ )
945
+
946
+
947
+ @library.command("publish")
948
+ @click.argument("path", type=click.Path(exists=True))
949
+ @click.option("--registry", help="Registry URL")
950
+ @click.option("--token", help="Auth token")
951
+ @click.option("--dry-run", is_flag=True, help="Validate package without publishing")
952
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
953
+ @click.pass_context
954
+ def library_publish(
955
+ ctx: click.Context,
956
+ path: str,
957
+ registry: Optional[str],
958
+ token: Optional[str],
959
+ dry_run: bool,
960
+ json_output: bool,
961
+ ) -> None:
962
+ """
963
+ Publish a package to the registry.
964
+
965
+ Examples:
966
+ yuho library publish ./my-statute --token $YUHO_TOKEN
967
+ yuho library publish ./my-statute --dry-run
968
+ """
969
+ from yuho.cli.commands.library import run_library_publish
970
+ run_library_publish(
971
+ path,
972
+ registry=registry,
973
+ token=token,
974
+ dry_run=dry_run,
975
+ json_output=json_output,
976
+ verbose=ctx.obj["verbose"],
977
+ )
978
+
979
+
980
+ @library.command("info")
981
+ @click.argument("package")
982
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
983
+ @click.pass_context
984
+ def library_info(
985
+ ctx: click.Context,
986
+ package: str,
987
+ json_output: bool,
988
+ ) -> None:
989
+ """
990
+ Show detailed package information.
991
+
992
+ Examples:
993
+ yuho library info S403
994
+ """
995
+ from yuho.cli.commands.library import run_library_info
996
+ run_library_info(
997
+ package,
998
+ json_output=json_output,
999
+ verbose=ctx.obj["verbose"],
1000
+ )
1001
+
1002
+
1003
+ @library.command("outdated")
1004
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
1005
+ @click.pass_context
1006
+ def library_outdated(
1007
+ ctx: click.Context,
1008
+ json_output: bool,
1009
+ ) -> None:
1010
+ """
1011
+ Show packages with updates available.
1012
+
1013
+ Displays installed packages that have newer versions in the registry,
1014
+ along with the type of update (major, minor, patch) and deprecation warnings.
1015
+
1016
+ Examples:
1017
+ yuho library outdated
1018
+ yuho library outdated --json
1019
+ """
1020
+ from yuho.cli.commands.library import run_library_outdated
1021
+ run_library_outdated(
1022
+ json_output=json_output,
1023
+ verbose=ctx.obj["verbose"],
1024
+ )
1025
+
1026
+
1027
+ @library.command("tree")
1028
+ @click.argument("package", required=False)
1029
+ @click.option("--depth", "-d", default=10, help="Maximum depth to display")
1030
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
1031
+ @click.pass_context
1032
+ def library_tree(
1033
+ ctx: click.Context,
1034
+ package: Optional[str],
1035
+ depth: int,
1036
+ json_output: bool,
1037
+ ) -> None:
1038
+ """
1039
+ Show dependency tree for packages.
1040
+
1041
+ Displays a tree visualization of package dependencies. If no package
1042
+ is specified, shows trees for all installed packages.
1043
+
1044
+ Examples:
1045
+ yuho library tree
1046
+ yuho library tree S403
1047
+ yuho library tree --depth 3
1048
+ """
1049
+ from yuho.cli.commands.library import run_library_tree
1050
+ run_library_tree(
1051
+ package=package,
1052
+ depth=depth,
1053
+ json_output=json_output,
1054
+ verbose=ctx.obj["verbose"],
1055
+ )
1056
+
1057
+
1058
+ def main() -> None:
1059
+ """Main entry point."""
1060
+ cli()
1061
+
1062
+
1063
+ if __name__ == "__main__":
1064
+ main()