Skip to content

Commit 290b181

Browse files
authored
Merge pull request #22 from zhujian0805/main
increase tests coverage adn Add EnvLoader class and get_config_path function
2 parents d4d5360 + 6b1f009 commit 290b181

30 files changed

Lines changed: 6928 additions & 73 deletions

code_assistant_manager/cli/plugins/plugin_install_commands.py

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -60,44 +60,50 @@ def _resolve_plugin_conflict(plugin_name: str, app_type: str) -> str:
6060
k: v for k, v in all_repos.items() if v.type == "marketplace"
6161
}
6262

63+
# Check configured marketplaces for the plugin
64+
found_in_marketplaces = []
65+
unreachable_marketplaces = []
66+
6367
for marketplace_name, repo in configured_marketplaces.items():
64-
if not repo.repo_owner or not repo.repo_name:
68+
# Handle both PluginRepo objects and installed marketplace dictionaries
69+
if hasattr(repo, 'repo_owner'): # PluginRepo object
70+
repo_owner = repo.repo_owner
71+
repo_name = repo.repo_name
72+
repo_branch = repo.repo_branch or "main"
73+
elif isinstance(repo, dict) and 'source' in repo: # Installed marketplace dict
74+
# Skip installed marketplaces - we'll handle them separately
75+
continue
76+
else:
77+
continue
78+
79+
if not repo_owner or not repo_name:
6580
continue
6681

6782
try:
6883
from code_assistant_manager.plugins.fetch import fetch_repo_info
69-
info = fetch_repo_info(repo.repo_owner, repo.repo_name, repo.repo_branch or "main")
84+
info = fetch_repo_info(repo_owner, repo_name, repo_branch)
7085
if info and info.plugins:
7186
for plugin in info.plugins:
7287
if plugin.get("name", "").lower() == plugin_name.lower():
7388
found_in_marketplaces.append({
7489
"marketplace": marketplace_name,
7590
"plugin": plugin,
76-
"source": f"github.com/{repo.repo_owner}/{repo.repo_name}"
91+
"source": f"github.com/{repo_owner}/{repo_name}",
92+
"available": True
7793
})
7894
break
79-
except Exception:
80-
# Skip marketplaces that can't be fetched
81-
continue
82-
83-
# Also check installed marketplaces in the app
84-
try:
85-
installed_marketplaces = handler.get_known_marketplaces()
86-
for marketplace_name, marketplace_info in installed_marketplaces.items():
87-
# If we already found it in configured marketplaces, skip
88-
if any(f["marketplace"] == marketplace_name for f in found_in_marketplaces):
89-
continue
90-
91-
# For installed marketplaces, we can't easily check their contents
92-
# without fetching, but we can note they're available
93-
source_info = marketplace_info.get("source", {}).get("url", "")
94-
found_in_marketplaces.append({
95+
except Exception as e:
96+
# Log the error but don't skip the marketplace entirely
97+
import logging
98+
logger = logging.getLogger(__name__)
99+
logger.debug(f"Failed to fetch marketplace '{marketplace_name}': {e}")
100+
# Mark marketplace as unreachable but still show it
101+
unreachable_marketplaces.append({
95102
"marketplace": marketplace_name,
96-
"plugin": {"name": plugin_name}, # Placeholder
97-
"source": source_info or "installed marketplace"
103+
"source": f"github.com/{repo_owner}/{repo_name}",
104+
"error": str(e),
105+
"available": False
98106
})
99-
except Exception:
100-
pass
101107

102108
# Handle results
103109
if not found_in_marketplaces:
@@ -107,6 +113,11 @@ def _resolve_plugin_conflict(plugin_name: str, app_type: str) -> str:
107113
typer.echo(f"\n{Colors.CYAN}Available marketplaces:{Colors.RESET}")
108114
for name in sorted(configured_marketplaces.keys()):
109115
typer.echo(f" • {name}")
116+
# Show unreachable marketplaces if any
117+
if unreachable_marketplaces:
118+
typer.echo(f"\n{Colors.YELLOW}Unreachable marketplaces (temporarily unavailable):{Colors.RESET}")
119+
for unreachable in unreachable_marketplaces:
120+
typer.echo(f" • {unreachable['marketplace']} ({unreachable['source']})")
110121
typer.echo(f"\n{Colors.CYAN}Browse plugins:{Colors.RESET} cam plugin browse")
111122
raise typer.Exit(1)
112123

@@ -125,47 +136,59 @@ def _resolve_plugin_conflict(plugin_name: str, app_type: str) -> str:
125136
)
126137
typer.echo()
127138

128-
for i, found in enumerate(found_in_marketplaces, 1):
139+
# Combine available and unreachable marketplaces for display
140+
all_marketplaces = found_in_marketplaces + unreachable_marketplaces
141+
142+
for i, found in enumerate(all_marketplaces, 1):
129143
marketplace = found["marketplace"]
130144
source = found["source"]
131-
plugin_info = found["plugin"]
145+
plugin_info = found.get("plugin", {})
146+
available = found.get("available", True)
132147
version = plugin_info.get("version", "")
133148
description = plugin_info.get("description", "")
134149

135-
typer.echo(f" {i}. {Colors.BOLD}{marketplace}{Colors.RESET}")
150+
status_indicator = "" if available else f" {Colors.YELLOW}(unreachable){Colors.RESET}"
151+
typer.echo(f" {i}. {Colors.BOLD}{marketplace}{Colors.RESET}{status_indicator}")
136152
if version:
137153
typer.echo(f" Version: {version}")
138154
if description:
139155
typer.echo(f" Description: {description[:60]}{'...' if len(description) > 60 else ''}")
140156
typer.echo(f" Source: {source}")
141157
typer.echo()
142158

143-
# Prompt user to choose
159+
# Prompt user to choose (only allow selecting available marketplaces)
160+
available_marketplaces = [f for f in all_marketplaces if f.get("available", True)]
144161
while True:
145162
try:
146163
choice = typer.prompt(
147-
f"Choose marketplace (1-{len(found_in_marketplaces)}) or 'cancel'",
164+
f"Choose marketplace (1-{len(all_marketplaces)}) or 'cancel'",
148165
type=str
149166
).strip().lower()
150167

151168
if choice == "cancel":
152169
raise typer.Exit(0)
153170

154171
choice_idx = int(choice) - 1
155-
if 0 <= choice_idx < len(found_in_marketplaces):
156-
selected = found_in_marketplaces[choice_idx]
172+
if 0 <= choice_idx < len(all_marketplaces):
173+
selected = all_marketplaces[choice_idx]
174+
if not selected.get("available", True):
175+
typer.echo(
176+
f"{Colors.RED}Cannot select unreachable marketplace '{selected['marketplace']}'. Please choose an available marketplace.{Colors.RESET}"
177+
)
178+
continue
179+
157180
marketplace = selected["marketplace"]
158181
typer.echo(
159182
f"{Colors.GREEN}Selected: {marketplace}{Colors.RESET}"
160183
)
161184
return marketplace
162185
else:
163186
typer.echo(
164-
f"{Colors.RED}Invalid choice. Please enter 1-{len(found_in_marketplaces)} or 'cancel'{Colors.RESET}"
187+
f"{Colors.RED}Invalid choice. Please enter 1-{len(all_marketplaces)} or 'cancel'{Colors.RESET}"
165188
)
166189
except ValueError:
167190
typer.echo(
168-
f"{Colors.RED}Invalid input. Please enter a number 1-{len(found_in_marketplaces)} or 'cancel'{Colors.RESET}"
191+
f"{Colors.RED}Invalid input. Please enter a number 1-{len(all_marketplaces)} or 'cancel'{Colors.RESET}"
169192
)
170193
except (EOFError, KeyboardInterrupt):
171194
typer.echo(f"\n{Colors.YELLOW}Cancelled.{Colors.RESET}")

code_assistant_manager/config.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -685,14 +685,6 @@ def _validate_simple_command(value: str) -> bool:
685685

686686

687687
def validate_command(value: str) -> bool:
688-
"""Validate a command string with balanced security and functionality.
689-
690-
Args:
691-
value: Command string to validate
692-
693-
Returns:
694-
True if valid, False otherwise
695-
"""
696688
if not value:
697689
return False
698690

@@ -732,3 +724,23 @@ def validate_command(value: str) -> bool:
732724

733725
# Validate simple commands
734726
return _validate_simple_command(value)
727+
728+
729+
def get_config_path() -> Optional[Path]:
730+
"""Get the path to the main configuration file.
731+
732+
Returns:
733+
Path to the configuration file if found, None otherwise
734+
"""
735+
# Try standard locations in order of preference
736+
locations = [
737+
Path.home() / ".config" / "code-assistant-manager" / "config.json",
738+
Path.home() / ".code-assistant-manager" / "config.json",
739+
Path.cwd() / ".code-assistant-manager" / "config.json",
740+
]
741+
742+
for config_path in locations:
743+
if config_path.exists() and config_path.is_file():
744+
return config_path
745+
746+
return None

code_assistant_manager/env_loader.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,48 @@
1212
_ENV_LOADED = False
1313

1414

15+
class EnvLoader:
16+
"""Environment variable loader class with prefix filtering support."""
17+
18+
def __init__(self, prefix: Optional[str] = None):
19+
"""Initialize EnvLoader with optional prefix filtering.
20+
21+
Args:
22+
prefix: If provided, only environment variables starting with this prefix
23+
will be accessible (prefix will be stripped from keys).
24+
"""
25+
self.prefix = prefix
26+
27+
def get(self, key: str) -> Optional[str]:
28+
"""Get environment variable value.
29+
30+
Args:
31+
key: Environment variable name
32+
33+
Returns:
34+
Value of the environment variable, or None if not found
35+
"""
36+
import os
37+
38+
# Apply prefix filtering
39+
if self.prefix:
40+
full_key = f"{self.prefix}{key}"
41+
else:
42+
full_key = key
43+
44+
# Try exact case first
45+
value = os.environ.get(full_key)
46+
if value is not None:
47+
return value
48+
49+
# Try case-insensitive lookup
50+
for env_key, env_value in os.environ.items():
51+
if env_key.lower() == full_key.lower():
52+
return env_value
53+
54+
return None
55+
56+
1557
def find_env_file(
1658
custom_path: Optional[str] = None, strict: bool = False
1759
) -> Optional[Path]:

code_assistant_manager/plugin_repos.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"repoName": "awesome-claude-code-plugins",
3131
"repoBranch": "main"
3232
},
33-
"claude-code-marketplace": {
34-
"name": "claude-code-marketplace",
33+
"cc-marketplace": {
34+
"name": "cc-marketplace",
3535
"description": "A marketplace for Claude Code plugins",
3636
"enabled": true,
3737
"type": "marketplace",

0 commit comments

Comments
 (0)