Skip to content

Commit 70de199

Browse files
authored
Merge pull request #39 from zhujian0805/main
add skill support for Qwen code
2 parents a39dab3 + 441aaa6 commit 70de199

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+7048
-556
lines changed

code_assistant_manager/cli/app.py

Lines changed: 1302 additions & 117 deletions
Large diffs are not rendered by default.

code_assistant_manager/cli/commands.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,7 @@
4040
UninstallContext,
4141
uninstall,
4242
)
43-
from code_assistant_manager.config import ConfigManager
44-
from code_assistant_manager.menu.base import Colors
45-
from code_assistant_manager.tools import (
46-
display_all_tool_endpoints,
47-
display_tool_endpoints,
48-
get_registered_tools,
49-
)
43+
# Lazy imports moved inside functions to improve startup time
5044

5145
logger = logging.getLogger(__name__)
5246

@@ -56,7 +50,13 @@
5650
# ============================================================================
5751

5852

59-
def _get_config_manager(ctx: Context) -> ConfigManager:
53+
54+
def _get_config_manager(ctx: Context):
55+
# Lazy import
56+
from code_assistant_manager.config import ConfigManager
57+
58+
# Return type hint for clarity
59+
6060
"""Get or create ConfigManager from context."""
6161
try:
6262
config_path = None
@@ -80,6 +80,12 @@ def upgrade(
8080
"""Upgrade CLI tools (alias: u). If not installed, will install.
8181
If installed, will try to upgrade."""
8282
from code_assistant_manager.cli.upgrade import handle_upgrade_command
83+
from code_assistant_manager.config import ConfigManager
84+
from code_assistant_manager.tools import (
85+
display_all_tool_endpoints,
86+
display_tool_endpoints,
87+
get_registered_tools,
88+
)
8389

8490
logger.debug(f"Upgrade command called with target: {target}")
8591
config_path = ctx.obj.get("config_path")
@@ -125,6 +131,10 @@ def doctor(
125131
config: Optional[str] = CONFIG_FILE_OPTION,
126132
):
127133
"""Run diagnostic checks on the code-assistant-manager installation (alias: d)"""
134+
135+
# Lazy imports
136+
from code_assistant_manager.config import ConfigManager
137+
from code_assistant_manager.tools import display_all_tool_endpoints, display_tool_endpoints
128138
# Initialize context object
129139
ctx.ensure_object(dict)
130140
ctx.obj["config_path"] = config
@@ -173,6 +183,10 @@ def doctor(
173183

174184
def launch_alias(ctx: Context, tool_name: str = TOOL_NAME_OPTION):
175185
"""Alias for 'launch' command."""
186+
187+
# Lazy imports
188+
from code_assistant_manager.config import ConfigManager
189+
from code_assistant_manager.tools import get_registered_tools
176190
# Initialize context object
177191
ctx.ensure_object(dict)
178192
ctx.obj["config_path"] = None
@@ -420,6 +434,10 @@ def validate_config(
420434
verbose: bool = VALIDATE_VERBOSE_OPTION,
421435
):
422436
"""Validate the configuration file for syntax and semantic errors."""
437+
438+
# Lazy imports
439+
from code_assistant_manager.config import ConfigManager
440+
from code_assistant_manager.menu.base import Colors
423441
try:
424442
cm = ConfigManager(config)
425443
typer.echo(

code_assistant_manager/cli/completion_commands.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -692,13 +692,13 @@ def _generate_zsh_completion() -> str:
692692
_values 'option' '--force[Skip confirmation]' '--help[Show help]'
693693
;;
694694
import)
695-
_values 'option' '--app[Application type]:app:(claude codex gemini copilot codebuddy)' '--level[Level]:level:(user project)' '--project-dir[Project directory]:directory:_files -/' '--description[Prompt description]' '--help[Show help]'
695+
_values 'option' '--app[Application type]:app:(claude codex gemini copilot qwen codebuddy)' '--level[Level]:level:(user project)' '--project-dir[Project directory]:directory:_files -/' '--description[Prompt description]' '--help[Show help]'
696696
;;
697697
install)
698-
_values 'option' '--app[Application type]:app:(claude codex gemini copilot codebuddy)' '--level[Level]:level:(user project)' '--project-dir[Project directory]:directory:_files -/' '--help[Show help]'
698+
_values 'option' '--app[Application type]:app:(claude codex gemini copilot qwen codebuddy)' '--level[Level]:level:(user project)' '--project-dir[Project directory]:directory:_files -/' '--help[Show help]'
699699
;;
700700
uninstall)
701-
_values 'option' '--app[Application type]:app:(claude codex gemini copilot codebuddy)' '--level[Level]:level:(user project)' '--project-dir[Project directory]:directory:_files -/' '--force[Skip confirmation]' '--help[Show help]'
701+
_values 'option' '--app[Application type]:app:(claude codex gemini copilot qwen codebuddy)' '--level[Level]:level:(user project)' '--project-dir[Project directory]:directory:_files -/' '--force[Skip confirmation]' '--help[Show help]'
702702
;;
703703
status)
704704
_values 'option' '--project-dir[Project directory]:directory:_files -/' '--help[Show help]'
@@ -753,7 +753,7 @@ def _generate_zsh_completion() -> str:
753753
_values 'option' '--owner[Repository owner]' '--repo[Repository name]' '--help[Show help]'
754754
;;
755755
install|uninstall|enable|disable|validate)
756-
_values 'option' '--app[Application type]:app:(claude codebuddy codex copilot)' '--help[Show help]'
756+
_values 'option' '--app[Application type]:app:(claude codebuddy codex copilot gemini qwen)' '--help[Show help]'
757757
;;
758758
browse|view|status)
759759
_values 'option' '--help[Show help]'

code_assistant_manager/cli/plugins/plugin_install_commands.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,16 @@ def _resolve_plugin_conflict(plugin_name: str, app_type: str) -> str:
6464
found_in_marketplaces = []
6565
unreachable_marketplaces = []
6666

67-
for marketplace_name, repo in configured_marketplaces.items():
67+
for marketplace_key, repo in configured_marketplaces.items():
6868
# Handle both PluginRepo objects and installed marketplace dictionaries
6969
if hasattr(repo, 'repo_owner'): # PluginRepo object
7070
repo_owner = repo.repo_owner
7171
repo_name = repo.repo_name
7272
repo_branch = repo.repo_branch or "main"
73+
# Use the actual marketplace name from the repo object, not the dict key
74+
# The dict key might be "owner/repo" format from remote configs,
75+
# but repo.name contains the correct marketplace name from marketplace.json
76+
marketplace_name = repo.name
7377
elif isinstance(repo, dict) and 'source' in repo: # Installed marketplace dict
7478
# Skip installed marketplaces - we'll handle them separately
7579
continue
@@ -105,6 +109,28 @@ def _resolve_plugin_conflict(plugin_name: str, app_type: str) -> str:
105109
"available": False
106110
})
107111

112+
# Deduplicate marketplaces based on (name, source) combination
113+
# This handles cases where remote configs have duplicate entries with different keys
114+
# pointing to the same repo
115+
seen = set()
116+
deduplicated_found = []
117+
for entry in found_in_marketplaces:
118+
key = (entry["marketplace"], entry["source"])
119+
if key not in seen:
120+
seen.add(key)
121+
deduplicated_found.append(entry)
122+
found_in_marketplaces = deduplicated_found
123+
124+
# Also deduplicate unreachable marketplaces
125+
seen = set()
126+
deduplicated_unreachable = []
127+
for entry in unreachable_marketplaces:
128+
key = (entry["marketplace"], entry["source"])
129+
if key not in seen:
130+
seen.add(key)
131+
deduplicated_unreachable.append(entry)
132+
unreachable_marketplaces = deduplicated_unreachable
133+
108134
# Handle results
109135
if not found_in_marketplaces:
110136
typer.echo(

code_assistant_manager/cli/plugins/plugin_management_commands.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
from code_assistant_manager.menu.base import Colors
1212
from code_assistant_manager.plugins import (
13-
BUILTIN_PLUGIN_REPOS,
1413
VALID_APP_TYPES,
1514
PluginManager,
1615
PluginRepo,

code_assistant_manager/cli/skills_commands.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
logger = logging.getLogger(__name__)
2020

2121
skill_app = typer.Typer(
22-
help="Manage skills for AI assistants (Claude, Codex, Copilot, Gemini, Droid, CodeBuddy)",
22+
help="Manage skills for AI assistants (Claude, Codex, Copilot, Gemini, Droid, CodeBuddy, Qwen)",
2323
no_args_is_help=True,
2424
)
2525

@@ -35,7 +35,13 @@ def list_skills(
3535
"claude",
3636
"--app",
3737
"-a",
38-
help="App type(s) to check installed status (claude, codex, copilot, gemini, all)",
38+
help="App type(s) to check installed status (claude, codex, copilot, gemini, qwen, all)",
39+
),
40+
query: Optional[str] = typer.Option(
41+
None,
42+
"--query",
43+
"-q",
44+
help="Filter skills by repository name (e.g., 'BrownFineSecurity/iothackbot')",
3945
),
4046
):
4147
"""List all skills."""
@@ -48,21 +54,47 @@ def list_skills(
4854

4955
skills = manager.get_all()
5056

57+
# Filter skills by query if provided
58+
if query and isinstance(query, str):
59+
filtered_skills = {}
60+
query_lower = query.lower()
61+
for skill_key, skill in skills.items():
62+
if skill.repo_owner and skill.repo_name:
63+
repo_full_name = f"{skill.repo_owner}/{skill.repo_name}".lower()
64+
if query_lower in repo_full_name:
65+
filtered_skills[skill_key] = skill
66+
skills = filtered_skills
67+
5168
if not skills:
52-
typer.echo(
53-
f"{Colors.YELLOW}No skills found. Run 'cam skill fetch' to discover skills from repositories.{Colors.RESET}"
54-
)
69+
if query and isinstance(query, str):
70+
typer.echo(
71+
f"{Colors.YELLOW}No skills found matching query '{query}'. Run 'cam skill fetch' to discover skills from repositories.{Colors.RESET}"
72+
)
73+
else:
74+
typer.echo(
75+
f"{Colors.YELLOW}No skills found. Run 'cam skill fetch' to discover skills from repositories.{Colors.RESET}"
76+
)
5577
return
5678

5779
context = ", ".join(target_apps)
58-
typer.echo(f"\n{Colors.BOLD}Skills (for {context}):{Colors.RESET}\n")
80+
if query and isinstance(query, str):
81+
typer.echo(f"\n{Colors.BOLD}Skills matching '{query}' (for {context}):{Colors.RESET}\n")
82+
else:
83+
typer.echo(f"\n{Colors.BOLD}Skills (for {context}):{Colors.RESET}\n")
5984
for skill_key, skill in sorted(skills.items()):
6085
status = (
6186
f"{Colors.GREEN}{Colors.RESET}"
6287
if skill.installed
6388
else f"{Colors.RED}{Colors.RESET}"
6489
)
65-
typer.echo(f"{status} {Colors.BOLD}{skill.name}{Colors.RESET} ({skill_key})")
90+
# Create simplified install key: repo_owner/repo_name:directory
91+
# This matches the simplified keys that are also stored in the skills dict
92+
if skill.repo_owner and skill.repo_name:
93+
install_key = f"{skill.repo_owner}/{skill.repo_name}:{skill.directory}"
94+
else:
95+
install_key = skill_key
96+
97+
typer.echo(f"{status} {Colors.BOLD}{skill.name}{Colors.RESET} ({install_key})")
6698
if skill.description:
6799
typer.echo(f" {Colors.CYAN}Description:{Colors.RESET} {skill.description}")
68100
typer.echo(f" {Colors.CYAN}Directory:{Colors.RESET} {skill.directory}")
@@ -344,7 +376,7 @@ def install_skill(
344376
"claude",
345377
"--app",
346378
"-a",
347-
help="App type(s) to install to (claude, codex, gemini, all)",
379+
help="App type(s) to install to (claude, codex, gemini, qwen, all)",
348380
),
349381
):
350382
"""Install a skill to one or more app skills directories."""
@@ -372,7 +404,7 @@ def uninstall_skill(
372404
"claude",
373405
"--app",
374406
"-a",
375-
help="App type(s) to uninstall from (claude, codex, gemini, all)",
407+
help="App type(s) to uninstall from (claude, codex, gemini, qwen, all)",
376408
),
377409
):
378410
"""Uninstall a skill from one or more app skills directories."""
@@ -504,7 +536,7 @@ def skill_status(
504536
None,
505537
"--app",
506538
"-a",
507-
help="App type(s) to show (claude, codex, gemini, all). Default shows all.",
539+
help="App type(s) to show (claude, codex, gemini, qwen, all). Default shows all.",
508540
),
509541
):
510542
"""Show skill installation status across apps (alias: installed)."""
@@ -517,7 +549,7 @@ def list_installed_skills(
517549
None,
518550
"--app",
519551
"-a",
520-
help="App type(s) to show (claude, codex, gemini, all). Default shows all.",
552+
help="App type(s) to show (claude, codex, gemini, qwen, all). Default shows all.",
521553
),
522554
):
523555
"""Show installed skills for each app."""
@@ -574,7 +606,7 @@ def uninstall_all_skills(
574606
...,
575607
"--app",
576608
"-a",
577-
help="App type(s) to uninstall all skills from (claude, codex, gemini, all)",
609+
help="App type(s) to uninstall all skills from (claude, codex, gemini, qwen, all)",
578610
),
579611
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
580612
):

code_assistant_manager/cli/upgrade.py

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,10 @@ def _format_version(value) -> str:
204204
percent = completed / total if total else 1.0
205205
filled = int(percent * bar_len)
206206
bar = "█" * filled + "-" * (bar_len - filled)
207-
sys.stdout.write(
208-
f"Upgrading: [{bar}] {completed}/{total} tools processed"
207+
# Use \r to ensure we start at the beginning of the line
208+
typer.echo(
209+
f"Upgrading: [{bar}] {completed}/{total} tools processed", nl=False
209210
)
210-
sys.stdout.flush()
211211

212212
# First, count the skipped tools as completed
213213
for _tool_name, action, _ in prepared_tools:
@@ -217,10 +217,11 @@ def _format_version(value) -> str:
217217
percent = completed / total if total else 1.0
218218
filled = int(percent * bar_len)
219219
bar = "█" * filled + "-" * (bar_len - filled)
220-
sys.stdout.write(
221-
f"\rUpgrading: [{bar}] {completed}/{total} tools processed"
220+
# Clear line and update progress
221+
typer.echo(
222+
f"\033[2K\rUpgrading: [{bar}] {completed}/{total} tools processed",
223+
nl=False,
222224
)
223-
sys.stdout.flush()
224225
time.sleep(
225226
0.05
226227
) # Small delay to make incremental progress visible
@@ -269,21 +270,20 @@ def _format_version(value) -> str:
269270
percent = completed / total if total else 1.0
270271
filled = int(percent * bar_len)
271272
bar = "█" * filled + "-" * (bar_len - filled)
272-
sys.stdout.write(
273-
f"\rUpgrading: [{bar}] {completed}/{total} tools processed"
273+
typer.echo(
274+
f"\033[2K\rUpgrading: [{bar}] {completed}/{total} tools processed",
275+
nl=False,
274276
)
275-
sys.stdout.flush()
276277
else:
277278
# Compact per-line update, keep only minimal text
278279
typer.echo(f" [{completed}/{total}] tools processed")
279280

280281
if is_tty:
281-
# Finish the progress bar line
282-
sys.stdout.write("\n")
283-
sys.stdout.flush()
282+
# Finish the progress bar line and ensure clean terminal state
283+
typer.echo("")
284284

285285
# Display results for ALL upgradeable tools, including ones skipped by pre-check
286-
typer.echo(f"\n{Colors.GREEN}Upgrade results:{Colors.RESET}")
286+
typer.echo(f"{Colors.GREEN}Upgrade results:{Colors.RESET}")
287287
for tool_name in upgradeable_tools.keys():
288288
before_v = _format_version(pre_versions.get(tool_name, "unknown"))
289289

@@ -453,32 +453,11 @@ def _perform_upgrade_task(tool_name, tool, install_cmd, quiet: bool = False):
453453
if not quiet:
454454
# typer.echo(f"{Colors.BLUE}Starting upgrade for {tool_name}...{Colors.RESET}")
455455
pass
456-
# If quiet mode requested, ask tool to run quieter by passing a wrapper command
457-
if quiet:
458-
# Try to use script command for quiet execution, but fall back if it doesn't work
459-
# Different Unix systems have different script command syntax
460-
import subprocess
461456

462-
try:
463-
# Test if script command supports -q -c syntax (macOS style)
464-
subprocess.run(
465-
["script", "-q", "-c", "echo test"],
466-
capture_output=True,
467-
text=True,
468-
timeout=1,
469-
)
470-
# If we get here, script -q -c works
471-
wrapper_cmd = f"script -q -c {install_cmd!r} /dev/null"
472-
except (
473-
subprocess.TimeoutExpired,
474-
subprocess.CalledProcessError,
475-
FileNotFoundError,
476-
):
477-
# script command doesn't support -q -c, or doesn't exist, fall back to redirection
478-
wrapper_cmd = f"{install_cmd} >/dev/null 2>&1"
479-
result = tool._perform_upgrade(desc, wrapper_cmd)
480-
else:
481-
result = tool._perform_upgrade(desc, install_cmd)
457+
# Perform the upgrade
458+
# Note: CommandRunner (used by _perform_upgrade) captures output, so it is inherently quiet.
459+
# We do not need to wrap commands in 'script' or redirects which can corrupt terminal state.
460+
result = tool._perform_upgrade(desc, install_cmd)
482461
return result
483462
except Exception as e:
484463
# Do not print exception stack traces to stderr; return structured failure

code_assistant_manager/fetching/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def _fetch_from_single_repo(self, repo: RepoConfig) -> List[T]:
133133

134134
try:
135135
# Clone/download repository
136-
with git_repo.clone() as temp_dir:
136+
with git_repo.clone() as (temp_dir, actual_branch):
137137
# Determine scan directory
138138
scan_dir = temp_dir
139139
if repo.path:

0 commit comments

Comments
 (0)