11from pathlib import Path
2+ from typing import List , Dict , Set
3+ import tomli
4+ import importlib .resources
25
36import copier
47import typer
58from rich .console import Console
69from rich .panel import Panel
10+ from rich .tree import Tree
711
812app = typer .Typer (
913 name = "components" ,
1822DEFAULT_COMPONENTS_DIR = Path ("components/ui" )
1923DEFAULT_BRANCH = "main"
2024
21- # typer cli arg options
22- COMPONENTS_DIR_OPTION = typer .Option (DEFAULT_COMPONENTS_DIR , "--components-dir" , "-d" , help = "Directory to update components in" )
23- REPO_URL_OPTION = typer .Option (DEFAULT_REPO_URL , "--repo-url" , "-r" , help = "Repository URL to update from" )
24- BRANCH_OPTION = typer .Option (DEFAULT_BRANCH , "--branch" , "-b" , help = "Branch, tag, or commit to update from" )
25-
25+ def load_dependencies () -> Dict [str , List [str ]]:
26+ """Load component dependencies from component_dependencies.toml within the package."""
27+ try :
28+ with importlib .resources .open_text ('basic_components' , 'component_dependencies.toml' ) as f :
29+ toml_data = tomli .loads (f .read ())
30+ return toml_data .get ('dependencies' , {})
31+ except Exception as e :
32+ console .print (f"[red]Error loading dependencies: { e } [/red]" )
33+ return {}
34+
35+ def normalize_component_name (name : str ) -> str :
36+ """Convert component references to normalized form"""
37+ # Only normalize if it's an icon reference
38+ if name .startswith ("icons/" ):
39+ # If already in icons/Name format, return as-is
40+ return name
41+
42+ if "/" not in name and name != "icons" :
43+ # Handle bare icon names (without icons/ prefix)
44+ # Convert to PascalCase if needed
45+ if name .lower () in {"check" , "x" , "moon" , "sun" }:
46+ return f"icons/{ name .title ()} "
47+ # Handle compound names
48+ if name .lower () in {"chevron-right" , "chevron-down" , "chevron-up" , "chevrons-up-down" }:
49+ parts = name .split ("-" )
50+ pascal_name = "" .join (p .title () for p in parts )
51+ return f"icons/{ pascal_name } "
52+
53+ return name
54+
55+ def get_component_pattern (component : str ) -> str :
56+ """Get the file pattern for a component."""
57+ if component .startswith ("icons/" ):
58+ icon_name = component .split ("/" )[1 ]
59+ return f"icons/{ icon_name } Icon.jinja"
60+ else :
61+ return f"{ component } /**"
2662
2763def add_component (
28- component : str ,
29- dest_dir : Path ,
30- repo_url : str = DEFAULT_REPO_URL ,
31- branch : str = DEFAULT_BRANCH ,
64+ component : str ,
65+ dest_dir : Path ,
66+ repo_url : str = DEFAULT_REPO_URL ,
67+ branch : str = DEFAULT_BRANCH ,
68+ dry_run : bool = False ,
3269) -> None :
3370 """Add a specific component to the project."""
3471 try :
35- console .print (f"[green]Installing { component } from '{ repo_url } ' ...[/green]" )
72+ console .print (f"[green]Installing { component } ...[/green]" )
73+
74+ # Get the pattern for this component
75+ pattern = get_component_pattern (component )
76+
77+ # Build exclude list - exclude everything except our pattern
78+ excludes = ["*" , f"!{ pattern } " ]
79+
80+ # Debug output
81+ console .print ("[yellow]Debug: Copying with args:[/yellow]" )
82+ console .print (f" src_path: { repo_url } " )
83+ console .print (f" dst_path: { dest_dir } " )
84+ console .print (f" exclude patterns: { excludes } " )
85+ console .print (f" vcs_ref: { branch } " )
3686
3787 copier .run_copy (
3888 src_path = repo_url ,
3989 dst_path = str (dest_dir ),
40- exclude = [
41- "*" ,
42- f"!{ component } " ,
43- ],
90+ exclude = excludes ,
4491 vcs_ref = branch ,
92+ pretend = dry_run ,
4593 )
4694
47- except Exception as e : # pyright: ignore [reportAttributeAccessIssue]
48- console .print (f"[red]Error: { str (e )} [/red]" )
95+ except Exception as e :
96+ console .print (f"[red]Error installing { component } : { str (e )} [/red]" )
4997 raise typer .Exit (1 )
5098
99+ def display_installation_plan (component : str , dependencies : Set [str ], dry_run : bool = False ) -> None :
100+ """Display what will be installed in a tree format"""
101+ tree = Tree (
102+ f"[bold cyan]{ component } [/bold cyan] "
103+ f"[dim]({ 'preview' if dry_run else 'will be installed' } )[/dim]"
104+ )
105+
106+ if dependencies :
107+ deps_branch = tree .add ("[bold yellow]Dependencies[/bold yellow]" )
108+ for dep in sorted (dependencies ):
109+ deps_branch .add (f"[green]{ dep } [/green]" )
110+
111+ console .print (tree )
51112
52113@app .command ()
53114def add (
54- component : str = typer .Argument (..., help = "Name of the component to install" ),
55- branch : str = typer .Option (
56- DEFAULT_BRANCH , "--branch" , "-b" , help = "Branch, tag, or commit to install from"
57- ),
58- repo_url : str = typer .Option (
59- DEFAULT_REPO_URL , "--repo-url" , "-r" , help = "Repository URL to use"
60- ),
61- components_dir : Path = typer .Option (
62- DEFAULT_COMPONENTS_DIR , "--components-dir" , "-d" , help = "Directory to install components"
63- )
115+ component : str = typer .Argument (..., help = "Name of the component to install" ),
116+ branch : str = typer .Option (
117+ DEFAULT_BRANCH , "--branch" , "-b" , help = "Branch, tag, or commit to install from"
118+ ),
119+ repo_url : str = typer .Option (
120+ DEFAULT_REPO_URL , "--repo-url" , "-r" , help = "Repository URL to use"
121+ ),
122+ components_dir : Path = typer .Option (
123+ DEFAULT_COMPONENTS_DIR , "--components-dir" , "-d" , help = "Directory to install components"
124+ ),
125+ with_deps : bool = typer .Option (
126+ True , "--with-deps/--no-deps" , help = "Install dependencies automatically"
127+ ),
128+ dry_run : bool = typer .Option (
129+ False , "--dry-run" , help = "Preview what would be installed without making changes"
130+ )
64131) -> None :
65132 """Add a component to your project."""
66133 try :
67- add_component (component , components_dir , repo_url , branch )
134+ # Load dependencies
135+ deps_map = load_dependencies ()
136+
137+ # Normalize component name
138+ component = normalize_component_name (component )
139+
140+ # Get all dependencies if requested
141+ components_to_install = {component }
142+ if with_deps :
143+ dependencies = set (deps_map .get (component , []))
144+ if dependencies :
145+ console .print (f"\n [yellow]Debug: Found dependencies: { dependencies } [/yellow]" )
146+ components_to_install .update (dependencies )
147+ else :
148+ dependencies = set ()
149+
150+ # Display installation plan
151+ display_installation_plan (component , dependencies , dry_run )
152+
153+ if dry_run :
154+ console .print ("\n [yellow]Dry run complete. No changes made.[/yellow]" )
155+ return
156+
157+ # Install each component separately with its own exclude pattern
158+ installed = []
159+ for comp in sorted (components_to_install ):
160+ console .print (f"\n [yellow]Debug: Installing component: { comp } [/yellow]" )
161+ add_component (comp , components_dir , repo_url , branch , dry_run )
162+ installed .append (comp )
163+
164+ # Show completion message
165+ deps_msg = "\n [cyan]Installed dependencies:[/cyan]\n " + "\n " .join (
166+ f" - { comp } " for comp in installed [1 :]
167+ ) if len (installed ) > 1 else ""
68168
69169 console .print (
70170 Panel (
71- f"[green]✓[/green] Added { component } component\n \n "
72- f"[cyan] components-dir={ components_dir } [/cyan]" ,
171+ f"[green]✓[/green] Added { component } component{ deps_msg } \n \n "
172+ f"[cyan]components-dir={ components_dir } [/cyan]" ,
73173 title = "Installation Complete" ,
74174 border_style = "green" ,
75175 )
76176 )
177+
77178 except Exception as e :
78179 console .print (f"[red]Error: { str (e )} [/red]" )
79180 raise typer .Exit (1 )
80181
81-
82182@app .command ()
83183def init (
84- components_dir : Path = COMPONENTS_DIR_OPTION ,
184+ components_dir : Path = typer .Option (
185+ DEFAULT_COMPONENTS_DIR ,
186+ "--components-dir" ,
187+ "-d" ,
188+ help = "Directory to install components"
189+ ),
85190) -> None :
86191 """Initialize project for basic-components."""
87- # Create components directory structure
88192 components_dir .mkdir (parents = True , exist_ok = True )
89193
90194 console .print (
@@ -106,6 +210,5 @@ def init(
106210 )
107211 )
108212
109-
110213if __name__ == "__main__" :
111214 app ()
0 commit comments