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,370 @@
1
+ """
2
+ TUI wizard for interactive statute creation.
3
+
4
+ Provides a step-by-step wizard interface for building Yuho statutes
5
+ using inquirer-style prompts. Generates valid .yh files from user input.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Optional, List, Dict, Any
11
+ from dataclasses import dataclass, field
12
+
13
+ import click
14
+
15
+
16
+ @dataclass
17
+ class ElementData:
18
+ """Data for a statute element."""
19
+ element_type: str # "actus_reus", "mens_rea", "circumstance"
20
+ name: str
21
+ description: str
22
+
23
+
24
+ @dataclass
25
+ class DefinitionData:
26
+ """Data for a definition entry."""
27
+ term: str
28
+ definition: str
29
+
30
+
31
+ @dataclass
32
+ class IllustrationData:
33
+ """Data for an illustration."""
34
+ label: str
35
+ description: str
36
+
37
+
38
+ @dataclass
39
+ class PenaltyData:
40
+ """Data for penalty specification."""
41
+ has_imprisonment: bool = False
42
+ imprisonment_min: str = ""
43
+ imprisonment_max: str = ""
44
+ has_fine: bool = False
45
+ fine_min: str = ""
46
+ fine_max: str = ""
47
+ supplementary: str = ""
48
+
49
+
50
+ @dataclass
51
+ class StatuteData:
52
+ """Complete data for a statute."""
53
+ section_number: str = ""
54
+ title: str = ""
55
+ definitions: List[DefinitionData] = field(default_factory=list)
56
+ elements: List[ElementData] = field(default_factory=list)
57
+ penalty: Optional[PenaltyData] = None
58
+ illustrations: List[IllustrationData] = field(default_factory=list)
59
+
60
+
61
+ def prompt_input(prompt: str, default: str = "", required: bool = False) -> str:
62
+ """Prompt for text input."""
63
+ while True:
64
+ if default:
65
+ result = click.prompt(prompt, default=default, show_default=True)
66
+ else:
67
+ result = click.prompt(prompt, default="", show_default=False)
68
+
69
+ if required and not result.strip():
70
+ click.echo(" This field is required.")
71
+ continue
72
+
73
+ return result.strip()
74
+
75
+
76
+ def prompt_confirm(prompt: str, default: bool = False) -> bool:
77
+ """Prompt for yes/no confirmation."""
78
+ return click.confirm(prompt, default=default)
79
+
80
+
81
+ def prompt_choice(prompt: str, choices: List[str], default: int = 0) -> str:
82
+ """Prompt for choice from list."""
83
+ click.echo(f"\n{prompt}")
84
+ for i, choice in enumerate(choices):
85
+ marker = ">" if i == default else " "
86
+ click.echo(f" {marker} [{i + 1}] {choice}")
87
+
88
+ while True:
89
+ value = click.prompt("Enter number", default=str(default + 1))
90
+ try:
91
+ idx = int(value) - 1
92
+ if 0 <= idx < len(choices):
93
+ return choices[idx]
94
+ except ValueError:
95
+ pass
96
+ click.echo(f" Please enter a number between 1 and {len(choices)}")
97
+
98
+
99
+ def wizard_header(title: str) -> None:
100
+ """Print a wizard section header."""
101
+ click.echo("")
102
+ click.echo(click.style(f"═══ {title} ═══", fg="cyan", bold=True))
103
+ click.echo("")
104
+
105
+
106
+ def collect_definitions(data: StatuteData) -> None:
107
+ """Collect definition entries."""
108
+ wizard_header("Definitions")
109
+ click.echo("Define key terms used in the statute.")
110
+ click.echo("(Press Enter with empty term to finish)")
111
+ click.echo("")
112
+
113
+ while True:
114
+ term = prompt_input("Term to define")
115
+ if not term:
116
+ break
117
+
118
+ definition = prompt_input(f" Definition of '{term}'", required=True)
119
+ data.definitions.append(DefinitionData(term=term, definition=definition))
120
+ click.echo(click.style(f" ✓ Added definition: {term}", fg="green"))
121
+
122
+ click.echo(f"\nTotal definitions: {len(data.definitions)}")
123
+
124
+
125
+ def collect_elements(data: StatuteData) -> None:
126
+ """Collect offense elements."""
127
+ wizard_header("Elements of the Offense")
128
+ click.echo("Define the elements (actus reus, mens rea, circumstances)")
129
+ click.echo("")
130
+
131
+ # Actus Reus
132
+ if prompt_confirm("Add actus reus (physical act) element?", default=True):
133
+ name = prompt_input(" Element name/identifier", default="act", required=True)
134
+ desc = prompt_input(" Description of the act", required=True)
135
+ data.elements.append(ElementData(
136
+ element_type="actus_reus",
137
+ name=name,
138
+ description=desc
139
+ ))
140
+ click.echo(click.style(f" ✓ Added actus_reus: {name}", fg="green"))
141
+
142
+ # Mens Rea
143
+ if prompt_confirm("Add mens rea (mental state) element?", default=True):
144
+ name = prompt_input(" Element name/identifier", default="intent", required=True)
145
+ desc = prompt_input(" Description of required mental state", required=True)
146
+ data.elements.append(ElementData(
147
+ element_type="mens_rea",
148
+ name=name,
149
+ description=desc
150
+ ))
151
+ click.echo(click.style(f" ✓ Added mens_rea: {name}", fg="green"))
152
+
153
+ # Additional elements
154
+ while prompt_confirm("Add another element?", default=False):
155
+ element_type = prompt_choice("Element type:", [
156
+ "actus_reus",
157
+ "mens_rea",
158
+ "circumstance"
159
+ ])
160
+ name = prompt_input(" Element name", required=True)
161
+ desc = prompt_input(" Description", required=True)
162
+ data.elements.append(ElementData(
163
+ element_type=element_type,
164
+ name=name,
165
+ description=desc
166
+ ))
167
+ click.echo(click.style(f" ✓ Added {element_type}: {name}", fg="green"))
168
+
169
+ click.echo(f"\nTotal elements: {len(data.elements)}")
170
+
171
+
172
+ def collect_penalty(data: StatuteData) -> None:
173
+ """Collect penalty specification."""
174
+ wizard_header("Penalty")
175
+
176
+ if not prompt_confirm("Does this statute have a penalty clause?", default=True):
177
+ return
178
+
179
+ penalty = PenaltyData()
180
+
181
+ # Imprisonment
182
+ if prompt_confirm("Include imprisonment?", default=True):
183
+ penalty.has_imprisonment = True
184
+ penalty.imprisonment_max = prompt_input(
185
+ " Maximum imprisonment (e.g., '10 years', '3Y')",
186
+ required=True
187
+ )
188
+ if prompt_confirm(" Specify minimum imprisonment?", default=False):
189
+ penalty.imprisonment_min = prompt_input(" Minimum imprisonment")
190
+
191
+ # Fine
192
+ if prompt_confirm("Include fine?", default=True):
193
+ penalty.has_fine = True
194
+ penalty.fine_max = prompt_input(
195
+ " Maximum fine (e.g., 'SGD 10000', '$5000')",
196
+ required=True
197
+ )
198
+ if prompt_confirm(" Specify minimum fine?", default=False):
199
+ penalty.fine_min = prompt_input(" Minimum fine")
200
+
201
+ # Supplementary
202
+ if prompt_confirm("Add supplementary penalty info?", default=False):
203
+ penalty.supplementary = prompt_input(" Supplementary information")
204
+
205
+ data.penalty = penalty
206
+ click.echo(click.style(" ✓ Penalty configured", fg="green"))
207
+
208
+
209
+ def collect_illustrations(data: StatuteData) -> None:
210
+ """Collect illustration examples."""
211
+ wizard_header("Illustrations")
212
+ click.echo("Add examples to illustrate the statute's application.")
213
+ click.echo("(Press Enter with empty label to finish)")
214
+ click.echo("")
215
+
216
+ label_counter = ord('a')
217
+
218
+ while True:
219
+ default_label = f"({chr(label_counter)})"
220
+ label = prompt_input(f"Illustration label", default=default_label)
221
+
222
+ if not label or label == default_label:
223
+ # Check if user wants to add with default label
224
+ if not label:
225
+ break
226
+
227
+ desc = prompt_input(" Illustration text", required=True)
228
+ data.illustrations.append(IllustrationData(label=label, description=desc))
229
+ click.echo(click.style(f" ✓ Added illustration {label}", fg="green"))
230
+ label_counter += 1
231
+
232
+ if not prompt_confirm("Add another illustration?", default=False):
233
+ break
234
+
235
+ click.echo(f"\nTotal illustrations: {len(data.illustrations)}")
236
+
237
+
238
+ def generate_yuho_code(data: StatuteData) -> str:
239
+ """Generate Yuho source code from collected data."""
240
+ lines: List[str] = []
241
+
242
+ # Statute header
243
+ lines.append(f'statute "{data.section_number}" "{data.title}" {{')
244
+
245
+ # Definitions
246
+ if data.definitions:
247
+ lines.append("")
248
+ lines.append(" definitions {")
249
+ for defn in data.definitions:
250
+ escaped_def = defn.definition.replace('"', '\\"')
251
+ lines.append(f' "{defn.term}" := "{escaped_def}";')
252
+ lines.append(" }")
253
+
254
+ # Elements
255
+ if data.elements:
256
+ lines.append("")
257
+ lines.append(" elements {")
258
+ for elem in data.elements:
259
+ escaped_desc = elem.description.replace('"', '\\"')
260
+ lines.append(f' {elem.element_type} {elem.name} := "{escaped_desc}";')
261
+ lines.append(" }")
262
+
263
+ # Penalty
264
+ if data.penalty:
265
+ lines.append("")
266
+ lines.append(" penalty {")
267
+ if data.penalty.has_imprisonment:
268
+ if data.penalty.imprisonment_min:
269
+ lines.append(f" imprisonment := {data.penalty.imprisonment_min} to {data.penalty.imprisonment_max};")
270
+ else:
271
+ lines.append(f" imprisonment := {data.penalty.imprisonment_max};")
272
+ if data.penalty.has_fine:
273
+ if data.penalty.fine_min:
274
+ lines.append(f" fine := {data.penalty.fine_min} to {data.penalty.fine_max};")
275
+ else:
276
+ lines.append(f" fine := {data.penalty.fine_max};")
277
+ if data.penalty.supplementary:
278
+ escaped = data.penalty.supplementary.replace('"', '\\"')
279
+ lines.append(f' supplementary := "{escaped}";')
280
+ lines.append(" }")
281
+
282
+ # Illustrations
283
+ if data.illustrations:
284
+ lines.append("")
285
+ lines.append(" illustrations {")
286
+ for illus in data.illustrations:
287
+ escaped = illus.description.replace('"', '\\"')
288
+ lines.append(f' {illus.label} "{escaped}";')
289
+ lines.append(" }")
290
+
291
+ lines.append("}")
292
+
293
+ return "\n".join(lines)
294
+
295
+
296
+ def run_wizard(
297
+ output: Optional[str] = None,
298
+ section: Optional[str] = None,
299
+ title: Optional[str] = None,
300
+ verbose: bool = False,
301
+ color: bool = True,
302
+ ) -> None:
303
+ """
304
+ Run the interactive statute creation wizard.
305
+
306
+ Args:
307
+ output: Output file path (None = stdout)
308
+ section: Pre-set section number
309
+ title: Pre-set title
310
+ verbose: Enable verbose output
311
+ color: Use colored output
312
+ """
313
+ click.echo(click.style("""
314
+ ╔═══════════════════════════════════════════════════════════╗
315
+ ║ YUHO STATUTE CREATION WIZARD ║
316
+ ║ ║
317
+ ║ This wizard will guide you through creating a new ║
318
+ ║ Yuho statute definition step by step. ║
319
+ ╚═══════════════════════════════════════════════════════════╝
320
+ """, fg="cyan"))
321
+
322
+ data = StatuteData()
323
+
324
+ # Basic info
325
+ wizard_header("Basic Information")
326
+
327
+ data.section_number = section or prompt_input(
328
+ "Section number (e.g., 299, 300A)",
329
+ required=True
330
+ )
331
+
332
+ data.title = title or prompt_input(
333
+ "Statute title (e.g., 'Culpable Homicide')",
334
+ required=True
335
+ )
336
+
337
+ click.echo(click.style(f"\n✓ Creating statute: Section {data.section_number} - {data.title}", fg="green"))
338
+
339
+ # Collect sections
340
+ collect_definitions(data)
341
+ collect_elements(data)
342
+ collect_penalty(data)
343
+ collect_illustrations(data)
344
+
345
+ # Generate code
346
+ wizard_header("Generated Code")
347
+ code = generate_yuho_code(data)
348
+
349
+ click.echo(click.style("```yuho", fg="yellow"))
350
+ click.echo(code)
351
+ click.echo(click.style("```", fg="yellow"))
352
+
353
+ # Save or output
354
+ if output:
355
+ path = Path(output)
356
+ if path.exists() and not prompt_confirm(f"\nOverwrite existing file {path}?", default=False):
357
+ click.echo("Cancelled.")
358
+ return
359
+
360
+ path.write_text(code + "\n")
361
+ click.echo(click.style(f"\n✓ Saved to {path}", fg="green"))
362
+ else:
363
+ if prompt_confirm("\nSave to file?", default=True):
364
+ default_name = f"s{data.section_number.lower()}.yh"
365
+ filename = prompt_input("Filename", default=default_name)
366
+ path = Path(filename)
367
+ path.write_text(code + "\n")
368
+ click.echo(click.style(f"✓ Saved to {path}", fg="green"))
369
+
370
+ click.echo(click.style("\n✓ Wizard complete!", fg="green", bold=True))
@@ -0,0 +1,182 @@
1
+ """
2
+ Shell completion script generation for Yuho CLI.
3
+
4
+ Generates completion scripts for bash, zsh, and fish shells.
5
+ Uses Click's built-in completion support.
6
+ """
7
+
8
+ from typing import Literal
9
+ import click
10
+
11
+ # Shell completion templates
12
+
13
+ BASH_COMPLETION_SCRIPT = '''
14
+ # Yuho bash completion script
15
+ # Add to ~/.bashrc or ~/.bash_completion:
16
+ # source <(yuho --show-completion bash)
17
+ # Or save to /etc/bash_completion.d/yuho
18
+
19
+ _yuho_completion() {
20
+ local IFS=$'\\n'
21
+ COMPREPLY=( $(env COMP_WORDS="${COMP_WORDS[*]}" \\
22
+ COMP_CWORD=$COMP_CWORD \\
23
+ _YUHO_COMPLETE=bash_complete $1) )
24
+ return 0
25
+ }
26
+
27
+ complete -o default -F _yuho_completion yuho
28
+ '''
29
+
30
+ ZSH_COMPLETION_SCRIPT = '''
31
+ #compdef yuho
32
+ # Yuho zsh completion script
33
+ # Add to ~/.zshrc:
34
+ # source <(yuho --show-completion zsh)
35
+ # Or save to a file in $fpath (e.g., ~/.zsh/completions/_yuho)
36
+
37
+ _yuho() {
38
+ local -a completions
39
+ local -a completions_with_descriptions
40
+ local -a response
41
+ (( ! $+commands[yuho] )) && return 1
42
+
43
+ response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _YUHO_COMPLETE=zsh_complete yuho)}")
44
+
45
+ for key descr in ${(kv)response}; do
46
+ if [[ "$descr" == "_" ]]; then
47
+ completions+=("$key")
48
+ else
49
+ completions_with_descriptions+=("$key":"$descr")
50
+ fi
51
+ done
52
+
53
+ if [[ -n $googlemock_completions_with_descriptions ]]; then
54
+ _describe -V unsorted completions_with_descriptions -U
55
+ fi
56
+
57
+ if [[ -n $completions ]]; then
58
+ compadd -U -V unsorted -a completions
59
+ fi
60
+ }
61
+
62
+ compdef _yuho yuho
63
+ '''
64
+
65
+ FISH_COMPLETION_SCRIPT = '''
66
+ # Yuho fish completion script
67
+ # Add to ~/.config/fish/completions/yuho.fish
68
+ # Or source directly: yuho --show-completion fish | source
69
+
70
+ function _yuho_completion
71
+ set -l response (env _YUHO_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) yuho)
72
+
73
+ for completion in $response
74
+ set -l metadata (string split "," -- $completion)
75
+ set -l comp $metadata[1]
76
+ set -l desc ""
77
+ if test (count $metadata) -ge 2
78
+ set desc $metadata[2]
79
+ end
80
+ if test -z "$desc"
81
+ echo -e "$comp"
82
+ else
83
+ echo -e "$comp\\t$desc"
84
+ end
85
+ end
86
+ end
87
+
88
+ complete -c yuho -f -a "(_yuho_completion)"
89
+ '''
90
+
91
+
92
+ ShellType = Literal["bash", "zsh", "fish"]
93
+
94
+
95
+ def get_completion_script(shell: ShellType) -> str:
96
+ """
97
+ Get the shell completion script for the specified shell.
98
+
99
+ Args:
100
+ shell: The shell type ("bash", "zsh", or "fish")
101
+
102
+ Returns:
103
+ The completion script as a string
104
+
105
+ Raises:
106
+ ValueError: If shell type is not supported
107
+ """
108
+ scripts = {
109
+ "bash": BASH_COMPLETION_SCRIPT,
110
+ "zsh": ZSH_COMPLETION_SCRIPT,
111
+ "fish": FISH_COMPLETION_SCRIPT,
112
+ }
113
+
114
+ if shell not in scripts:
115
+ raise ValueError(
116
+ f"Unsupported shell: {shell}. Supported: {', '.join(scripts.keys())}"
117
+ )
118
+
119
+ return scripts[shell].strip()
120
+
121
+
122
+ def print_completion_script(shell: ShellType) -> None:
123
+ """
124
+ Print the completion script for the specified shell.
125
+
126
+ Args:
127
+ shell: The shell type
128
+ """
129
+ click.echo(get_completion_script(shell))
130
+
131
+
132
+ def get_install_instructions(shell: ShellType) -> str:
133
+ """
134
+ Get installation instructions for the completion script.
135
+
136
+ Args:
137
+ shell: The shell type
138
+
139
+ Returns:
140
+ Installation instructions as a string
141
+ """
142
+ instructions = {
143
+ "bash": """
144
+ # Bash completion installation:
145
+ # Option 1: Add to ~/.bashrc
146
+ echo 'eval "$(yuho completion bash)"' >> ~/.bashrc
147
+
148
+ # Option 2: Save to bash_completion.d (requires sudo)
149
+ yuho completion bash | sudo tee /etc/bash_completion.d/yuho
150
+
151
+ # Then restart your shell or run:
152
+ source ~/.bashrc
153
+ """,
154
+ "zsh": """
155
+ # Zsh completion installation:
156
+ # Option 1: Add to ~/.zshrc
157
+ echo 'eval "$(yuho completion zsh)"' >> ~/.zshrc
158
+
159
+ # Option 2: Save to fpath directory
160
+ mkdir -p ~/.zsh/completions
161
+ yuho completion zsh > ~/.zsh/completions/_yuho
162
+ # Then add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)
163
+
164
+ # Then restart your shell or run:
165
+ source ~/.zshrc
166
+ """,
167
+ "fish": """
168
+ # Fish completion installation:
169
+ # Option 1: Save to completions directory
170
+ yuho completion fish > ~/.config/fish/completions/yuho.fish
171
+
172
+ # Option 2: Add to config.fish
173
+ echo 'yuho completion fish | source' >> ~/.config/fish/config.fish
174
+
175
+ # Completions will be available immediately in new shells
176
+ """,
177
+ }
178
+
179
+ if shell not in instructions:
180
+ return f"No installation instructions for shell: {shell}"
181
+
182
+ return instructions[shell].strip()