55from functools import wraps
66from dotenv import load_dotenv , find_dotenv
77import os
8+ import sys
89from humanloop import Humanloop
910from humanloop .sync .sync_client import SyncClient
1011from datetime import datetime
1819if not logger .hasHandlers ():
1920 logger .addHandler (console_handler )
2021
22+ # Color constants
23+ SUCCESS_COLOR = "green"
24+ ERROR_COLOR = "red"
25+ INFO_COLOR = "blue"
26+ WARNING_COLOR = "yellow"
27+
2128def get_client (api_key : Optional [str ] = None , env_file : Optional [str ] = None , base_url : Optional [str ] = None ) -> Humanloop :
2229 """Get a Humanloop client instance."""
2330 if not api_key :
@@ -36,7 +43,7 @@ def get_client(api_key: Optional[str] = None, env_file: Optional[str] = None, ba
3643 api_key = os .getenv ("HUMANLOOP_API_KEY" )
3744 if not api_key :
3845 raise click .ClickException (
39- "No API key found. Set HUMANLOOP_API_KEY in .env file or environment, or use --api-key"
46+ click . style ( "No API key found. Set HUMANLOOP_API_KEY in .env file or environment, or use --api-key" , fg = ERROR_COLOR )
4047 )
4148
4249 return Humanloop (api_key = api_key , base_url = base_url )
@@ -47,21 +54,21 @@ def common_options(f: Callable) -> Callable:
4754 "--api-key" ,
4855 help = "Humanloop API key. If not provided, uses HUMANLOOP_API_KEY from .env or environment." ,
4956 default = None ,
57+ show_default = False ,
5058 )
5159 @click .option (
5260 "--env-file" ,
5361 help = "Path to .env file. If not provided, looks for .env in current directory." ,
5462 default = None ,
5563 type = click .Path (exists = True ),
64+ show_default = False ,
5665 )
5766 @click .option (
5867 "--base-dir" ,
5968 help = "Base directory for synced files" ,
6069 default = "humanloop" ,
6170 type = click .Path (),
6271 )
63- # Hidden option for internal use - allows overriding the Humanloop API base URL
64- # Can be set via --base-url or HUMANLOOP_BASE_URL environment variable
6572 @click .option (
6673 "--base-url" ,
6774 default = None ,
@@ -79,20 +86,29 @@ def wrapper(*args, **kwargs):
7986 try :
8087 return f (* args , ** kwargs )
8188 except Exception as e :
82- logger . error ( f"Error during sync operation : { str ( e ) } " )
83- raise click . ClickException ( str ( e ) )
89+ click . echo ( click . style ( str ( f"Error: { e } " ), fg = ERROR_COLOR ) )
90+ sys . exit ( 1 )
8491 return wrapper
8592
86- @click .group ()
87- def cli ():
93+ @click .group (
94+ help = "Humanloop CLI for managing sync operations." ,
95+ context_settings = {
96+ "help_option_names" : ["-h" , "--help" ],
97+ "max_content_width" : 100 ,
98+ }
99+ )
100+ @common_options
101+ def cli (api_key : Optional [str ], env_file : Optional [str ], base_dir : str , base_url : Optional [str ]):
88102 """Humanloop CLI for managing sync operations."""
89103 pass
90104
91105@cli .command ()
92106@click .option (
93107 "--path" ,
94108 "-p" ,
95- help = "Path to pull (file or directory). If not provided, pulls everything." ,
109+ help = "Path to pull (file or directory). If not provided, pulls everything. " +
110+ "To pull a specific file, ensure the extension for the file is included (e.g. .prompt or .agent). " +
111+ "To pull a directory, simply specify the path to the directory (e.g. abc/def to pull all files under abc/def and its subdirectories)." ,
96112 default = None ,
97113)
98114@click .option (
@@ -101,37 +117,37 @@ def cli():
101117 help = "Environment to pull from (e.g. 'production', 'staging')" ,
102118 default = None ,
103119)
104- @common_options
105120@handle_sync_errors
106121def pull (path : Optional [str ], environment : Optional [str ], api_key : Optional [str ], env_file : Optional [str ], base_dir : str , base_url : Optional [str ]):
107- """Pull files from Humanloop to local filesystem.
108-
109- If PATH is provided and ends with .prompt or .agent, pulls that specific file.
110- Otherwise, pulls all files under the specified directory path.
111- If no PATH is provided, pulls all files from the root.
112- """
122+ """Pull files from Humanloop to your local filesystem."""
113123 client = get_client (api_key , env_file , base_url )
114124 sync_client = SyncClient (client , base_dir = base_dir )
115125
116- click .echo ("Pulling files from Humanloop..." )
126+ click .echo (click . style ( "Pulling files from Humanloop..." , fg = INFO_COLOR ) )
117127
118- click .echo (f"Path: { path or '(root)' } " )
119- click .echo (f"Environment: { environment or '(default)' } " )
128+ click .echo (click . style ( f"Path: { path or '(root)' } " , fg = INFO_COLOR ) )
129+ click .echo (click . style ( f"Environment: { environment or '(default)' } " , fg = INFO_COLOR ) )
120130
121131 successful_files = sync_client .pull (path , environment )
122132
123133 # Get metadata about the operation
124134 metadata = sync_client .metadata .get_last_operation ()
125135 if metadata :
126- click .echo (f"\n Sync completed in { metadata ['duration_ms' ]} ms" )
136+ # Determine if the operation was successful based on failed_files
137+ is_successful = not metadata .get ('failed_files' ) and not metadata .get ('error' )
138+ duration_color = SUCCESS_COLOR if is_successful else ERROR_COLOR
139+ click .echo (click .style (f"\n Sync completed in { metadata ['duration_ms' ]} ms" , fg = duration_color ))
140+
127141 if metadata ['successful_files' ]:
128- click .echo (f"\n Successfully synced { len (metadata ['successful_files' ])} files:" )
142+ click .echo (click . style ( f"\n Successfully synced { len (metadata ['successful_files' ])} files:" , fg = SUCCESS_COLOR ) )
129143 for file in metadata ['successful_files' ]:
130- click .echo (f" ✓ { file } " )
144+ click .echo (click . style ( f" ✓ { file } " , fg = SUCCESS_COLOR ) )
131145 if metadata ['failed_files' ]:
132- click .echo (f"\n Failed to sync { len (metadata ['failed_files' ])} files:" )
146+ click .echo (click . style ( f"\n Failed to sync { len (metadata ['failed_files' ])} files:" , fg = ERROR_COLOR ) )
133147 for file in metadata ['failed_files' ]:
134- click .echo (f" ✗ { file } " )
148+ click .echo (click .style (f" ✗ { file } " , fg = ERROR_COLOR ))
149+ if metadata .get ('error' ):
150+ click .echo (click .style (f"\n Error: { metadata ['error' ]} " , fg = ERROR_COLOR ))
135151
136152def format_timestamp (timestamp : str ) -> str :
137153 """Format timestamp to a more readable format."""
@@ -147,7 +163,6 @@ def format_timestamp(timestamp: str) -> str:
147163 is_flag = True ,
148164 help = "Display history in a single line per operation" ,
149165)
150- @common_options
151166@handle_sync_errors
152167def history (api_key : Optional [str ], env_file : Optional [str ], base_dir : str , base_url : Optional [str ], oneline : bool ):
153168 """Show sync operation history."""
@@ -156,32 +171,32 @@ def history(api_key: Optional[str], env_file: Optional[str], base_dir: str, base
156171
157172 history = sync_client .metadata .get_history ()
158173 if not history :
159- click .echo ("No sync operations found in history." )
174+ click .echo (click . style ( "No sync operations found in history." , fg = WARNING_COLOR ) )
160175 return
161176
162177 if not oneline :
163- click .echo ("Sync Operation History:" )
164- click .echo ("======================" )
178+ click .echo (click . style ( "Sync Operation History:" , fg = INFO_COLOR ) )
179+ click .echo (click . style ( "======================" , fg = INFO_COLOR ) )
165180
166181 for op in history :
167182 if oneline :
168183 # Format: timestamp | operation_type | path | environment | duration_ms | status
169- status = "✓" if not op ['failed_files' ] else "✗"
184+ status = click . style ( "✓" , fg = SUCCESS_COLOR ) if not op ['failed_files' ] else click . style ( "✗" , fg = ERROR_COLOR )
170185 click .echo (f"{ format_timestamp (op ['timestamp' ])} | { op ['operation_type' ]} | { op ['path' ] or '(root)' } | { op ['environment' ] or '-' } | { op ['duration_ms' ]} ms | { status } " )
171186 else :
172- click .echo (f"\n Operation: { op ['operation_type' ]} " )
187+ click .echo (click . style ( f"\n Operation: { op ['operation_type' ]} " , fg = INFO_COLOR ) )
173188 click .echo (f"Timestamp: { format_timestamp (op ['timestamp' ])} " )
174189 click .echo (f"Path: { op ['path' ] or '(root)' } " )
175190 if op ['environment' ]:
176191 click .echo (f"Environment: { op ['environment' ]} " )
177192 click .echo (f"Duration: { op ['duration_ms' ]} ms" )
178193 if op ['successful_files' ]:
179- click .echo (f"Successfully synced { len (op ['successful_files' ])} file{ '' if len (op ['successful_files' ]) == 1 else 's' } " )
194+ click .echo (click . style ( f"Successfully synced { len (op ['successful_files' ])} file{ '' if len (op ['successful_files' ]) == 1 else 's' } " , fg = SUCCESS_COLOR ) )
180195 if op ['failed_files' ]:
181- click .echo (f"Failed to sync { len (op ['failed_files' ])} file{ '' if len (op ['failed_files' ]) == 1 else 's' } " )
196+ click .echo (click . style ( f"Failed to sync { len (op ['failed_files' ])} file{ '' if len (op ['failed_files' ]) == 1 else 's' } " , fg = ERROR_COLOR ) )
182197 if op ['error' ]:
183- click .echo (f"Error: { op ['error' ]} " )
184- click .echo ("----------------------" )
198+ click .echo (click . style ( f"Error: { op ['error' ]} " , fg = ERROR_COLOR ) )
199+ click .echo (click . style ( "----------------------" , fg = INFO_COLOR ) )
185200
186201if __name__ == "__main__" :
187202 cli ()
0 commit comments