devsync 0.12.0__tar.gz → 0.14.0__tar.gz
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.
- {devsync-0.12.0 → devsync-0.14.0}/PKG-INFO +2 -1
- {devsync-0.12.0 → devsync-0.14.0}/README.md +1 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/capability_registry.py +2 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/cli/extract.py +168 -6
- {devsync-0.12.0 → devsync-0.14.0}/devsync/cli/install_v2.py +96 -10
- {devsync-0.12.0 → devsync-0.14.0}/devsync/cli/main.py +70 -4
- devsync-0.14.0/devsync/cli/tools.py +74 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/component_detector.py +301 -158
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/extractor.py +29 -6
- devsync-0.14.0/devsync/core/pip_utils.py +314 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/practice.py +9 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/prompts.py +5 -1
- {devsync-0.12.0 → devsync-0.14.0}/devsync.egg-info/PKG-INFO +2 -1
- {devsync-0.12.0 → devsync-0.14.0}/devsync.egg-info/SOURCES.txt +1 -0
- {devsync-0.12.0 → devsync-0.14.0}/pyproject.toml +1 -1
- devsync-0.12.0/devsync/cli/tools.py +0 -47
- {devsync-0.12.0 → devsync-0.14.0}/LICENSE +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/__main__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/aider.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/amazonq.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/amp.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/anteroom.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/antigravity.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/augment.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/base.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/claude.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/claude_desktop.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/cline.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/codex.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/continuedev.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/copilot.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/cursor.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/detector.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/gemini.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/jetbrains.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/junie.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/kiro.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/mcp_syncer.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/opencode.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/openhands.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/roo.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/tabnine.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/trae.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/translator.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/winsurf.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/ai_tools/zed.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/cli/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/cli/list_v2.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/cli/setup.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/cli/uninstall.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/adapter.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/checksum.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/conflict_resolution.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/git_operations.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/mcp/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/mcp/credentials.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/mcp/manager.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/mcp/set_manager.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/mcp/validator.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/mcp_credential_prompter.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/models.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/package_creator.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/package_manifest.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/package_manifest_v2.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/repository.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/secret_detector.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/core/version.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/anthropic.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/config.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/openai_provider.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/openrouter.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/provider.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/llm/response_models.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/storage/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/storage/mcp_tracker.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/storage/package_tracker.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/storage/tracker.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/tui/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/__init__.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/atomic_write.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/backup.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/dotenv.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/git_helpers.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/logging.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/namespace.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/paths.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/project.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/streaming.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/ui.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync/utils/validation.py +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync.egg-info/dependency_links.txt +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync.egg-info/entry_points.txt +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync.egg-info/requires.txt +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/devsync.egg-info/top_level.txt +0 -0
- {devsync-0.12.0 → devsync-0.14.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: devsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Distribute and sync dev tool configurations across teams
|
|
5
5
|
Author-email: Troy Larson <troy@calvinware.com>
|
|
6
6
|
License: MIT License
|
|
@@ -86,6 +86,7 @@ No API key? DevSync works without one -- it falls back to file-copy mode. Add `-
|
|
|
86
86
|
- **AI-powered extraction** -- LLM reads your project's rules, MCP configs, and commands to produce abstract practice declarations
|
|
87
87
|
- **AI-powered installation** -- LLM adapts incoming practices to your existing setup with intelligent merging
|
|
88
88
|
- **23+ AI tool integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, Kiro, Roo Code, Cline, Codex, and more
|
|
89
|
+
- **MCP server dependencies** -- auto-detects pip-installable MCP servers and prompts to install them (`--skip-pip` to skip)
|
|
89
90
|
- **MCP credential handling** -- prompts for credentials at install time, never stores them in repos
|
|
90
91
|
- **v1 backward compatibility** -- old `ai-config-kit-package.yaml` packages still install via file-copy
|
|
91
92
|
- **Graceful degradation** -- works without an API key, `--no-ai` flag for explicit file-copy mode
|
|
@@ -47,6 +47,7 @@ No API key? DevSync works without one -- it falls back to file-copy mode. Add `-
|
|
|
47
47
|
- **AI-powered extraction** -- LLM reads your project's rules, MCP configs, and commands to produce abstract practice declarations
|
|
48
48
|
- **AI-powered installation** -- LLM adapts incoming practices to your existing setup with intelligent merging
|
|
49
49
|
- **23+ AI tool integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, Kiro, Roo Code, Cline, Codex, and more
|
|
50
|
+
- **MCP server dependencies** -- auto-detects pip-installable MCP servers and prompts to install them (`--skip-pip` to skip)
|
|
50
51
|
- **MCP credential handling** -- prompts for credentials at install time, never stores them in repos
|
|
51
52
|
- **v1 backward compatibility** -- old `ai-config-kit-package.yaml` packages still install via file-copy
|
|
52
53
|
- **Graceful degradation** -- works without an API key, `--no-ai` flag for explicit file-copy mode
|
|
@@ -28,6 +28,7 @@ class IDECapability:
|
|
|
28
28
|
skills_directory: str | None = None # Claude skills
|
|
29
29
|
workflows_directory: str | None = None # Windsurf workflows
|
|
30
30
|
memory_file_name: str | None = None # CLAUDE.md
|
|
31
|
+
mcp_servers_json_key: str = "mcpServers" # JSON key for MCP servers in config files
|
|
31
32
|
notes: str = ""
|
|
32
33
|
|
|
33
34
|
def supports_component(self, component_type: ComponentType) -> bool:
|
|
@@ -494,6 +495,7 @@ CAPABILITY_REGISTRY: dict[AIToolType, IDECapability] = {
|
|
|
494
495
|
mcp_project_config_path=".vscode/mcp.json", # Workspace-level MCP config
|
|
495
496
|
hooks_directory=None, # Hooks not supported
|
|
496
497
|
commands_directory=None, # Commands not supported
|
|
498
|
+
mcp_servers_json_key="servers", # VS Code uses "servers" not "mcpServers"
|
|
497
499
|
notes=(
|
|
498
500
|
"GitHub Copilot uses .github/copilot-instructions.md (main) and "
|
|
499
501
|
".github/instructions/**/*.instructions.md (file-specific with globs). "
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
"""Extract command — reads project configs and produces a shareable package."""
|
|
2
2
|
|
|
3
3
|
import shutil
|
|
4
|
+
import warnings
|
|
5
|
+
from collections import Counter
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Optional
|
|
6
8
|
|
|
7
9
|
from rich.console import Console
|
|
8
10
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
-
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from devsync.core.component_detector import (
|
|
14
|
+
COMPONENT_TYPE_MAP,
|
|
15
|
+
ComponentDetector,
|
|
16
|
+
DetectionResult,
|
|
17
|
+
filter_detection_result,
|
|
18
|
+
)
|
|
10
19
|
from devsync.core.extractor import PracticeExtractor
|
|
11
20
|
from devsync.core.package_manifest_v2 import PackageManifestV2, detect_manifest_format, parse_manifest
|
|
12
21
|
from devsync.llm.config import load_config
|
|
@@ -14,6 +23,89 @@ from devsync.llm.provider import resolve_provider
|
|
|
14
23
|
|
|
15
24
|
console = Console()
|
|
16
25
|
|
|
26
|
+
# Map DetectionResult field names back to user-facing component names
|
|
27
|
+
_FIELD_TO_LABEL: dict[str, str] = {
|
|
28
|
+
"instructions": "Rules",
|
|
29
|
+
"mcp_servers": "MCP",
|
|
30
|
+
"hooks": "Hooks",
|
|
31
|
+
"commands": "Commands",
|
|
32
|
+
"skills": "Skills",
|
|
33
|
+
"workflows": "Workflows",
|
|
34
|
+
"memory_files": "Memory",
|
|
35
|
+
"resources": "Resources",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_detection_rows(detection: DetectionResult) -> list[tuple[str, str, int]]:
|
|
40
|
+
"""Build (component_label, source_tool, count) rows from detection.
|
|
41
|
+
|
|
42
|
+
Groups by component type + source tool for a concise table.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of (label, source, count) tuples, sorted by label then source.
|
|
46
|
+
"""
|
|
47
|
+
rows: list[tuple[str, str, int]] = []
|
|
48
|
+
|
|
49
|
+
for field_name, label in _FIELD_TO_LABEL.items():
|
|
50
|
+
items = getattr(detection, field_name, [])
|
|
51
|
+
if not items:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
source_counts: Counter[str] = Counter()
|
|
55
|
+
for item in items:
|
|
56
|
+
source = getattr(item, "source_tool", "") or getattr(item, "source_ide", "") or "project"
|
|
57
|
+
source_counts[source] += 1
|
|
58
|
+
|
|
59
|
+
for source, count in sorted(source_counts.items()):
|
|
60
|
+
rows.append((label, source, count))
|
|
61
|
+
|
|
62
|
+
return rows
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _display_detection_summary(detection: DetectionResult) -> None:
|
|
66
|
+
"""Display a Rich table summarizing detected components."""
|
|
67
|
+
rows = _get_detection_rows(detection)
|
|
68
|
+
|
|
69
|
+
if not rows:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
table = Table(title="Detected Components", show_header=True, header_style="bold cyan")
|
|
73
|
+
table.add_column("Component", style="cyan", no_wrap=True)
|
|
74
|
+
table.add_column("Source", style="green")
|
|
75
|
+
table.add_column("Count", justify="right")
|
|
76
|
+
|
|
77
|
+
for label, source, count in rows:
|
|
78
|
+
table.add_row(label, source, str(count))
|
|
79
|
+
|
|
80
|
+
console.print()
|
|
81
|
+
console.print(table)
|
|
82
|
+
|
|
83
|
+
tool_sources = {source for _, source, _ in rows if source not in ("project", "devsync")}
|
|
84
|
+
tool_count = len(tool_sources) if tool_sources else 1
|
|
85
|
+
console.print(f"\n Total: {detection.total_count} components from {tool_count} tool(s)")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _display_zero_result_warning(
|
|
89
|
+
tool: Optional[list[str]] = None,
|
|
90
|
+
component: Optional[list[str]] = None,
|
|
91
|
+
include_global: bool = False,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Display a helpful warning when filters match no components."""
|
|
94
|
+
console.print("\n[yellow]No components found matching your filters.[/yellow]")
|
|
95
|
+
|
|
96
|
+
active_filters = []
|
|
97
|
+
if tool:
|
|
98
|
+
active_filters.append(f"--tool {' --tool '.join(tool)}")
|
|
99
|
+
if component:
|
|
100
|
+
active_filters.append(f"--component {' --component '.join(component)}")
|
|
101
|
+
if active_filters:
|
|
102
|
+
console.print(f"\n Active: {' '.join(active_filters)}")
|
|
103
|
+
|
|
104
|
+
console.print("\n Suggestions:")
|
|
105
|
+
console.print(" - Run [cyan]devsync extract --dry-run[/cyan] without filters to see all available components")
|
|
106
|
+
if not include_global:
|
|
107
|
+
console.print(" - Try [cyan]--include-global[/cyan] to include home directory configs")
|
|
108
|
+
|
|
17
109
|
|
|
18
110
|
def extract_command(
|
|
19
111
|
output: Optional[str] = None,
|
|
@@ -21,6 +113,11 @@ def extract_command(
|
|
|
21
113
|
no_ai: bool = False,
|
|
22
114
|
project_dir: Optional[str] = None,
|
|
23
115
|
upgrade: Optional[str] = None,
|
|
116
|
+
tool: Optional[list[str]] = None,
|
|
117
|
+
component: Optional[list[str]] = None,
|
|
118
|
+
scope: str = "project",
|
|
119
|
+
dry_run: bool = False,
|
|
120
|
+
include_global: bool = False,
|
|
24
121
|
) -> int:
|
|
25
122
|
"""Extract practices from the current project into a shareable package.
|
|
26
123
|
|
|
@@ -30,6 +127,11 @@ def extract_command(
|
|
|
30
127
|
no_ai: Force file-copy mode (no LLM calls).
|
|
31
128
|
project_dir: Project directory to extract from. Defaults to cwd.
|
|
32
129
|
upgrade: Path to a v1 package to convert to v2 format.
|
|
130
|
+
tool: Only extract from these AI tool(s).
|
|
131
|
+
component: Only extract these component types.
|
|
132
|
+
scope: Detection scope — project, global, or all. Deprecated; use include_global.
|
|
133
|
+
dry_run: Show what would be extracted without writing files.
|
|
134
|
+
include_global: Include home directory / global configs.
|
|
33
135
|
|
|
34
136
|
Returns:
|
|
35
137
|
Exit code (0 = success).
|
|
@@ -37,11 +139,70 @@ def extract_command(
|
|
|
37
139
|
if upgrade:
|
|
38
140
|
return _upgrade_v1_package(upgrade, output=output, name=name, no_ai=no_ai)
|
|
39
141
|
|
|
142
|
+
# Resolve effective scope: --include-global takes precedence
|
|
143
|
+
effective_scope = "project"
|
|
144
|
+
if include_global:
|
|
145
|
+
effective_scope = "all"
|
|
146
|
+
elif scope != "project":
|
|
147
|
+
warnings.warn(
|
|
148
|
+
"--scope is deprecated, use --include-global instead",
|
|
149
|
+
DeprecationWarning,
|
|
150
|
+
stacklevel=2,
|
|
151
|
+
)
|
|
152
|
+
console.print("[yellow]--scope is deprecated. Use --include-global instead.[/yellow]")
|
|
153
|
+
effective_scope = scope
|
|
154
|
+
|
|
155
|
+
# Validate scope
|
|
156
|
+
if effective_scope not in ("project", "global", "all"):
|
|
157
|
+
console.print(f"[red]Invalid scope: {effective_scope}. Must be project, global, or all.[/red]")
|
|
158
|
+
return 1
|
|
159
|
+
|
|
160
|
+
# Validate tool names
|
|
161
|
+
if tool:
|
|
162
|
+
from devsync.ai_tools.detector import AIToolDetector
|
|
163
|
+
|
|
164
|
+
detector = AIToolDetector()
|
|
165
|
+
for t in tool:
|
|
166
|
+
if not detector.validate_tool_name(t):
|
|
167
|
+
console.print(f"[red]Unknown tool: {t}[/red]")
|
|
168
|
+
console.print(f"Supported tools: {', '.join(detector.get_tool_names())}")
|
|
169
|
+
return 1
|
|
170
|
+
|
|
171
|
+
# Validate component names
|
|
172
|
+
if component:
|
|
173
|
+
for c in component:
|
|
174
|
+
if c.lower() not in COMPONENT_TYPE_MAP:
|
|
175
|
+
valid = sorted(set(COMPONENT_TYPE_MAP.keys()))
|
|
176
|
+
console.print(f"[red]Unknown component type: {c}[/red]")
|
|
177
|
+
console.print(f"Valid types: {', '.join(valid)}")
|
|
178
|
+
return 1
|
|
179
|
+
|
|
40
180
|
project_path = Path(project_dir) if project_dir else Path.cwd()
|
|
41
181
|
if not project_path.is_dir():
|
|
42
182
|
console.print(f"[red]Not a directory: {project_path}[/red]")
|
|
43
183
|
return 1
|
|
44
184
|
|
|
185
|
+
# Phase 1: Detection + filtering
|
|
186
|
+
comp_detector = ComponentDetector(project_path, scope=effective_scope, tool_filter=tool)
|
|
187
|
+
detection = comp_detector.detect_all()
|
|
188
|
+
|
|
189
|
+
if component:
|
|
190
|
+
detection = filter_detection_result(detection, component_filter=component)
|
|
191
|
+
|
|
192
|
+
# Zero-result handling
|
|
193
|
+
if detection.total_count == 0:
|
|
194
|
+
_display_zero_result_warning(tool=tool, component=component, include_global=include_global)
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
# Display detection summary
|
|
198
|
+
_display_detection_summary(detection)
|
|
199
|
+
|
|
200
|
+
# Dry-run: stop here
|
|
201
|
+
if dry_run:
|
|
202
|
+
console.print("\n[dim]Dry run — no files written.[/dim]")
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
# Phase 2: Extraction
|
|
45
206
|
package_name = name or project_path.name
|
|
46
207
|
output_path = Path(output) if output else project_path / "devsync-package"
|
|
47
208
|
|
|
@@ -63,8 +224,8 @@ def extract_command(
|
|
|
63
224
|
TextColumn("[progress.description]{task.description}"),
|
|
64
225
|
console=console,
|
|
65
226
|
) as progress:
|
|
66
|
-
task = progress.add_task("
|
|
67
|
-
result = extractor.extract(project_path)
|
|
227
|
+
task = progress.add_task("Extracting practices...", total=None)
|
|
228
|
+
result = extractor.extract(project_path, detection=detection)
|
|
68
229
|
progress.update(task, description="Building package...")
|
|
69
230
|
|
|
70
231
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -96,12 +257,13 @@ def extract_command(
|
|
|
96
257
|
manifest_path = output_path / "devsync-package.yaml"
|
|
97
258
|
manifest_path.write_text(manifest.to_yaml())
|
|
98
259
|
|
|
260
|
+
# Enhanced output
|
|
99
261
|
mode = "[green]AI-powered[/green]" if result.ai_powered else "[yellow]file-copy[/yellow]"
|
|
100
|
-
console.print(f"\
|
|
101
|
-
console.print(f" Practices: {len(result.practices)}")
|
|
262
|
+
console.print(f"\nExtraction complete ({mode})")
|
|
263
|
+
console.print(f" Practices generated: {len(result.practices)}")
|
|
102
264
|
console.print(f" MCP servers: {len(result.mcp_servers)}")
|
|
103
265
|
console.print(f" Source files: {len(result.source_files)}")
|
|
104
|
-
console.print(f"\
|
|
266
|
+
console.print(f"\n Package written to: [cyan]{output_path}[/cyan]")
|
|
105
267
|
return 0
|
|
106
268
|
|
|
107
269
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"""V2 install command — AI-powered package installation."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import shutil
|
|
4
6
|
import tempfile
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from devsync.core.practice import MCPDeclaration
|
|
7
12
|
|
|
8
13
|
from rich.console import Console
|
|
9
14
|
from rich.prompt import Confirm
|
|
@@ -26,6 +31,7 @@ def install_v2_command(
|
|
|
26
31
|
no_ai: bool = False,
|
|
27
32
|
conflict: str = "prompt",
|
|
28
33
|
project_dir: Optional[str] = None,
|
|
34
|
+
skip_pip: bool = False,
|
|
29
35
|
) -> int:
|
|
30
36
|
"""Install a package into the current project.
|
|
31
37
|
|
|
@@ -37,6 +43,7 @@ def install_v2_command(
|
|
|
37
43
|
no_ai: Disable AI-powered adaptation.
|
|
38
44
|
conflict: Conflict strategy ('prompt', 'skip', 'overwrite', 'rename').
|
|
39
45
|
project_dir: Target project directory. Defaults to cwd.
|
|
46
|
+
skip_pip: Skip pip package installations for MCP servers.
|
|
40
47
|
|
|
41
48
|
Returns:
|
|
42
49
|
Exit code (0 = success).
|
|
@@ -72,8 +79,8 @@ def install_v2_command(
|
|
|
72
79
|
console.print(f" Tools: {', '.join(target_tools)}")
|
|
73
80
|
|
|
74
81
|
if manifest.is_v2 and manifest.has_practices and not no_ai:
|
|
75
|
-
return _install_v2_ai(manifest, project_root, target_tools)
|
|
76
|
-
return _install_v2_fallback(manifest, package_path, project_root, target_tools, conflict)
|
|
82
|
+
return _install_v2_ai(manifest, project_root, target_tools, skip_pip=skip_pip)
|
|
83
|
+
return _install_v2_fallback(manifest, package_path, project_root, target_tools, conflict, skip_pip=skip_pip)
|
|
77
84
|
finally:
|
|
78
85
|
if cloned_tmp and cloned_tmp.exists():
|
|
79
86
|
shutil.rmtree(cloned_tmp, ignore_errors=True)
|
|
@@ -125,6 +132,7 @@ def _install_v2_ai(
|
|
|
125
132
|
manifest: PackageManifestV2,
|
|
126
133
|
project_root: Path,
|
|
127
134
|
target_tools: list[str],
|
|
135
|
+
skip_pip: bool = False,
|
|
128
136
|
) -> int:
|
|
129
137
|
"""Install using AI-powered adaptation."""
|
|
130
138
|
config = load_config()
|
|
@@ -142,7 +150,7 @@ def _install_v2_ai(
|
|
|
142
150
|
_execute_plan(plan, project_root, target_tools)
|
|
143
151
|
|
|
144
152
|
if manifest.mcp_servers:
|
|
145
|
-
_install_mcp_servers(manifest, project_root)
|
|
153
|
+
_install_mcp_servers(manifest, project_root, skip_pip=skip_pip)
|
|
146
154
|
|
|
147
155
|
console.print(f"\n[green]Installed {manifest.name} successfully.[/green]")
|
|
148
156
|
return 0
|
|
@@ -154,6 +162,7 @@ def _install_v2_fallback(
|
|
|
154
162
|
project_root: Path,
|
|
155
163
|
target_tools: list[str],
|
|
156
164
|
conflict: str,
|
|
165
|
+
skip_pip: bool = False,
|
|
157
166
|
) -> int:
|
|
158
167
|
"""Install using file-copy mode (v1 compat or --no-ai)."""
|
|
159
168
|
installed_count = 0
|
|
@@ -205,7 +214,7 @@ def _install_v2_fallback(
|
|
|
205
214
|
console.print(f" Installed: {ref.name} → {dest.relative_to(project_root)}")
|
|
206
215
|
|
|
207
216
|
if manifest.mcp_servers:
|
|
208
|
-
_install_mcp_servers(manifest, project_root)
|
|
217
|
+
_install_mcp_servers(manifest, project_root, skip_pip=skip_pip)
|
|
209
218
|
|
|
210
219
|
console.print(f"\n[green]Installed {installed_count} instructions.[/green]")
|
|
211
220
|
return 0
|
|
@@ -258,17 +267,94 @@ def _get_tool_instruction_path(tool_name: str, project_root: Path, instruction_n
|
|
|
258
267
|
return project_root / dir_name / f"{instruction_name}{ext}"
|
|
259
268
|
|
|
260
269
|
|
|
261
|
-
def _install_mcp_servers(
|
|
262
|
-
|
|
263
|
-
|
|
270
|
+
def _install_mcp_servers(
|
|
271
|
+
manifest: PackageManifestV2,
|
|
272
|
+
project_root: Path,
|
|
273
|
+
skip_pip: bool = False,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Install MCP server configurations with pip dependencies and credential prompting."""
|
|
276
|
+
failed_pip_servers = _install_pip_dependencies(manifest.mcp_servers, skip_pip=skip_pip)
|
|
277
|
+
|
|
278
|
+
# Skip credential prompting for servers whose pip deps failed
|
|
279
|
+
eligible_servers = [s for s in manifest.mcp_servers if s.name not in failed_pip_servers]
|
|
280
|
+
servers_with_creds = [s for s in eligible_servers if s.credentials]
|
|
264
281
|
if servers_with_creds:
|
|
265
282
|
env_path = project_root / ".devsync" / ".env"
|
|
266
283
|
credentials = prompt_mcp_credentials(servers_with_creds, env_path=env_path)
|
|
267
284
|
|
|
268
|
-
for server in
|
|
285
|
+
for server in eligible_servers:
|
|
269
286
|
server_creds = credentials.get(server.name, {})
|
|
270
287
|
build_mcp_config(server, server_creds)
|
|
271
288
|
console.print(f" MCP: {server.name} configured")
|
|
272
289
|
else:
|
|
273
|
-
for server in
|
|
290
|
+
for server in eligible_servers:
|
|
274
291
|
console.print(f" MCP: {server.name} (no credentials needed)")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _install_pip_dependencies(
|
|
295
|
+
mcp_servers: list[MCPDeclaration],
|
|
296
|
+
skip_pip: bool = False,
|
|
297
|
+
) -> set[str]:
|
|
298
|
+
"""Install pip package dependencies for MCP servers.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
mcp_servers: List of MCPDeclaration objects.
|
|
302
|
+
skip_pip: If True, skip all pip installations.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Set of server names whose pip dependency installation failed or was declined.
|
|
306
|
+
"""
|
|
307
|
+
from devsync.core.pip_utils import (
|
|
308
|
+
get_installed_version,
|
|
309
|
+
install_pip_package,
|
|
310
|
+
installed_version_satisfies,
|
|
311
|
+
validate_pip_spec,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
failed_servers: set[str] = set()
|
|
315
|
+
servers_with_pip = [s for s in mcp_servers if s.pip_package]
|
|
316
|
+
if not servers_with_pip:
|
|
317
|
+
return failed_servers
|
|
318
|
+
|
|
319
|
+
console.print("\n[bold]MCP Server Dependencies[/bold]")
|
|
320
|
+
|
|
321
|
+
if skip_pip:
|
|
322
|
+
console.print(" [yellow]Skipping pip installations (--skip-pip)[/yellow]")
|
|
323
|
+
for server in servers_with_pip:
|
|
324
|
+
console.print(f" [dim]{server.name}: {server.pip_package} (skipped)[/dim]")
|
|
325
|
+
return failed_servers
|
|
326
|
+
|
|
327
|
+
for server in servers_with_pip:
|
|
328
|
+
spec = server.pip_package
|
|
329
|
+
assert spec is not None # guarded by servers_with_pip filter
|
|
330
|
+
|
|
331
|
+
if not validate_pip_spec(spec):
|
|
332
|
+
console.print(f" [red]Invalid package spec for {server.name}: {spec}[/red]")
|
|
333
|
+
failed_servers.add(server.name)
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
if installed_version_satisfies(spec):
|
|
337
|
+
installed_ver = get_installed_version(spec)
|
|
338
|
+
console.print(f" [dim]{server.name}: {spec} already installed (v{installed_ver})[/dim]")
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
console.print(f" [cyan]{server.name} requires pip package: {spec}[/cyan]")
|
|
342
|
+
if server.description:
|
|
343
|
+
console.print(f" [dim]{server.description}[/dim]")
|
|
344
|
+
|
|
345
|
+
if not Confirm.ask(f" Install {spec}?", default=True):
|
|
346
|
+
console.print(f" [yellow]Skipped pip install for {server.name}[/yellow]")
|
|
347
|
+
failed_servers.add(server.name)
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
with console.status(f" Installing {spec}..."):
|
|
351
|
+
success, message = install_pip_package(spec)
|
|
352
|
+
|
|
353
|
+
if success:
|
|
354
|
+
console.print(f" [green]{message}[/green]")
|
|
355
|
+
else:
|
|
356
|
+
console.print(f" [red]{message}[/red]")
|
|
357
|
+
console.print(f" [yellow]MCP server {server.name} may not work without {spec}[/yellow]")
|
|
358
|
+
failed_servers.add(server.name)
|
|
359
|
+
|
|
360
|
+
return failed_servers
|
|
@@ -30,13 +30,20 @@ def setup() -> None:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
@app.command()
|
|
33
|
-
def tools(
|
|
33
|
+
def tools(
|
|
34
|
+
verbose: bool = typer.Option(
|
|
35
|
+
False,
|
|
36
|
+
"--verbose",
|
|
37
|
+
"-v",
|
|
38
|
+
help="Show capabilities and valid filter names",
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
34
41
|
"""Show detected AI coding tools.
|
|
35
42
|
|
|
36
43
|
Display which AI coding tools are installed on your system
|
|
37
44
|
and where their configuration directories are located.
|
|
38
45
|
"""
|
|
39
|
-
exit_code = show_tools()
|
|
46
|
+
exit_code = show_tools(verbose=verbose)
|
|
40
47
|
raise typer.Exit(code=exit_code)
|
|
41
48
|
|
|
42
49
|
|
|
@@ -70,11 +77,41 @@ def extract(
|
|
|
70
77
|
"--upgrade",
|
|
71
78
|
help="Convert a v1 package to v2 format",
|
|
72
79
|
),
|
|
80
|
+
tool: Optional[list[str]] = typer.Option(
|
|
81
|
+
None,
|
|
82
|
+
"--tool",
|
|
83
|
+
"-t",
|
|
84
|
+
help="Only extract from specific AI tool(s). Repeatable.",
|
|
85
|
+
),
|
|
86
|
+
component: Optional[list[str]] = typer.Option(
|
|
87
|
+
None,
|
|
88
|
+
"--component",
|
|
89
|
+
"-c",
|
|
90
|
+
help="Component types to extract: rules, mcp, hooks, commands, skills, workflows, memory. Repeatable.",
|
|
91
|
+
),
|
|
92
|
+
scope: str = typer.Option(
|
|
93
|
+
"project",
|
|
94
|
+
"--scope",
|
|
95
|
+
"-s",
|
|
96
|
+
help="(Deprecated) Detection scope: project, global, or all. Use --include-global instead.",
|
|
97
|
+
hidden=True,
|
|
98
|
+
),
|
|
99
|
+
dry_run: bool = typer.Option(
|
|
100
|
+
False,
|
|
101
|
+
"--dry-run",
|
|
102
|
+
help="Show detected components without writing files or calling the LLM",
|
|
103
|
+
),
|
|
104
|
+
include_global: bool = typer.Option(
|
|
105
|
+
False,
|
|
106
|
+
"--include-global",
|
|
107
|
+
help="Include home directory / global configs in extraction",
|
|
108
|
+
),
|
|
73
109
|
) -> None:
|
|
74
110
|
"""Extract practices from a project into a shareable package.
|
|
75
111
|
|
|
76
|
-
Reads your project's AI tool configs (rules, MCP servers, hooks, commands
|
|
77
|
-
and produces a devsync-package.yaml
|
|
112
|
+
Reads your project's AI tool configs (rules, MCP servers, hooks, commands,
|
|
113
|
+
skills, workflows, memory files, resources) and produces a devsync-package.yaml
|
|
114
|
+
with abstract practice declarations.
|
|
78
115
|
|
|
79
116
|
Examples:
|
|
80
117
|
# AI-powered extraction
|
|
@@ -86,6 +123,21 @@ def extract(
|
|
|
86
123
|
# Custom output and name
|
|
87
124
|
devsync extract --output ./my-package --name team-standards
|
|
88
125
|
|
|
126
|
+
# Extract only from Cursor
|
|
127
|
+
devsync extract --tool cursor
|
|
128
|
+
|
|
129
|
+
# Extract only MCP configs
|
|
130
|
+
devsync extract --component mcp
|
|
131
|
+
|
|
132
|
+
# Preview what would be extracted (no files written)
|
|
133
|
+
devsync extract --dry-run
|
|
134
|
+
|
|
135
|
+
# Include home directory / global configs
|
|
136
|
+
devsync extract --include-global
|
|
137
|
+
|
|
138
|
+
# Combine filters: extract only rules and hooks from Claude Code
|
|
139
|
+
devsync extract --tool claude --component rules --component hooks
|
|
140
|
+
|
|
89
141
|
# Upgrade v1 package to v2
|
|
90
142
|
devsync extract --upgrade ./old-package
|
|
91
143
|
"""
|
|
@@ -97,6 +149,11 @@ def extract(
|
|
|
97
149
|
no_ai=no_ai,
|
|
98
150
|
project_dir=project_dir,
|
|
99
151
|
upgrade=upgrade,
|
|
152
|
+
tool=tool,
|
|
153
|
+
component=component,
|
|
154
|
+
scope=scope,
|
|
155
|
+
dry_run=dry_run,
|
|
156
|
+
include_global=include_global,
|
|
100
157
|
)
|
|
101
158
|
raise typer.Exit(code=exit_code)
|
|
102
159
|
|
|
@@ -130,6 +187,11 @@ def install(
|
|
|
130
187
|
"-p",
|
|
131
188
|
help="Target project directory (default: current directory)",
|
|
132
189
|
),
|
|
190
|
+
skip_pip: bool = typer.Option(
|
|
191
|
+
False,
|
|
192
|
+
"--skip-pip",
|
|
193
|
+
help="Skip pip package installations for MCP servers",
|
|
194
|
+
),
|
|
133
195
|
) -> None:
|
|
134
196
|
"""Install a package into the current project.
|
|
135
197
|
|
|
@@ -151,6 +213,9 @@ def install(
|
|
|
151
213
|
|
|
152
214
|
# Skip conflicts
|
|
153
215
|
devsync install ./package --conflict skip
|
|
216
|
+
|
|
217
|
+
# Skip pip installations
|
|
218
|
+
devsync install ./package --skip-pip
|
|
154
219
|
"""
|
|
155
220
|
from devsync.cli.install_v2 import install_v2_command
|
|
156
221
|
|
|
@@ -160,6 +225,7 @@ def install(
|
|
|
160
225
|
no_ai=no_ai,
|
|
161
226
|
conflict=conflict,
|
|
162
227
|
project_dir=project_dir,
|
|
228
|
+
skip_pip=skip_pip,
|
|
163
229
|
)
|
|
164
230
|
raise typer.Exit(code=exit_code)
|
|
165
231
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Tools command to show detected AI coding tools."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from devsync.ai_tools.detector import get_detector
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
# Map ComponentType enum values to user-facing filter names
|
|
11
|
+
_COMPONENT_TYPE_LABELS: dict[str, str] = {
|
|
12
|
+
"instruction": "rules",
|
|
13
|
+
"mcp_server": "mcp",
|
|
14
|
+
"hook": "hooks",
|
|
15
|
+
"command": "commands",
|
|
16
|
+
"skill": "skills",
|
|
17
|
+
"workflow": "workflows",
|
|
18
|
+
"resource": "resources",
|
|
19
|
+
"memory_file": "memory",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def show_tools(verbose: bool = False) -> int:
|
|
24
|
+
"""Show detected AI coding tools.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
verbose: Show capabilities column and valid filter names.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Exit code (0 for success)
|
|
31
|
+
"""
|
|
32
|
+
from devsync.ai_tools.capability_registry import CAPABILITY_REGISTRY
|
|
33
|
+
|
|
34
|
+
detector = get_detector()
|
|
35
|
+
|
|
36
|
+
table = Table(title="AI Coding Tools", show_header=True, header_style="bold cyan")
|
|
37
|
+
table.add_column("Tool", style="cyan", no_wrap=True)
|
|
38
|
+
table.add_column("Status", style="green")
|
|
39
|
+
if verbose:
|
|
40
|
+
table.add_column("Capabilities", style="dim")
|
|
41
|
+
|
|
42
|
+
for tool_type, tool in detector.tools.items():
|
|
43
|
+
is_installed = tool.is_installed()
|
|
44
|
+
status = "[green]✓ Installed[/green]" if is_installed else "[red]✗ Not found[/red]"
|
|
45
|
+
|
|
46
|
+
if verbose:
|
|
47
|
+
cap = CAPABILITY_REGISTRY.get(tool_type)
|
|
48
|
+
if cap:
|
|
49
|
+
labels = sorted(_COMPONENT_TYPE_LABELS.get(ct.value, ct.value) for ct in cap.supported_components)
|
|
50
|
+
caps_str = ", ".join(labels)
|
|
51
|
+
else:
|
|
52
|
+
caps_str = ""
|
|
53
|
+
table.add_row(tool.tool_name, status, caps_str)
|
|
54
|
+
else:
|
|
55
|
+
table.add_row(tool.tool_name, status)
|
|
56
|
+
|
|
57
|
+
console.print()
|
|
58
|
+
console.print(table)
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
installed = detector.detect_installed_tools()
|
|
62
|
+
if installed:
|
|
63
|
+
tool_names = ", ".join([t.tool_name for t in installed])
|
|
64
|
+
console.print(f"[green]Found {len(installed)} installed tool(s):[/green] {tool_names}")
|
|
65
|
+
else:
|
|
66
|
+
console.print("[yellow]No AI coding tools detected[/yellow]")
|
|
67
|
+
console.print("\nSupported tools: Cursor, GitHub Copilot, Winsurf, Claude Code")
|
|
68
|
+
|
|
69
|
+
if verbose:
|
|
70
|
+
all_labels = sorted(set(_COMPONENT_TYPE_LABELS.values()))
|
|
71
|
+
console.print(f"\nValid --component names: {', '.join(all_labels)}")
|
|
72
|
+
|
|
73
|
+
console.print()
|
|
74
|
+
return 0
|