ostruct-cli 0.7.1__py3-none-any.whl → 0.8.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 (46) hide show
  1. ostruct/cli/__init__.py +21 -3
  2. ostruct/cli/base_errors.py +1 -1
  3. ostruct/cli/cli.py +66 -1983
  4. ostruct/cli/click_options.py +460 -28
  5. ostruct/cli/code_interpreter.py +238 -0
  6. ostruct/cli/commands/__init__.py +32 -0
  7. ostruct/cli/commands/list_models.py +128 -0
  8. ostruct/cli/commands/quick_ref.py +50 -0
  9. ostruct/cli/commands/run.py +137 -0
  10. ostruct/cli/commands/update_registry.py +71 -0
  11. ostruct/cli/config.py +277 -0
  12. ostruct/cli/cost_estimation.py +134 -0
  13. ostruct/cli/errors.py +310 -6
  14. ostruct/cli/exit_codes.py +1 -0
  15. ostruct/cli/explicit_file_processor.py +548 -0
  16. ostruct/cli/field_utils.py +69 -0
  17. ostruct/cli/file_info.py +42 -9
  18. ostruct/cli/file_list.py +301 -102
  19. ostruct/cli/file_search.py +455 -0
  20. ostruct/cli/file_utils.py +47 -13
  21. ostruct/cli/mcp_integration.py +541 -0
  22. ostruct/cli/model_creation.py +150 -1
  23. ostruct/cli/model_validation.py +204 -0
  24. ostruct/cli/progress_reporting.py +398 -0
  25. ostruct/cli/registry_updates.py +14 -9
  26. ostruct/cli/runner.py +1418 -0
  27. ostruct/cli/schema_utils.py +113 -0
  28. ostruct/cli/services.py +626 -0
  29. ostruct/cli/template_debug.py +748 -0
  30. ostruct/cli/template_debug_help.py +162 -0
  31. ostruct/cli/template_env.py +15 -6
  32. ostruct/cli/template_filters.py +55 -3
  33. ostruct/cli/template_optimizer.py +474 -0
  34. ostruct/cli/template_processor.py +1080 -0
  35. ostruct/cli/template_rendering.py +69 -34
  36. ostruct/cli/token_validation.py +286 -0
  37. ostruct/cli/types.py +78 -0
  38. ostruct/cli/unattended_operation.py +269 -0
  39. ostruct/cli/validators.py +386 -3
  40. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
  41. ostruct_cli-0.8.0.dist-info/METADATA +633 -0
  42. ostruct_cli-0.8.0.dist-info/RECORD +69 -0
  43. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
  44. ostruct_cli-0.7.1.dist-info/METADATA +0 -369
  45. ostruct_cli-0.7.1.dist-info/RECORD +0 -45
  46. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,238 @@
1
+ """Code Interpreter integration for ostruct CLI.
2
+
3
+ This module provides support for uploading files to OpenAI's Code Interpreter
4
+ and integrating code execution capabilities with the OpenAI Responses API.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List
11
+
12
+ from openai import AsyncOpenAI
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CodeInterpreterManager:
18
+ """Manager for Code Interpreter file uploads and tool integration."""
19
+
20
+ def __init__(self, client: AsyncOpenAI):
21
+ """Initialize Code Interpreter manager.
22
+
23
+ Args:
24
+ client: AsyncOpenAI client instance
25
+ """
26
+ self.client = client
27
+ self.uploaded_file_ids: List[str] = []
28
+
29
+ async def upload_files_for_code_interpreter(
30
+ self, files: List[str]
31
+ ) -> List[str]:
32
+ """Upload files for Code Interpreter (validated working pattern).
33
+
34
+ This method uploads files to OpenAI's file storage with the correct
35
+ purpose for Code Interpreter usage.
36
+
37
+ Args:
38
+ files: List of file paths to upload
39
+
40
+ Returns:
41
+ List of file IDs from successful uploads
42
+
43
+ Raises:
44
+ FileNotFoundError: If a file doesn't exist
45
+ Exception: If upload fails
46
+ """
47
+ file_ids = []
48
+
49
+ for file_path in files:
50
+ try:
51
+ # Validate file exists
52
+ if not os.path.exists(file_path):
53
+ raise FileNotFoundError(f"File not found: {file_path}")
54
+
55
+ # Get file info
56
+ file_size = os.path.getsize(file_path)
57
+ logger.debug(
58
+ f"Uploading file: {file_path} ({file_size} bytes)"
59
+ )
60
+
61
+ # Upload with correct purpose for Code Interpreter
62
+ with open(file_path, "rb") as f:
63
+ file_obj = await self.client.files.create(
64
+ file=f,
65
+ purpose="assistants", # Validated correct purpose
66
+ )
67
+ file_ids.append(file_obj.id)
68
+ logger.debug(
69
+ f"Successfully uploaded {file_path} with ID: {file_obj.id}"
70
+ )
71
+
72
+ except Exception as e:
73
+ logger.error(f"Failed to upload file {file_path}: {e}")
74
+ # Clean up any successfully uploaded files on error
75
+ await self._cleanup_uploaded_files(file_ids)
76
+ raise
77
+
78
+ # Store for potential cleanup
79
+ self.uploaded_file_ids.extend(file_ids)
80
+ return file_ids
81
+
82
+ def build_tool_config(self, file_ids: List[str]) -> dict:
83
+ """Build Code Interpreter tool configuration.
84
+
85
+ Creates a tool configuration compatible with the OpenAI Responses API
86
+ for Code Interpreter functionality.
87
+
88
+ Args:
89
+ file_ids: List of uploaded file IDs
90
+
91
+ Returns:
92
+ Tool configuration dict for Responses API
93
+ """
94
+ return {
95
+ "type": "code_interpreter",
96
+ "container": {"type": "auto", "file_ids": file_ids},
97
+ }
98
+
99
+ async def download_generated_files(
100
+ self, response_file_ids: List[str], output_dir: str = "."
101
+ ) -> List[str]:
102
+ """Download files generated by Code Interpreter.
103
+
104
+ Args:
105
+ response_file_ids: List of file IDs from Code Interpreter response
106
+ output_dir: Directory to save downloaded files
107
+
108
+ Returns:
109
+ List of local file paths where files were saved
110
+
111
+ Raises:
112
+ Exception: If download fails
113
+ """
114
+ downloaded_paths = []
115
+ output_path = Path(output_dir)
116
+ output_path.mkdir(exist_ok=True)
117
+
118
+ for file_id in response_file_ids:
119
+ try:
120
+ # Get file info
121
+ file_info = await self.client.files.retrieve(file_id)
122
+ filename = (
123
+ file_info.filename or f"generated_file_{file_id[:8]}.dat"
124
+ )
125
+
126
+ # Download file content
127
+ file_content = await self.client.files.content(file_id)
128
+
129
+ # Save to local file
130
+ local_path = output_path / filename
131
+ with open(local_path, "wb") as f:
132
+ f.write(file_content.read())
133
+
134
+ downloaded_paths.append(str(local_path))
135
+ logger.debug(f"Downloaded generated file: {local_path}")
136
+
137
+ except Exception as e:
138
+ logger.error(f"Failed to download file {file_id}: {e}")
139
+ raise
140
+
141
+ return downloaded_paths
142
+
143
+ async def cleanup_uploaded_files(self) -> None:
144
+ """Clean up uploaded files from OpenAI storage.
145
+
146
+ This method removes files that were uploaded during the session
147
+ to avoid accumulating files in the user's OpenAI storage.
148
+ """
149
+ await self._cleanup_uploaded_files(self.uploaded_file_ids)
150
+ self.uploaded_file_ids.clear()
151
+
152
+ async def _cleanup_uploaded_files(self, file_ids: List[str]) -> None:
153
+ """Internal method to clean up specific file IDs.
154
+
155
+ Args:
156
+ file_ids: List of file IDs to delete
157
+ """
158
+ for file_id in file_ids:
159
+ try:
160
+ await self.client.files.delete(file_id)
161
+ logger.debug(f"Cleaned up uploaded file: {file_id}")
162
+ except Exception as e:
163
+ logger.warning(f"Failed to clean up file {file_id}: {e}")
164
+
165
+ def validate_files_for_upload(self, files: List[str]) -> List[str]:
166
+ """Validate files are suitable for Code Interpreter upload.
167
+
168
+ Args:
169
+ files: List of file paths to validate
170
+
171
+ Returns:
172
+ List of validation error messages, empty if all files are valid
173
+ """
174
+ errors = []
175
+
176
+ # Common file types supported by Code Interpreter
177
+ supported_extensions = {
178
+ ".py",
179
+ ".txt",
180
+ ".csv",
181
+ ".json",
182
+ ".xlsx",
183
+ ".xls",
184
+ ".pdf",
185
+ ".docx",
186
+ ".md",
187
+ ".xml",
188
+ ".html",
189
+ ".js",
190
+ ".sql",
191
+ ".log",
192
+ ".yaml",
193
+ ".yml",
194
+ ".toml",
195
+ ".ini",
196
+ }
197
+
198
+ # Size limits (approximate - OpenAI has file size limits)
199
+ max_file_size = 100 * 1024 * 1024 # 100MB
200
+
201
+ for file_path in files:
202
+ try:
203
+ if not os.path.exists(file_path):
204
+ errors.append(f"File not found: {file_path}")
205
+ continue
206
+
207
+ # Check file size
208
+ file_size = os.path.getsize(file_path)
209
+ if file_size > max_file_size:
210
+ errors.append(
211
+ f"File too large: {file_path} ({file_size / 1024 / 1024:.1f}MB > 100MB)"
212
+ )
213
+
214
+ # Check file extension
215
+ file_ext = Path(file_path).suffix.lower()
216
+ if file_ext not in supported_extensions:
217
+ logger.warning(
218
+ f"File extension {file_ext} may not be supported by Code Interpreter: {file_path}"
219
+ )
220
+
221
+ except Exception as e:
222
+ errors.append(f"Error validating file {file_path}: {e}")
223
+
224
+ return errors
225
+
226
+ def get_container_limits_info(self) -> Dict[str, Any]:
227
+ """Get information about Code Interpreter container limits.
228
+
229
+ Returns:
230
+ Dictionary with container limit information
231
+ """
232
+ return {
233
+ "max_runtime_minutes": 20,
234
+ "idle_timeout_minutes": 2,
235
+ "max_file_size_mb": 100,
236
+ "supported_languages": ["python"],
237
+ "note": "Container expires after 20 minutes of runtime or 2 minutes of inactivity",
238
+ }
@@ -0,0 +1,32 @@
1
+ """Command modules for ostruct CLI."""
2
+
3
+ import click
4
+
5
+ from .list_models import list_models
6
+ from .quick_ref import quick_reference
7
+ from .run import run
8
+ from .update_registry import update_registry
9
+
10
+
11
+ def create_command_group() -> click.Group:
12
+ """Create and configure the CLI command group with all commands."""
13
+ # Create the main CLI group
14
+ group = click.Group()
15
+
16
+ # Add all commands to the group
17
+ group.add_command(run)
18
+ group.add_command(quick_reference)
19
+ group.add_command(update_registry)
20
+ group.add_command(list_models)
21
+
22
+ return group
23
+
24
+
25
+ # Export commands for easy importing
26
+ __all__ = [
27
+ "run",
28
+ "quick_reference",
29
+ "update_registry",
30
+ "list_models",
31
+ "create_command_group",
32
+ ]
@@ -0,0 +1,128 @@
1
+ """List models command for ostruct CLI."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ import click
7
+ from openai_model_registry import ModelRegistry
8
+
9
+ from ..exit_codes import ExitCode
10
+
11
+
12
+ @click.command("list-models")
13
+ @click.option(
14
+ "--format",
15
+ type=click.Choice(["table", "json", "simple"]),
16
+ default="table",
17
+ help="Output format for model list",
18
+ )
19
+ @click.option(
20
+ "--show-deprecated",
21
+ is_flag=True,
22
+ help="Include deprecated models in output",
23
+ )
24
+ def list_models(format: str = "table", show_deprecated: bool = False) -> None:
25
+ """List available models from the registry."""
26
+ try:
27
+ registry = ModelRegistry.get_instance()
28
+ models = registry.models
29
+
30
+ # Filter models if not showing deprecated
31
+ if not show_deprecated:
32
+ # Filter out deprecated models (this depends on registry implementation)
33
+ filtered_models = []
34
+ for model_id in models:
35
+ try:
36
+ capabilities = registry.get_capabilities(model_id)
37
+ # If we can get capabilities, it's likely not deprecated
38
+ filtered_models.append(
39
+ {
40
+ "id": model_id,
41
+ "context_window": capabilities.context_window,
42
+ "max_output": capabilities.max_output_tokens,
43
+ }
44
+ )
45
+ except Exception:
46
+ # Skip models that can't be accessed (likely deprecated)
47
+ continue
48
+ models_data = filtered_models
49
+ else:
50
+ # Include all models
51
+ models_data = []
52
+ for model_id in models:
53
+ try:
54
+ capabilities = registry.get_capabilities(model_id)
55
+ models_data.append(
56
+ {
57
+ "id": model_id,
58
+ "context_window": capabilities.context_window,
59
+ "max_output": capabilities.max_output_tokens,
60
+ "status": "active",
61
+ }
62
+ )
63
+ except Exception:
64
+ models_data.append(
65
+ {
66
+ "id": model_id,
67
+ "context_window": "N/A",
68
+ "max_output": "N/A",
69
+ "status": "deprecated",
70
+ }
71
+ )
72
+
73
+ if format == "table":
74
+ # Calculate dynamic column widths based on actual data
75
+ max_id_width = (
76
+ max(len(str(model["id"])) for model in models_data)
77
+ if models_data
78
+ else 8
79
+ )
80
+ max_id_width = max(max_id_width, len("Model ID"))
81
+
82
+ max_context_width = (
83
+ 15 # Keep reasonable default for context window
84
+ )
85
+ max_output_width = 12 # Keep reasonable default for max output
86
+ status_width = 10 # Keep fixed for status
87
+
88
+ # Ensure minimum widths for readability
89
+ id_width = max(max_id_width, 8)
90
+
91
+ total_width = (
92
+ id_width
93
+ + max_context_width
94
+ + max_output_width
95
+ + status_width
96
+ + 6
97
+ ) # 6 for spacing
98
+
99
+ click.echo("Available Models:")
100
+ click.echo("-" * total_width)
101
+ click.echo(
102
+ f"{'Model ID':<{id_width}} {'Context Window':<{max_context_width}} {'Max Output':<{max_output_width}} {'Status':<{status_width}}"
103
+ )
104
+ click.echo("-" * total_width)
105
+ for model in models_data:
106
+ status = model.get("status", "active")
107
+ context = (
108
+ f"{model['context_window']:,}"
109
+ if isinstance(model["context_window"], int)
110
+ else model["context_window"]
111
+ )
112
+ output = (
113
+ f"{model['max_output']:,}"
114
+ if isinstance(model["max_output"], int)
115
+ else model["max_output"]
116
+ )
117
+ click.echo(
118
+ f"{model['id']:<{id_width}} {context:<{max_context_width}} {output:<{max_output_width}} {status:<{status_width}}"
119
+ )
120
+ elif format == "json":
121
+ click.echo(json.dumps(models_data, indent=2))
122
+ else: # simple
123
+ for model in models_data:
124
+ click.echo(model["id"])
125
+
126
+ except Exception as e:
127
+ click.echo(f"❌ Error listing models: {str(e)}")
128
+ sys.exit(ExitCode.API_ERROR.value)
@@ -0,0 +1,50 @@
1
+ """Quick reference command for ostruct CLI."""
2
+
3
+ import click
4
+
5
+
6
+ @click.command("quick-ref")
7
+ def quick_reference() -> None:
8
+ """Show quick reference for file routing and common usage patterns."""
9
+ quick_ref = """
10
+ 🚀 OSTRUCT QUICK REFERENCE
11
+
12
+ 📁 FILE ROUTING:
13
+ -ft FILE 📄 Template access only (config files, small data)
14
+ -fc FILE 💻 Code Interpreter upload (data files, scripts)
15
+ -fs FILE 🔍 File Search vector store (documents, manuals)
16
+
17
+ -dt DIR 📁 Template directory (config dirs, reference data)
18
+ -dc DIR 📂 Code execution directory (datasets, code repos)
19
+ -ds DIR 📁 Search directory (documentation, knowledge)
20
+
21
+ 🔄 ADVANCED ROUTING:
22
+ --file-for code-interpreter data.csv Single tool, single file
23
+ --file-for file-search docs.pdf Single tool, single file
24
+ --file-for code-interpreter shared.json --file-for file-search shared.json Multi-tool routing
25
+
26
+ 🏷️ VARIABLES:
27
+ -V name=value Simple string variables
28
+ -J config='{"key":"value"}' JSON structured data
29
+
30
+ 🔌 TOOLS:
31
+ --mcp-server label@https://server.com/sse MCP server integration
32
+ --timeout 7200 2-hour timeout for long operations
33
+
34
+ ⚡ COMMON PATTERNS:
35
+ # Basic template rendering
36
+ ostruct run template.j2 schema.json -V env=prod
37
+
38
+ # Data analysis with Code Interpreter
39
+ ostruct run analysis.j2 schema.json -fc data.csv -V task=analyze
40
+
41
+ # Document search + processing
42
+ ostruct run search.j2 schema.json -fs docs/ -ft config.yaml
43
+
44
+ # Multi-tool workflow
45
+ ostruct run workflow.j2 schema.json -fc raw_data.csv -fs knowledge/ -ft config.json
46
+
47
+ 📖 Full help: ostruct run --help
48
+ 📖 Documentation: https://ostruct.readthedocs.io
49
+ """
50
+ click.echo(quick_ref)
@@ -0,0 +1,137 @@
1
+ """Run command for ostruct CLI."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import sys
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+ from ..click_options import all_options
12
+ from ..config import OstructConfig
13
+ from ..errors import (
14
+ CLIError,
15
+ InvalidJSONError,
16
+ SchemaFileError,
17
+ SchemaValidationError,
18
+ handle_error,
19
+ )
20
+ from ..exit_codes import ExitCode
21
+ from ..runner import run_cli_async
22
+ from ..types import CLIParams
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @click.command()
28
+ @click.argument("task_template", type=click.Path(exists=True))
29
+ @click.argument("schema_file", type=click.Path(exists=True))
30
+ @all_options
31
+ @click.pass_context
32
+ def run(
33
+ ctx: click.Context,
34
+ task_template: str,
35
+ schema_file: str,
36
+ **kwargs: Any,
37
+ ) -> None:
38
+ """Run structured output generation with multi-tool integration.
39
+
40
+ \b
41
+ 📁 FILE ROUTING OPTIONS:
42
+
43
+ Template Access Only:
44
+ -ft, --file-for-template FILE Files available in template only
45
+ -dt, --dir-for-template DIR Directories for template access
46
+
47
+ Code Interpreter (execution & analysis):
48
+ -fc, --file-for-code-interpreter FILE Upload files for code execution
49
+ -dc, --dir-for-code-interpreter DIR Upload directories for analysis
50
+
51
+ File Search (document retrieval):
52
+ -fs, --file-for-file-search FILE Upload files for vector search
53
+ -ds, --dir-for-search DIR Upload directories for search
54
+
55
+ Advanced Routing:
56
+ --file-for TOOL PATH Route files to specific tools
57
+ Example: --file-for code-interpreter data.json
58
+
59
+ \b
60
+ 🔧 TOOL INTEGRATION:
61
+
62
+ MCP Servers:
63
+ --mcp-server [LABEL@]URL Connect to MCP server
64
+ Example: --mcp-server deepwiki@https://mcp.deepwiki.com/sse
65
+
66
+ \b
67
+ ⚡ EXAMPLES:
68
+
69
+ Basic usage:
70
+ ostruct run template.j2 schema.json -V name=value
71
+
72
+ Multi-tool explicit routing:
73
+ ostruct run analysis.j2 schema.json -fc data.csv -fs docs.pdf -ft config.yaml
74
+
75
+ Legacy compatibility (still works):
76
+ ostruct run template.j2 schema.json -f config main.py -d src ./src
77
+
78
+ \b
79
+ Arguments:
80
+ TASK_TEMPLATE Path to Jinja2 template file
81
+ SCHEMA_FILE Path to JSON schema file defining output structure
82
+ """
83
+ try:
84
+ # Convert Click parameters to typed dict
85
+ params: CLIParams = {
86
+ "task_file": task_template,
87
+ "task": None,
88
+ "schema_file": schema_file,
89
+ }
90
+ # Add all kwargs to params (type ignore for dynamic key assignment)
91
+ for k, v in kwargs.items():
92
+ params[k] = v # type: ignore[literal-required]
93
+
94
+ # Apply configuration defaults if values not explicitly provided
95
+ # Check for command-level config option first, then group-level
96
+ command_config = kwargs.get("config")
97
+ if command_config:
98
+ config = OstructConfig.load(command_config)
99
+ else:
100
+ config = ctx.obj.get("config") if ctx.obj else OstructConfig()
101
+
102
+ if params.get("model") is None:
103
+ params["model"] = config.get_model_default()
104
+
105
+ # Run the async function synchronously
106
+ loop = asyncio.new_event_loop()
107
+ asyncio.set_event_loop(loop)
108
+ try:
109
+ exit_code = loop.run_until_complete(run_cli_async(params))
110
+ sys.exit(int(exit_code))
111
+ except SchemaValidationError as e:
112
+ # Log the error with full context
113
+ logger.error("Schema validation error: %s", str(e))
114
+ if e.context:
115
+ logger.debug(
116
+ "Error context: %s", json.dumps(e.context, indent=2)
117
+ )
118
+ # Re-raise to preserve error chain and exit code
119
+ raise
120
+ except (CLIError, InvalidJSONError, SchemaFileError) as e:
121
+ handle_error(e)
122
+ sys.exit(
123
+ e.exit_code
124
+ if hasattr(e, "exit_code")
125
+ else ExitCode.INTERNAL_ERROR
126
+ )
127
+ except click.UsageError as e:
128
+ handle_error(e)
129
+ sys.exit(ExitCode.USAGE_ERROR)
130
+ except Exception as e:
131
+ handle_error(e)
132
+ sys.exit(ExitCode.INTERNAL_ERROR)
133
+ finally:
134
+ loop.close()
135
+ except KeyboardInterrupt:
136
+ logger.info("Operation cancelled by user")
137
+ raise
@@ -0,0 +1,71 @@
1
+ """Update registry command for ostruct CLI."""
2
+
3
+ import sys
4
+ from typing import Optional
5
+
6
+ import click
7
+ from openai_model_registry import ModelRegistry
8
+
9
+ from ..exit_codes import ExitCode
10
+
11
+
12
+ @click.command("update-registry")
13
+ @click.option(
14
+ "--url",
15
+ help="URL to fetch the registry from. Defaults to official repository.",
16
+ default=None,
17
+ )
18
+ @click.option(
19
+ "--force",
20
+ is_flag=True,
21
+ help="Force update even if the registry is already up to date.",
22
+ default=False,
23
+ )
24
+ def update_registry(url: Optional[str] = None, force: bool = False) -> None:
25
+ """Update the model registry with the latest model definitions.
26
+
27
+ This command fetches the latest model registry from the official repository
28
+ or a custom URL if provided, and updates the local registry file.
29
+
30
+ Example:
31
+ ostruct update-registry
32
+ ostruct update-registry --url https://example.com/models.yml
33
+ """
34
+ try:
35
+ registry = ModelRegistry.get_instance()
36
+
37
+ # Show current registry config path
38
+ config_path = registry.config.registry_path
39
+ click.echo(f"Current registry file: {config_path}")
40
+
41
+ if force:
42
+ click.echo("🔄 Forcing registry update...")
43
+ refresh_result = registry.refresh_from_remote(url)
44
+ if refresh_result.success:
45
+ click.echo("✅ Registry successfully updated!")
46
+ else:
47
+ click.echo(
48
+ f"❌ Failed to update registry: {refresh_result.message}"
49
+ )
50
+ return
51
+
52
+ click.echo("🔍 Checking for registry updates...")
53
+ update_result = registry.check_for_updates()
54
+
55
+ if update_result.status.value == "update_available":
56
+ click.echo(f"📦 Update available: {update_result.message}")
57
+ click.echo("🔄 Updating registry...")
58
+ refresh_result = registry.refresh_from_remote(url)
59
+ if refresh_result.success:
60
+ click.echo("✅ Registry successfully updated!")
61
+ else:
62
+ click.echo(
63
+ f"❌ Failed to update registry: {refresh_result.message}"
64
+ )
65
+ elif update_result.status.value == "already_current":
66
+ click.echo("✅ Registry is already up to date")
67
+ else:
68
+ click.echo(f"⚠️ Registry check failed: {update_result.message}")
69
+ except Exception as e:
70
+ click.echo(f"❌ Error updating registry: {str(e)}")
71
+ sys.exit(ExitCode.API_ERROR.value)