Skip to content

Commit 32911b1

Browse files
author
jzhu
committed
add
1 parent 6613a8b commit 32911b1

7 files changed

Lines changed: 157 additions & 43 deletions

File tree

code_assistant_manager/cli/prompts_commands.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,17 +254,56 @@ def deactivate_prompt(prompt_id: str = typer.Argument(..., help="Prompt identifi
254254

255255

256256
@prompt_app.command("sync")
257-
def sync_prompts():
258-
"""Sync all active prompts to their respective app files."""
257+
def sync_prompts(
258+
prompt_id: Optional[str] = typer.Argument(
259+
None, help="Prompt ID to sync. If not specified, syncs all active prompts."
260+
),
261+
app_type: Optional[str] = typer.Option(
262+
None,
263+
"--app",
264+
"-a",
265+
help="App type to sync to (claude, codex, gemini). Required if prompt_id is specified.",
266+
),
267+
):
268+
"""Sync prompts to app files. Can sync a specific prompt or all active prompts."""
259269
manager = _get_prompt_manager()
260270

261-
results = manager.sync_all()
271+
if prompt_id:
272+
# Sync specific prompt to specific app
273+
if not app_type:
274+
typer.echo(
275+
f"{Colors.RED}✗ --app is required when specifying a prompt ID{Colors.RESET}"
276+
)
277+
raise typer.Exit(1)
262278

263-
for app_type, success in results.items():
264-
if success:
265-
typer.echo(f"{Colors.GREEN}{app_type}: synced{Colors.RESET}")
266-
else:
267-
typer.echo(f"{Colors.RED}{app_type}: failed{Colors.RESET}")
279+
if app_type not in VALID_APP_TYPES:
280+
typer.echo(
281+
f"{Colors.RED}✗ Invalid app type: {app_type}. Valid: {', '.join(VALID_APP_TYPES)}{Colors.RESET}"
282+
)
283+
raise typer.Exit(1)
284+
285+
prompt = manager.get(prompt_id)
286+
if not prompt:
287+
typer.echo(f"{Colors.RED}✗ Prompt not found: {prompt_id}{Colors.RESET}")
288+
raise typer.Exit(1)
289+
290+
try:
291+
manager._sync_prompt_to_file(prompt.content, app_type)
292+
typer.echo(f"{Colors.GREEN}✓ Synced '{prompt_id}' to {app_type}{Colors.RESET}")
293+
file_path = PROMPT_FILE_PATHS.get(app_type)
294+
typer.echo(f" {Colors.CYAN}File:{Colors.RESET} {file_path}")
295+
except Exception as e:
296+
typer.echo(f"{Colors.RED}✗ Error: {e}{Colors.RESET}")
297+
raise typer.Exit(1)
298+
else:
299+
# Sync all active prompts
300+
results = manager.sync_all()
301+
302+
for app, synced_prompt_id in results.items():
303+
if synced_prompt_id:
304+
typer.echo(f"{Colors.GREEN}{app}: synced ({synced_prompt_id}){Colors.RESET}")
305+
else:
306+
typer.echo(f"{Colors.YELLOW}{app}: no active prompt{Colors.RESET}")
268307

269308

270309
@prompt_app.command("import-live")

code_assistant_manager/prompts.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,12 @@ def get_active_prompt(self, app_type: str) -> Optional[Prompt]:
439439
return prompt
440440
return None
441441

442-
def sync_all(self) -> Dict[str, bool]:
442+
def sync_all(self) -> Dict[str, Optional[str]]:
443443
"""
444444
Sync all active prompts to their respective app files.
445445
446446
Returns:
447-
Dictionary mapping app types to success status
447+
Dictionary mapping app types to prompt ID synced (or None if no prompt)
448448
"""
449449
prompts = self._load_prompts()
450450
results = {}
@@ -462,11 +462,11 @@ def sync_all(self) -> Dict[str, bool]:
462462
if active_prompt:
463463
try:
464464
self._sync_prompt_to_file(active_prompt.content, app_type)
465-
results[app_type] = True
465+
results[app_type] = active_prompt.id
466466
except Exception as e:
467467
logger.error(f"Failed to sync prompt to {app_type}: {e}")
468-
results[app_type] = False
468+
results[app_type] = None
469469
else:
470-
results[app_type] = True # No active prompt is not an error
470+
results[app_type] = None # No active prompt
471471

472472
return results

code_assistant_manager/skills.py

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,52 @@
2323

2424
logger = logging.getLogger(__name__)
2525

26-
# Default skill repositories (similar to cc-switch)
27-
DEFAULT_SKILL_REPOS = [
28-
{
29-
"owner": "anthropics",
30-
"name": "skills",
31-
"branch": "main",
32-
"enabled": True,
33-
"skillsPath": None,
34-
},
35-
{
36-
"owner": "ComposioHQ",
37-
"name": "awesome-claude-skills",
38-
"branch": "main",
39-
"enabled": True,
40-
"skillsPath": None,
41-
},
42-
]
26+
27+
def _load_builtin_skill_repos() -> List[Dict]:
28+
"""Load built-in skill repos from the bundled skill_repos.json file."""
29+
# Look for skill_repos.json in the package directory
30+
package_dir = Path(__file__).parent
31+
repos_file = package_dir / "skill_repos.json"
32+
33+
if repos_file.exists():
34+
try:
35+
with open(repos_file, "r", encoding="utf-8") as f:
36+
repos_data = json.load(f)
37+
# Convert from dict format to list format
38+
return [
39+
{
40+
"owner": repo.get("owner"),
41+
"name": repo.get("name"),
42+
"branch": repo.get("branch", "main"),
43+
"enabled": repo.get("enabled", True),
44+
"skillsPath": repo.get("skillsPath"),
45+
}
46+
for repo in repos_data.values()
47+
]
48+
except Exception as e:
49+
logger.warning(f"Failed to load builtin skill repos: {e}")
50+
51+
# Fallback defaults if file not found
52+
return [
53+
{
54+
"owner": "ComposioHQ",
55+
"name": "awesome-claude-skills",
56+
"branch": "main",
57+
"enabled": True,
58+
"skillsPath": None,
59+
},
60+
{
61+
"owner": "obra",
62+
"name": "superpowers",
63+
"branch": "main",
64+
"enabled": True,
65+
"skillsPath": "skills",
66+
},
67+
]
68+
69+
70+
# Default skill repositories loaded from bundled file
71+
DEFAULT_SKILL_REPOS = _load_builtin_skill_repos()
4372

4473
# Skill install directories for each app type
4574
SKILL_INSTALL_DIRS = {
@@ -195,9 +224,10 @@ def _save_skills(self, skills: Dict[str, Skill]) -> None:
195224
raise
196225

197226
def _load_repos(self) -> Dict[str, SkillRepo]:
198-
"""Load skill repos from file."""
227+
"""Load skill repos from file. Initializes with defaults if file doesn't exist."""
199228
if not self.repos_file.exists():
200-
return {}
229+
# Initialize with default repos on first use
230+
self._init_default_repos_file()
201231

202232
try:
203233
with open(self.repos_file, "r") as f:
@@ -210,6 +240,23 @@ def _load_repos(self) -> Dict[str, SkillRepo]:
210240
logger.warning(f"Failed to load skill repos: {e}")
211241
return {}
212242

243+
def _init_default_repos_file(self) -> None:
244+
"""Initialize the repos file with default skill repos."""
245+
repos = {}
246+
for repo_data in DEFAULT_SKILL_REPOS:
247+
repo = SkillRepo(
248+
owner=repo_data["owner"],
249+
name=repo_data["name"],
250+
branch=repo_data.get("branch", "main"),
251+
enabled=repo_data.get("enabled", True),
252+
skills_path=repo_data.get("skillsPath"),
253+
)
254+
repo_id = f"{repo.owner}/{repo.name}"
255+
repos[repo_id] = repo
256+
257+
self._save_repos(repos)
258+
logger.info(f"Initialized {len(repos)} default skill repos")
259+
213260
def _save_repos(self, repos: Dict[str, SkillRepo]) -> None:
214261
"""Save skill repos to file."""
215262
try:

install.sh

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,30 @@ setup_config() {
168168

169169
mkdir -p ~/.config/code-assistant-manager
170170

171-
if [ -f "code_assistant_manager/providers.json" ]; then
172-
cp code_assistant_manager/providers.json ~/.config/code-assistant-manager/providers.json
171+
# Try to find config files from local dir or installed package
172+
local pkg_dir=""
173+
if [ -d "code_assistant_manager" ]; then
174+
pkg_dir="code_assistant_manager"
175+
else
176+
# Try to find from installed package
177+
pkg_dir=$(python3 -c "import code_assistant_manager; import os; print(os.path.dirname(code_assistant_manager.__file__))" 2>/dev/null || echo "")
178+
fi
179+
180+
if [ -n "$pkg_dir" ] && [ -f "$pkg_dir/providers.json" ]; then
181+
cp "$pkg_dir/providers.json" ~/.config/code-assistant-manager/providers.json
173182
print_success "Created providers.json"
174183
fi
175184

185+
# Initialize skill_repos.json with built-in repos
186+
if [ -n "$pkg_dir" ] && [ -f "$pkg_dir/skill_repos.json" ]; then
187+
if [ ! -f ~/.config/code-assistant-manager/skill_repos.json ]; then
188+
cp "$pkg_dir/skill_repos.json" ~/.config/code-assistant-manager/skill_repos.json
189+
print_success "Created skill_repos.json with default repositories"
190+
else
191+
print_info "skill_repos.json already exists, skipping"
192+
fi
193+
fi
194+
176195
if [ ! -f ~/.env ]; then
177196
touch ~/.env
178197
chmod 600 ~/.env

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ version = {attr = "code_assistant_manager.__version__"}
6464
"mcp/registry/*.json",
6565
"providers.json",
6666
"tools.yaml",
67+
"skill_repos.json",
6768
]
6869

6970
[project.scripts]

tests/unit/test_prompts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,9 @@ def test_sync_all(self, temp_config_dir, temp_prompt_dir):
363363
with patch.dict('code_assistant_manager.prompts.PROMPT_FILE_PATHS', {'claude': prompt_file, 'codex': temp_prompt_dir / "AGENTS.md", 'gemini': temp_prompt_dir / "GEMINI.md"}):
364364
results = manager.sync_all()
365365

366-
assert results["claude"] is True
366+
assert results["claude"] == "test" # Returns prompt ID
367+
assert results["codex"] is None # No active prompt
368+
assert results["gemini"] is None # No active prompt
367369
assert prompt_file.exists()
368370
assert prompt_file.read_text() == "Sync content"
369371

tests/unit/test_skills.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,16 +217,22 @@ def test_manager_delete(self, temp_config_dir):
217217
def test_manager_add_remove_repo(self, temp_config_dir):
218218
"""Test adding and removing skill repos."""
219219
manager = SkillManager(temp_config_dir)
220-
repo = SkillRepo(owner="owner", name="repo", branch="main")
220+
221+
# Get initial count (includes default repos)
222+
initial_repos = manager.get_repos()
223+
initial_count = len(initial_repos)
224+
225+
repo = SkillRepo(owner="testowner", name="testrepo", branch="main")
221226
manager.add_repo(repo)
222227

223228
repos = manager.get_repos()
224-
assert len(repos) == 1
225-
assert repos[0].owner == "owner"
229+
assert len(repos) == initial_count + 1
230+
# Check that our new repo is in the list
231+
assert any(r.owner == "testowner" and r.name == "testrepo" for r in repos)
226232

227-
manager.remove_repo("owner", "repo")
233+
manager.remove_repo("testowner", "testrepo")
228234
repos = manager.get_repos()
229-
assert len(repos) == 0
235+
assert len(repos) == initial_count
230236

231237
def test_manager_export_import(self, temp_config_dir):
232238
"""Test exporting and importing skills."""
@@ -287,11 +293,11 @@ def test_manager_init_default_repos(self, temp_config_dir):
287293
"""Test initializing default repos."""
288294
manager = SkillManager(temp_config_dir)
289295

290-
# Initially no repos
296+
# Repos are auto-initialized now, so should already have defaults
291297
repos = manager.get_repos()
292-
assert len(repos) == 0
298+
assert len(repos) == len(DEFAULT_SKILL_REPOS)
293299

294-
# Initialize defaults
300+
# Calling init_default_repos again should be idempotent
295301
manager.init_default_repos()
296302
repos = manager.get_repos()
297303
assert len(repos) == len(DEFAULT_SKILL_REPOS)

0 commit comments

Comments
 (0)