Skip to content

Commit 597502f

Browse files
zhujian0805claude
andcommitted
refactor: Rename sync/unsync commands to install/uninstall for consistency
- Rename 'sync' command to 'install' to match other CLI tools (skill, agent, plugin) - Rename 'unsync' command to 'uninstall' for consistency - Keep old command names as hidden aliases for backwards compatibility - Update command help text and examples to use new names - Update tests to verify new command behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5daecb2 commit 597502f

File tree

2 files changed

+114
-83
lines changed

2 files changed

+114
-83
lines changed

code_assistant_manager/cli/prompts_commands.py

Lines changed: 98 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -278,22 +278,23 @@ def clear_default_prompt():
278278
raise typer.Exit(1)
279279

280280

281-
@prompt_app.command("sync")
282-
def sync_prompts(
281+
@prompt_app.command("install")
282+
def install_prompts(
283283
prompt_id: Optional[str] = typer.Argument(
284-
None, help="Prompt ID to sync. If not specified, syncs the default prompt."
284+
None,
285+
help="Prompt ID to install. If not specified, installs the default prompt.",
285286
),
286287
app_type: Optional[str] = typer.Option(
287288
None,
288289
"--app",
289290
"-a",
290-
help="App(s) to sync to, comma-separated (e.g., 'claude,codex,gemini' or 'all'). Default: all user-level apps.",
291+
help="App(s) to install to, comma-separated (e.g., 'claude,codex,gemini' or 'all'). Default: all user-level apps.",
291292
),
292293
level: str = typer.Option(
293294
"user",
294295
"--level",
295296
"-l",
296-
help="Sync level: 'user' (~/.claude/) or 'project' (current directory). Copilot only supports 'project'.",
297+
help="Install level: 'user' (~/.claude/) or 'project' (current directory). Copilot only supports 'project'.",
297298
),
298299
project_dir: Optional[Path] = typer.Option(
299300
None,
@@ -312,20 +313,20 @@ def sync_prompts(
312313
help="(Copilot only) Exclude agent: 'coding-agent' or 'code-review'",
313314
),
314315
):
315-
"""Sync prompts to app files.
316+
"""Install prompts to app files.
316317
317-
Without arguments: syncs the default prompt to all user-level apps (claude, codex, gemini).
318+
Without arguments: installs the default prompt to all user-level apps (claude, codex, gemini).
318319
319320
Examples:
320-
cam prompt sync # Sync default to all apps
321-
cam prompt sync my-prompt -a claude # Sync specific prompt to claude
322-
cam prompt sync -a claude,codex # Sync default to multiple apps
323-
cam prompt sync my-prompt -a copilot # Sync to copilot
324-
cam prompt sync -a all -l project # Sync default to all apps at project level
321+
cam prompt install # Install default to all apps
322+
cam prompt install my-prompt -a claude # Install specific prompt to claude
323+
cam prompt install -a claude,codex # Install default to multiple apps
324+
cam prompt install my-prompt -a copilot # Install to copilot
325+
cam prompt install -a all -l project # Install default to all apps at project level
325326
"""
326327
manager = _get_prompt_manager()
327328

328-
# Determine which prompt to sync
329+
# Determine which prompt to install
329330
if prompt_id:
330331
prompt = manager.get(prompt_id)
331332
if not prompt:
@@ -365,7 +366,7 @@ def sync_prompts(
365366
else None
366367
)
367368

368-
# Sync to each target app
369+
# Install to each target app
369370
for app in target_apps:
370371
# Handle Copilot specially
371372
if app == "copilot":
@@ -374,7 +375,7 @@ def sync_prompts(
374375
f"{Colors.YELLOW}○ copilot: skipped (project level only){Colors.RESET}"
375376
)
376377
continue
377-
_sync_copilot(
378+
_install_copilot(
378379
manager, prompt_id, apply_to, exclude_agent, level_project_dir
379380
)
380381
continue
@@ -394,58 +395,57 @@ def sync_prompts(
394395
try:
395396
manager.sync_to_app(prompt_id, app, level, level_project_dir)
396397
typer.echo(
397-
f"{Colors.GREEN}{app}: synced '{prompt_id}' ({level}){Colors.RESET}"
398+
f"{Colors.GREEN}{app}: installed '{prompt_id}' ({level}){Colors.RESET}"
398399
)
399400
typer.echo(f" {Colors.CYAN}File:{Colors.RESET} {file_path}")
400401
except Exception as e:
401402
typer.echo(f"{Colors.RED}{app}: {e}{Colors.RESET}")
402403

403404

404-
def _sync_copilot(
405-
manager: PromptManager,
406-
prompt_id: str,
407-
apply_to: Optional[str],
408-
exclude_agent: Optional[str],
409-
project_dir: Optional[Path],
405+
# Add sync as an alias for install
406+
@prompt_app.command("sync", hidden=True)
407+
def sync_prompts_alias(
408+
prompt_id: Optional[str] = typer.Argument(
409+
None, help="Prompt ID to sync. If not specified, syncs the default prompt."
410+
),
411+
app_type: Optional[str] = typer.Option(
412+
None,
413+
"--app",
414+
"-a",
415+
help="App(s) to sync to, comma-separated (e.g., 'claude,codex,gemini' or 'all'). Default: all user-level apps.",
416+
),
417+
level: str = typer.Option(
418+
"user",
419+
"--level",
420+
"-l",
421+
help="Sync level: 'user' (~/.claude/) or 'project' (current directory). Copilot only supports 'project'.",
422+
),
423+
project_dir: Optional[Path] = typer.Option(
424+
None,
425+
"--project-dir",
426+
help="Project directory when using project level (defaults to current directory)",
427+
),
428+
# Copilot-specific options
429+
apply_to: Optional[str] = typer.Option(
430+
None,
431+
"--apply-to",
432+
help="(Copilot only) Glob pattern for path-specific instructions",
433+
),
434+
exclude_agent: Optional[str] = typer.Option(
435+
None,
436+
"--exclude-agent",
437+
help="(Copilot only) Exclude agent: 'coding-agent' or 'code-review'",
438+
),
410439
):
411-
"""Helper to sync a prompt to Copilot instructions."""
412-
prompt = manager.get(prompt_id)
413-
if not prompt:
414-
typer.echo(f"{Colors.RED}✗ Prompt not found: {prompt_id}{Colors.RESET}")
415-
raise typer.Exit(1)
416-
417-
instruction_type = "path-specific" if apply_to else "repo-wide"
418-
419-
try:
420-
manager.sync_copilot_instructions(
421-
prompt_id,
422-
instruction_type=instruction_type,
423-
apply_to=apply_to,
424-
exclude_agent=exclude_agent,
425-
project_dir=project_dir,
426-
)
427-
428-
typer.echo(
429-
f"{Colors.GREEN}✓ copilot: synced '{prompt_id}' ({instruction_type}){Colors.RESET}"
430-
)
431-
432-
if instruction_type == "repo-wide":
433-
typer.echo(
434-
f" {Colors.CYAN}File:{Colors.RESET} .github/copilot-instructions.md"
435-
)
436-
else:
437-
typer.echo(
438-
f" {Colors.CYAN}File:{Colors.RESET} .github/instructions/{prompt_id}.instructions.md"
439-
)
440-
typer.echo(f" {Colors.CYAN}Apply to:{Colors.RESET} {apply_to}")
441-
if exclude_agent:
442-
typer.echo(
443-
f" {Colors.CYAN}Exclude agent:{Colors.RESET} {exclude_agent}"
444-
)
445-
446-
except ValueError as e:
447-
typer.echo(f"{Colors.RED}✗ copilot: {e}{Colors.RESET}")
448-
raise typer.Exit(1)
440+
"""Sync prompts to app files (deprecated, use 'install' instead)."""
441+
return install_prompts(
442+
prompt_id=prompt_id,
443+
app_type=app_type,
444+
level=level,
445+
project_dir=project_dir,
446+
apply_to=apply_to,
447+
exclude_agent=exclude_agent,
448+
)
449449

450450

451451
@prompt_app.command("import-live")
@@ -711,19 +711,19 @@ def export_prompts(
711711
raise typer.Exit(1)
712712

713713

714-
@prompt_app.command("unsync")
715-
def unsync_prompt(
714+
@prompt_app.command("uninstall")
715+
def uninstall_prompt(
716716
app_type: str = typer.Option(
717717
...,
718718
"--app",
719719
"-a",
720-
help="App(s) to unsync, comma-separated (e.g., 'claude,codex' or 'all')",
720+
help="App(s) to uninstall, comma-separated (e.g., 'claude,codex' or 'all')",
721721
),
722722
level: str = typer.Option(
723723
"user",
724724
"--level",
725725
"-l",
726-
help="Unsync level: 'user', 'project', or 'all'. Copilot only supports 'project'.",
726+
help="Uninstall level: 'user', 'project', or 'all'. Copilot only supports 'project'.",
727727
),
728728
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
729729
project_dir: Optional[Path] = typer.Option(
@@ -732,7 +732,7 @@ def unsync_prompt(
732732
help="Project directory when clearing project prompts (defaults to current directory)",
733733
),
734734
):
735-
"""Clear/unsync prompt files for one or more app/level combinations.
735+
"""Clear/uninstall prompt files for one or more app/level combinations.
736736
737737
For Copilot, clears .github/copilot-instructions.md
738738
"""
@@ -776,7 +776,7 @@ def unsync_prompt(
776776

777777
if not force:
778778
summary = ", ".join(f"{app}:{lvl}" for app, lvl, *_ in all_targets)
779-
typer.confirm(f"Clear prompt files for {summary}?", abort=True)
779+
typer.confirm(f"Uninstall prompt files for {summary}?", abort=True)
780780

781781
for app, lvl, file_path, _ in all_targets:
782782
if not file_path.exists():
@@ -788,13 +788,44 @@ def unsync_prompt(
788788
try:
789789
file_path.write_text("", encoding="utf-8")
790790
typer.echo(
791-
f"{Colors.GREEN}Cleared prompt file: {file_path}{Colors.RESET}"
791+
f"{Colors.GREEN}Uninstalled prompt file: {file_path}{Colors.RESET}"
792792
)
793793
except Exception as e:
794794
typer.echo(f"{Colors.RED}✗ Error: {e}{Colors.RESET}")
795795
raise typer.Exit(1)
796796

797797

798+
# Add unsync as an alias for uninstall
799+
@prompt_app.command("unsync", hidden=True)
800+
def unsync_prompt_alias(
801+
app_type: str = typer.Option(
802+
...,
803+
"--app",
804+
"-a",
805+
help="App(s) to unsync, comma-separated (e.g., 'claude,codex' or 'all')",
806+
),
807+
level: str = typer.Option(
808+
"user",
809+
"--level",
810+
"-l",
811+
help="Unsync level: 'user', 'project', or 'all'. Copilot only supports 'project'.",
812+
),
813+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
814+
project_dir: Optional[Path] = typer.Option(
815+
None,
816+
"--project-dir",
817+
help="Project directory when clearing project prompts (defaults to current directory)",
818+
),
819+
):
820+
"""Clear/unsync prompt files (deprecated, use 'uninstall' instead)."""
821+
return uninstall_prompt(
822+
app_type=app_type,
823+
level=level,
824+
force=force,
825+
project_dir=project_dir,
826+
)
827+
828+
798829
@prompt_app.command("status")
799830
def show_prompt_status(
800831
level: str = typer.Option(

tests/unit/test_prompts_cli.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ def capture(message=""):
162162
assert combined.count("Level:") >= 2
163163

164164

165-
def test_sync_prompt_project_scope(cli_manager, tmp_path, monkeypatch):
166-
"""sync supports project scope for every app."""
165+
def test_install_prompt_project_scope(cli_manager, tmp_path, monkeypatch):
166+
"""install supports project scope for every app."""
167167
prompt = Prompt(id="cli", name="CLI", content="project-level content")
168168
cli_manager.create(prompt)
169169

@@ -175,7 +175,7 @@ def test_sync_prompt_project_scope(cli_manager, tmp_path, monkeypatch):
175175
prompts_commands.typer, "echo", lambda msg="": outputs.append(str(msg))
176176
)
177177

178-
prompts_commands.sync_prompts(
178+
prompts_commands.install_prompts(
179179
prompt_id="cli",
180180
app_type="claude",
181181
level="project",
@@ -190,8 +190,8 @@ def test_sync_prompt_project_scope(cli_manager, tmp_path, monkeypatch):
190190
assert any("CLAUDE.md" in msg for msg in outputs)
191191

192192

193-
def test_sync_default_prompt(cli_manager, tmp_path, monkeypatch):
194-
"""sync without prompt_id syncs the default prompt."""
193+
def test_install_default_prompt(cli_manager, tmp_path, monkeypatch):
194+
"""install without prompt_id installs the default prompt."""
195195
from code_assistant_manager.prompts import PROMPT_HANDLERS
196196

197197
# Setup user-level file path
@@ -215,7 +215,7 @@ def test_sync_default_prompt(cli_manager, tmp_path, monkeypatch):
215215
prompts_commands.typer, "echo", lambda msg="": outputs.append(str(msg))
216216
)
217217

218-
prompts_commands.sync_prompts(
218+
prompts_commands.install_prompts(
219219
prompt_id=None,
220220
app_type="claude",
221221
level="user",
@@ -227,7 +227,7 @@ def test_sync_default_prompt(cli_manager, tmp_path, monkeypatch):
227227

228228
stored = cli_manager.get("test")
229229
assert stored.is_default is True
230-
assert any("synced" in msg.lower() for msg in outputs)
230+
assert any("installed" in msg.lower() for msg in outputs)
231231

232232

233233
# Additional tests for uncovered functions
@@ -511,34 +511,34 @@ def test_parse_app_list_invalid():
511511
assert "invalid" in str(exc_info.value).lower()
512512

513513

514-
def test_unsync_prompt_project_level(cli_manager, tmp_path, monkeypatch):
515-
"""unsync_prompt clears project-level prompt file content."""
514+
def test_uninstall_prompt_project_level(cli_manager, tmp_path, monkeypatch):
515+
"""uninstall_prompt clears project-level prompt file content."""
516516
project_dir = tmp_path / "project"
517517
project_dir.mkdir()
518518
prompt_file = project_dir / "CLAUDE.md"
519-
prompt_file.write_text("Content to unsync")
519+
prompt_file.write_text("Content to uninstall")
520520

521521
outputs = []
522522
monkeypatch.setattr(
523523
prompts_commands.typer, "echo", lambda msg="": outputs.append(str(msg))
524524
)
525525

526-
prompts_commands.unsync_prompt(
526+
prompts_commands.uninstall_prompt(
527527
app_type="claude",
528528
level="project",
529529
force=True,
530530
project_dir=project_dir,
531531
)
532532

533-
# unsync clears the file content, not deletes it
533+
# uninstall clears the file content, not deletes it
534534
assert prompt_file.exists()
535535
assert prompt_file.read_text() == ""
536536
combined = "\n".join(outputs)
537-
assert "cleared" in combined.lower()
537+
assert "uninstalled" in combined.lower()
538538

539539

540-
def test_unsync_prompt_file_not_found(cli_manager, tmp_path, monkeypatch):
541-
"""unsync_prompt handles missing file gracefully."""
540+
def test_uninstall_prompt_file_not_found(cli_manager, tmp_path, monkeypatch):
541+
"""uninstall_prompt handles missing file gracefully."""
542542
project_dir = tmp_path / "project"
543543
project_dir.mkdir()
544544

@@ -547,7 +547,7 @@ def test_unsync_prompt_file_not_found(cli_manager, tmp_path, monkeypatch):
547547
prompts_commands.typer, "echo", lambda msg="": outputs.append(str(msg))
548548
)
549549

550-
prompts_commands.unsync_prompt(
550+
prompts_commands.uninstall_prompt(
551551
app_type="claude",
552552
level="project",
553553
force=True,

0 commit comments

Comments
 (0)