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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- 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()
|