Coverage for src / lexigram / ui / cli / add.py: 91%
69 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""``lexigram-ui add`` — copy components into the user's project."""
3from __future__ import annotations
5from pathlib import Path
6import shutil
8import typer
10from lexigram.ui.cli.registry import COMPONENT_REGISTRY, ComponentEntry
12app = typer.Typer(name="add")
15@app.command()
16def add(
17 component_name: str = typer.Argument(
18 ..., help="Component name (e.g. 'button', 'card')"
19 ),
20 output_dir: str = typer.Option(
21 "src/components/ui", "--output", "-o", help="Output directory"
22 ),
23 force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
24) -> None:
25 """Copy a UI component into your project."""
26 entry = COMPONENT_REGISTRY.get(component_name)
27 if entry is None:
28 available = ", ".join(sorted(COMPONENT_REGISTRY))
29 typer.echo(f"Unknown component: {component_name!r}", err=True)
30 typer.echo(f"Available: {available}", err=True)
31 raise typer.Exit(1)
33 ui_pkg = _find_ui_package()
34 if ui_pkg is None:
35 typer.echo(
36 "Cannot locate lexigram-ui source. Install it in editable mode:\n"
37 " uv pip install -e path/to/lexigram-ui",
38 err=True,
39 )
40 raise typer.Exit(1)
42 all_files = _collect_files(entry, ui_pkg)
43 out_root = Path(output_dir)
45 copied: list[Path] = []
46 skipped: list[Path] = []
48 for src in all_files:
49 relative = src.relative_to(ui_pkg)
50 dest = out_root / relative
51 if dest.exists() and not force:
52 skipped.append(dest)
53 continue
54 dest.parent.mkdir(parents=True, exist_ok=True)
55 shutil.copy2(src, dest)
56 copied.append(dest)
58 if copied:
59 typer.echo(f"Added {component_name} component:")
60 for p in copied:
61 typer.echo(f" Created: {p}")
62 if skipped:
63 typer.echo(" Skipped (use --force to overwrite):")
64 for p in skipped:
65 typer.echo(f" {p}")
66 if not copied:
67 typer.echo("No files were copied (all already exist).")
70def _find_ui_package() -> Path | None:
71 """Locate the lexigram-ui source directory (package root containing lexigram/)."""
72 import lexigram.ui # noqa: F811
74 pkg_path = Path(lexigram.ui.__file__).resolve().parent
75 # pkg_path is the ui module directory
76 if pkg_path.name == "ui" and pkg_path.parent.name == "lexigram":
77 # source layout: .../src/lexigram/ui/ or .../lexigram/ui/
78 parent = pkg_path.parent.parent # .../src/ or .../
79 if (parent / "lexigram" / "ui").exists():
80 return parent
81 # Check if src/ prefix is present
82 if parent.name == "src" and (parent.parent / "lexigram" / "ui").exists():
83 return parent.parent
84 return pkg_path.parent
87def _collect_files(entry: ComponentEntry, ui_pkg: Path) -> list[Path]:
88 """Collect all source files for a component and its dependencies."""
89 seen: set[Path] = set()
90 result: list[Path] = []
92 def _add(dep_path: str) -> None:
93 full = (ui_pkg / dep_path).resolve()
94 if full.exists() and full not in seen:
95 seen.add(full)
96 result.append(full)
97 for other in COMPONENT_REGISTRY.values():
98 if dep_path.endswith(other.source_path):
99 for subdep in other.dependencies:
100 _add(subdep)
102 _add(entry.source_path)
103 for dep in entry.dependencies:
104 _add(dep)
106 return result
109if __name__ == "__main__":
110 app()