devsync 0.11.0__tar.gz → 0.12.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.
Files changed (124) hide show
  1. {devsync-0.11.0 → devsync-0.12.0}/PKG-INFO +41 -13
  2. {devsync-0.11.0 → devsync-0.12.0}/README.md +39 -12
  3. devsync-0.12.0/devsync/cli/extract.py +232 -0
  4. devsync-0.12.0/devsync/cli/install_v2.py +274 -0
  5. devsync-0.12.0/devsync/cli/list_v2.py +92 -0
  6. devsync-0.12.0/devsync/cli/main.py +251 -0
  7. devsync-0.12.0/devsync/cli/setup.py +69 -0
  8. devsync-0.12.0/devsync/core/adapter.py +181 -0
  9. devsync-0.12.0/devsync/core/extractor.py +178 -0
  10. devsync-0.12.0/devsync/core/mcp_credential_prompter.py +129 -0
  11. devsync-0.12.0/devsync/core/package_manifest_v2.py +240 -0
  12. devsync-0.12.0/devsync/core/practice.py +174 -0
  13. devsync-0.12.0/devsync/llm/__init__.py +13 -0
  14. devsync-0.12.0/devsync/llm/anthropic.py +97 -0
  15. devsync-0.12.0/devsync/llm/config.py +91 -0
  16. devsync-0.12.0/devsync/llm/openai_provider.py +98 -0
  17. devsync-0.12.0/devsync/llm/openrouter.py +100 -0
  18. devsync-0.12.0/devsync/llm/prompts.py +116 -0
  19. devsync-0.12.0/devsync/llm/provider.py +137 -0
  20. devsync-0.12.0/devsync/llm/response_models.py +186 -0
  21. devsync-0.12.0/devsync/tui/__init__.py +1 -0
  22. {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/PKG-INFO +41 -13
  23. {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/SOURCES.txt +17 -25
  24. {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/requires.txt +1 -0
  25. {devsync-0.11.0 → devsync-0.12.0}/pyproject.toml +2 -1
  26. devsync-0.11.0/devsync/cli/delete.py +0 -118
  27. devsync-0.11.0/devsync/cli/download.py +0 -274
  28. devsync-0.11.0/devsync/cli/install.py +0 -237
  29. devsync-0.11.0/devsync/cli/install_new.py +0 -937
  30. devsync-0.11.0/devsync/cli/list.py +0 -275
  31. devsync-0.11.0/devsync/cli/main.py +0 -454
  32. devsync-0.11.0/devsync/cli/mcp_configure.py +0 -233
  33. devsync-0.11.0/devsync/cli/mcp_install.py +0 -167
  34. devsync-0.11.0/devsync/cli/mcp_sync.py +0 -166
  35. devsync-0.11.0/devsync/cli/package.py +0 -386
  36. devsync-0.11.0/devsync/cli/package_create.py +0 -323
  37. devsync-0.11.0/devsync/cli/package_install.py +0 -474
  38. devsync-0.11.0/devsync/cli/template.py +0 -19
  39. devsync-0.11.0/devsync/cli/template_backup.py +0 -262
  40. devsync-0.11.0/devsync/cli/template_init.py +0 -499
  41. devsync-0.11.0/devsync/cli/template_install.py +0 -263
  42. devsync-0.11.0/devsync/cli/template_list.py +0 -172
  43. devsync-0.11.0/devsync/cli/template_uninstall.py +0 -146
  44. devsync-0.11.0/devsync/cli/template_update.py +0 -225
  45. devsync-0.11.0/devsync/cli/template_validate.py +0 -234
  46. devsync-0.11.0/devsync/cli/update.py +0 -309
  47. devsync-0.11.0/devsync/core/template_manifest.py +0 -283
  48. devsync-0.11.0/devsync/storage/library.py +0 -429
  49. devsync-0.11.0/devsync/storage/template_library.py +0 -231
  50. devsync-0.11.0/devsync/storage/template_tracker.py +0 -297
  51. devsync-0.11.0/devsync/tui/__init__.py +0 -5
  52. devsync-0.11.0/devsync/tui/installer.py +0 -511
  53. {devsync-0.11.0 → devsync-0.12.0}/LICENSE +0 -0
  54. {devsync-0.11.0 → devsync-0.12.0}/devsync/__init__.py +0 -0
  55. {devsync-0.11.0 → devsync-0.12.0}/devsync/__main__.py +0 -0
  56. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/__init__.py +0 -0
  57. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/aider.py +0 -0
  58. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/amazonq.py +0 -0
  59. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/amp.py +0 -0
  60. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/anteroom.py +0 -0
  61. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/antigravity.py +0 -0
  62. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/augment.py +0 -0
  63. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/base.py +0 -0
  64. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/capability_registry.py +0 -0
  65. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/claude.py +0 -0
  66. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/claude_desktop.py +0 -0
  67. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/cline.py +0 -0
  68. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/codex.py +0 -0
  69. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/continuedev.py +0 -0
  70. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/copilot.py +0 -0
  71. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/cursor.py +0 -0
  72. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/detector.py +0 -0
  73. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/gemini.py +0 -0
  74. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/jetbrains.py +0 -0
  75. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/junie.py +0 -0
  76. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/kiro.py +0 -0
  77. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/mcp_syncer.py +0 -0
  78. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/opencode.py +0 -0
  79. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/openhands.py +0 -0
  80. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/roo.py +0 -0
  81. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/tabnine.py +0 -0
  82. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/trae.py +0 -0
  83. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/translator.py +0 -0
  84. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/winsurf.py +0 -0
  85. {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/zed.py +0 -0
  86. {devsync-0.11.0 → devsync-0.12.0}/devsync/cli/__init__.py +0 -0
  87. {devsync-0.11.0 → devsync-0.12.0}/devsync/cli/tools.py +0 -0
  88. {devsync-0.11.0 → devsync-0.12.0}/devsync/cli/uninstall.py +0 -0
  89. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/__init__.py +0 -0
  90. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/checksum.py +0 -0
  91. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/component_detector.py +0 -0
  92. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/conflict_resolution.py +0 -0
  93. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/git_operations.py +0 -0
  94. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/__init__.py +0 -0
  95. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/credentials.py +0 -0
  96. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/manager.py +0 -0
  97. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/set_manager.py +0 -0
  98. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/validator.py +0 -0
  99. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/models.py +0 -0
  100. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/package_creator.py +0 -0
  101. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/package_manifest.py +0 -0
  102. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/repository.py +0 -0
  103. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/secret_detector.py +0 -0
  104. {devsync-0.11.0 → devsync-0.12.0}/devsync/core/version.py +0 -0
  105. {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/__init__.py +0 -0
  106. {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/mcp_tracker.py +0 -0
  107. {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/package_tracker.py +0 -0
  108. {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/tracker.py +0 -0
  109. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/__init__.py +0 -0
  110. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/atomic_write.py +0 -0
  111. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/backup.py +0 -0
  112. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/dotenv.py +0 -0
  113. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/git_helpers.py +0 -0
  114. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/logging.py +0 -0
  115. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/namespace.py +0 -0
  116. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/paths.py +0 -0
  117. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/project.py +0 -0
  118. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/streaming.py +0 -0
  119. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/ui.py +0 -0
  120. {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/validation.py +0 -0
  121. {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/dependency_links.txt +0 -0
  122. {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/entry_points.txt +0 -0
  123. {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/top_level.txt +0 -0
  124. {devsync-0.11.0 → devsync-0.12.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devsync
3
- Version: 0.11.0
3
+ Version: 0.12.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
@@ -25,6 +25,7 @@ Requires-Dist: pyyaml>=6.0
25
25
  Requires-Dist: textual>=6.0.0
26
26
  Requires-Dist: GitPython>=3.1.45
27
27
  Requires-Dist: python-dotenv>=1.0.0
28
+ Requires-Dist: httpx>=0.27.0
28
29
  Provides-Extra: dev
29
30
  Requires-Dist: pytest>=8.0.0; extra == "dev"
30
31
  Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
@@ -40,7 +41,7 @@ Requires-Dist: types-PyYAML>=6.0.12.20240808; extra == "dev"
40
41
 
41
42
  # DevSync
42
43
 
43
- **Distribute and sync AI coding assistant configurations across your team**
44
+ **AI-powered config distribution for AI coding assistants**
44
45
 
45
46
  [![CI](https://github.com/troylar/devsync/actions/workflows/ci.yml/badge.svg)](https://github.com/troylar/devsync/actions/workflows/ci.yml)
46
47
  [![Docs](https://readthedocs.org/projects/devsync/badge/?version=latest)](https://devsync.readthedocs.io)
@@ -55,31 +56,58 @@ Requires-Dist: types-PyYAML>=6.0.12.20240808; extra == "dev"
55
56
 
56
57
  ---
57
58
 
58
- DevSync is a CLI tool for managing AI coding assistant instructions, MCP servers, and configuration packages across 22+ IDEs. Download shared configs from Git repos, install them to any tool, and keep your team aligned.
59
+ DevSync uses LLM intelligence to extract coding practices from projects and adapt them to recipients' existing setups -- across 23+ AI coding assistants. Two commands: `extract` and `install`.
59
60
 
60
61
  ## Quick Start
61
62
 
62
63
  ```bash
63
64
  pip install devsync
64
65
 
66
+ # One-time: configure your LLM provider
67
+ devsync setup
68
+
65
69
  # Check detected AI tools
66
70
  devsync tools
67
71
 
68
- # Download instructions from a Git repo
69
- devsync download --from github.com/company/standards --as company
72
+ # Extract practices from a project
73
+ devsync extract
74
+
75
+ # Install a package into another project
76
+ devsync install ./team-standards
70
77
 
71
- # Install interactively
72
- devsync install
78
+ # Install from Git
79
+ devsync install https://github.com/company/standards
73
80
  ```
74
81
 
82
+ No API key? DevSync works without one -- it falls back to file-copy mode. Add `--no-ai` to any command to force this.
83
+
75
84
  ## Features
76
85
 
77
- - **Instructions** -- share coding standards, style guides, and AI prompts from Git repos
78
- - **MCP Servers** -- distribute Model Context Protocol configs with secure credential management
79
- - **Packages** -- bundle instructions, MCP servers, hooks, commands, and resources together
80
- - **23 IDE integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, and 19 more
81
- - **Templates** -- IDE-targeted content with slash commands, hooks, and backups
82
- - **Conflict resolution** -- skip, overwrite, or rename when files already exist
86
+ - **AI-powered extraction** -- LLM reads your project's rules, MCP configs, and commands to produce abstract practice declarations
87
+ - **AI-powered installation** -- LLM adapts incoming practices to your existing setup with intelligent merging
88
+ - **23+ AI tool integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, Kiro, Roo Code, Cline, Codex, and more
89
+ - **MCP credential handling** -- prompts for credentials at install time, never stores them in repos
90
+ - **v1 backward compatibility** -- old `ai-config-kit-package.yaml` packages still install via file-copy
91
+ - **Graceful degradation** -- works without an API key, `--no-ai` flag for explicit file-copy mode
92
+
93
+ ## Commands
94
+
95
+ | Command | Description |
96
+ |---------|-------------|
97
+ | `devsync setup` | Configure LLM provider (Anthropic, OpenAI, OpenRouter) |
98
+ | `devsync tools` | Detect installed AI coding tools |
99
+ | `devsync extract` | Extract practices from current project into a shareable package |
100
+ | `devsync install <source>` | Install a package with AI-powered adaptation |
101
+ | `devsync list` | Show installed packages |
102
+ | `devsync uninstall <name>` | Remove an installed package |
103
+
104
+ ## Migrating from v1
105
+
106
+ If you have v1 packages (`ai-config-kit-package.yaml`), they still work with `devsync install`. To upgrade them to v2 format:
107
+
108
+ ```bash
109
+ devsync extract --upgrade ./old-package
110
+ ```
83
111
 
84
112
  ## Documentation
85
113
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # DevSync
4
4
 
5
- **Distribute and sync AI coding assistant configurations across your team**
5
+ **AI-powered config distribution for AI coding assistants**
6
6
 
7
7
  [![CI](https://github.com/troylar/devsync/actions/workflows/ci.yml/badge.svg)](https://github.com/troylar/devsync/actions/workflows/ci.yml)
8
8
  [![Docs](https://readthedocs.org/projects/devsync/badge/?version=latest)](https://devsync.readthedocs.io)
@@ -17,31 +17,58 @@
17
17
 
18
18
  ---
19
19
 
20
- DevSync is a CLI tool for managing AI coding assistant instructions, MCP servers, and configuration packages across 22+ IDEs. Download shared configs from Git repos, install them to any tool, and keep your team aligned.
20
+ DevSync uses LLM intelligence to extract coding practices from projects and adapt them to recipients' existing setups -- across 23+ AI coding assistants. Two commands: `extract` and `install`.
21
21
 
22
22
  ## Quick Start
23
23
 
24
24
  ```bash
25
25
  pip install devsync
26
26
 
27
+ # One-time: configure your LLM provider
28
+ devsync setup
29
+
27
30
  # Check detected AI tools
28
31
  devsync tools
29
32
 
30
- # Download instructions from a Git repo
31
- devsync download --from github.com/company/standards --as company
33
+ # Extract practices from a project
34
+ devsync extract
35
+
36
+ # Install a package into another project
37
+ devsync install ./team-standards
32
38
 
33
- # Install interactively
34
- devsync install
39
+ # Install from Git
40
+ devsync install https://github.com/company/standards
35
41
  ```
36
42
 
43
+ No API key? DevSync works without one -- it falls back to file-copy mode. Add `--no-ai` to any command to force this.
44
+
37
45
  ## Features
38
46
 
39
- - **Instructions** -- share coding standards, style guides, and AI prompts from Git repos
40
- - **MCP Servers** -- distribute Model Context Protocol configs with secure credential management
41
- - **Packages** -- bundle instructions, MCP servers, hooks, commands, and resources together
42
- - **23 IDE integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, and 19 more
43
- - **Templates** -- IDE-targeted content with slash commands, hooks, and backups
44
- - **Conflict resolution** -- skip, overwrite, or rename when files already exist
47
+ - **AI-powered extraction** -- LLM reads your project's rules, MCP configs, and commands to produce abstract practice declarations
48
+ - **AI-powered installation** -- LLM adapts incoming practices to your existing setup with intelligent merging
49
+ - **23+ AI tool integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, Kiro, Roo Code, Cline, Codex, and more
50
+ - **MCP credential handling** -- prompts for credentials at install time, never stores them in repos
51
+ - **v1 backward compatibility** -- old `ai-config-kit-package.yaml` packages still install via file-copy
52
+ - **Graceful degradation** -- works without an API key, `--no-ai` flag for explicit file-copy mode
53
+
54
+ ## Commands
55
+
56
+ | Command | Description |
57
+ |---------|-------------|
58
+ | `devsync setup` | Configure LLM provider (Anthropic, OpenAI, OpenRouter) |
59
+ | `devsync tools` | Detect installed AI coding tools |
60
+ | `devsync extract` | Extract practices from current project into a shareable package |
61
+ | `devsync install <source>` | Install a package with AI-powered adaptation |
62
+ | `devsync list` | Show installed packages |
63
+ | `devsync uninstall <name>` | Remove an installed package |
64
+
65
+ ## Migrating from v1
66
+
67
+ If you have v1 packages (`ai-config-kit-package.yaml`), they still work with `devsync install`. To upgrade them to v2 format:
68
+
69
+ ```bash
70
+ devsync extract --upgrade ./old-package
71
+ ```
45
72
 
46
73
  ## Documentation
47
74
 
@@ -0,0 +1,232 @@
1
+ """Extract command — reads project configs and produces a shareable package."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from rich.console import Console
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn
9
+
10
+ from devsync.core.extractor import PracticeExtractor
11
+ from devsync.core.package_manifest_v2 import PackageManifestV2, detect_manifest_format, parse_manifest
12
+ from devsync.llm.config import load_config
13
+ from devsync.llm.provider import resolve_provider
14
+
15
+ console = Console()
16
+
17
+
18
+ def extract_command(
19
+ output: Optional[str] = None,
20
+ name: Optional[str] = None,
21
+ no_ai: bool = False,
22
+ project_dir: Optional[str] = None,
23
+ upgrade: Optional[str] = None,
24
+ ) -> int:
25
+ """Extract practices from the current project into a shareable package.
26
+
27
+ Args:
28
+ output: Output directory for the package. Defaults to './devsync-package/'.
29
+ name: Package name. Defaults to project directory name.
30
+ no_ai: Force file-copy mode (no LLM calls).
31
+ project_dir: Project directory to extract from. Defaults to cwd.
32
+ upgrade: Path to a v1 package to convert to v2 format.
33
+
34
+ Returns:
35
+ Exit code (0 = success).
36
+ """
37
+ if upgrade:
38
+ return _upgrade_v1_package(upgrade, output=output, name=name, no_ai=no_ai)
39
+
40
+ project_path = Path(project_dir) if project_dir else Path.cwd()
41
+ if not project_path.is_dir():
42
+ console.print(f"[red]Not a directory: {project_path}[/red]")
43
+ return 1
44
+
45
+ package_name = name or project_path.name
46
+ output_path = Path(output) if output else project_path / "devsync-package"
47
+
48
+ llm = None
49
+ if not no_ai:
50
+ config = load_config()
51
+ llm = resolve_provider(
52
+ preferred_provider=config.provider,
53
+ preferred_model=config.model,
54
+ )
55
+ if not llm:
56
+ console.print("[yellow]No LLM API key found. Using file-copy mode.[/yellow]")
57
+ console.print("Run [cyan]devsync setup[/cyan] to configure AI features.\n")
58
+
59
+ extractor = PracticeExtractor(llm_provider=llm)
60
+
61
+ with Progress(
62
+ SpinnerColumn(),
63
+ TextColumn("[progress.description]{task.description}"),
64
+ console=console,
65
+ ) as progress:
66
+ task = progress.add_task("Scanning project...", total=None)
67
+ result = extractor.extract(project_path)
68
+ progress.update(task, description="Building package...")
69
+
70
+ output_path.mkdir(parents=True, exist_ok=True)
71
+
72
+ manifest = PackageManifestV2(
73
+ format_version="2.0",
74
+ name=package_name,
75
+ version="1.0.0",
76
+ description=f"Extracted from {project_path.name}",
77
+ practices=result.practices,
78
+ mcp_servers=result.mcp_servers,
79
+ )
80
+
81
+ if not result.ai_powered:
82
+ _copy_source_files(project_path, output_path, result.source_files)
83
+ components: dict = {}
84
+ if result.source_files:
85
+ from devsync.core.package_manifest_v2 import ComponentRef
86
+
87
+ components["instructions"] = [
88
+ ComponentRef(
89
+ name=Path(f).stem,
90
+ file=f"instructions/{Path(f).name}",
91
+ )
92
+ for f in result.source_files
93
+ ]
94
+ manifest.components = components
95
+
96
+ manifest_path = output_path / "devsync-package.yaml"
97
+ manifest_path.write_text(manifest.to_yaml())
98
+
99
+ mode = "[green]AI-powered[/green]" if result.ai_powered else "[yellow]file-copy[/yellow]"
100
+ console.print(f"\nExtracted ({mode}):")
101
+ console.print(f" Practices: {len(result.practices)}")
102
+ console.print(f" MCP servers: {len(result.mcp_servers)}")
103
+ console.print(f" Source files: {len(result.source_files)}")
104
+ console.print(f"\nPackage written to: [cyan]{output_path}[/cyan]")
105
+ return 0
106
+
107
+
108
+ def _upgrade_v1_package(
109
+ v1_path: str,
110
+ output: Optional[str] = None,
111
+ name: Optional[str] = None,
112
+ no_ai: bool = False,
113
+ ) -> int:
114
+ """Convert a v1 package to v2 format.
115
+
116
+ Reads the v1 manifest, extracts instruction files, and produces a v2
117
+ package with practice declarations (AI-powered) or literal content (no-AI).
118
+
119
+ Args:
120
+ v1_path: Path to the v1 package directory.
121
+ output: Output directory for the v2 package.
122
+ name: Package name override.
123
+ no_ai: Disable AI-powered conversion.
124
+
125
+ Returns:
126
+ Exit code (0 = success).
127
+ """
128
+ package_path = Path(v1_path).expanduser()
129
+ if not package_path.is_dir():
130
+ console.print(f"[red]Not a directory: {package_path}[/red]")
131
+ return 1
132
+
133
+ fmt = detect_manifest_format(package_path)
134
+ if not fmt:
135
+ console.print(f"[red]No manifest found in {package_path}[/red]")
136
+ console.print("Expected: ai-config-kit-package.yaml or devsync-package.yaml")
137
+ return 1
138
+
139
+ if fmt == "v2":
140
+ console.print("[yellow]Package is already v2 format. No upgrade needed.[/yellow]")
141
+ return 0
142
+
143
+ v1_manifest = parse_manifest(package_path)
144
+ console.print(f"\n[bold]Upgrading v1 package: {v1_manifest.name} v{v1_manifest.version}[/bold]")
145
+
146
+ instruction_files: dict[str, str] = {}
147
+ for comp_type, refs in v1_manifest.components.items():
148
+ if comp_type != "instructions":
149
+ continue
150
+ for ref in refs:
151
+ src_file = (package_path / ref.file).resolve()
152
+ try:
153
+ src_file.relative_to(package_path.resolve())
154
+ except ValueError:
155
+ console.print(f" [red]Rejected (path traversal): {ref.file}[/red]")
156
+ continue
157
+ if src_file.exists() and src_file.stat().st_size < 100_000:
158
+ try:
159
+ content = src_file.read_text(encoding="utf-8")
160
+ instruction_files[ref.file] = content
161
+ except (OSError, UnicodeDecodeError):
162
+ console.print(f" [yellow]Could not read: {ref.file}[/yellow]")
163
+
164
+ if not instruction_files:
165
+ console.print("[red]No instruction files found in v1 package.[/red]")
166
+ return 1
167
+
168
+ llm = None
169
+ if not no_ai:
170
+ config = load_config()
171
+ llm = resolve_provider(preferred_provider=config.provider, preferred_model=config.model)
172
+ if not llm:
173
+ console.print("[yellow]No LLM API key found. Using file-copy mode.[/yellow]")
174
+
175
+ extractor = PracticeExtractor(llm_provider=llm)
176
+
177
+ with Progress(
178
+ SpinnerColumn(),
179
+ TextColumn("[progress.description]{task.description}"),
180
+ console=console,
181
+ ) as progress:
182
+ task = progress.add_task("Converting to v2...", total=None)
183
+ if llm:
184
+ result = extractor._extract_with_ai(instruction_files, [])
185
+ else:
186
+ result = extractor._extract_without_ai(instruction_files, [])
187
+ progress.update(task, description="Building v2 package...")
188
+
189
+ output_path = Path(output) if output else package_path.parent / f"{package_path.name}-v2"
190
+ output_path.mkdir(parents=True, exist_ok=True)
191
+
192
+ package_name = name or v1_manifest.name
193
+
194
+ v2_manifest = PackageManifestV2(
195
+ format_version="2.0",
196
+ name=package_name,
197
+ version=v1_manifest.version,
198
+ description=v1_manifest.description or f"Upgraded from v1: {v1_manifest.name}",
199
+ practices=result.practices,
200
+ mcp_servers=result.mcp_servers,
201
+ )
202
+
203
+ if not result.ai_powered:
204
+ _copy_source_files(package_path, output_path, list(instruction_files.keys()))
205
+ from devsync.core.package_manifest_v2 import ComponentRef
206
+
207
+ v2_manifest.components = {
208
+ "instructions": [
209
+ ComponentRef(name=Path(f).stem, file=f"instructions/{Path(f).name}") for f in instruction_files
210
+ ]
211
+ }
212
+
213
+ manifest_path = output_path / "devsync-package.yaml"
214
+ manifest_path.write_text(v2_manifest.to_yaml())
215
+
216
+ mode = "[green]AI-powered[/green]" if result.ai_powered else "[yellow]file-copy[/yellow]"
217
+ console.print(f"\nUpgraded ({mode}):")
218
+ console.print(f" Practices: {len(result.practices)}")
219
+ console.print(f" Source files: {len(instruction_files)}")
220
+ console.print(f"\nv2 package written to: [cyan]{output_path}[/cyan]")
221
+ return 0
222
+
223
+
224
+ def _copy_source_files(project_path: Path, output_path: Path, source_files: list[str]) -> None:
225
+ """Copy source instruction files to the output package directory."""
226
+ instructions_dir = output_path / "instructions"
227
+ instructions_dir.mkdir(parents=True, exist_ok=True)
228
+ for rel_path in source_files:
229
+ src = project_path / rel_path
230
+ if src.exists():
231
+ dest = instructions_dir / Path(rel_path).name
232
+ shutil.copy2(str(src), str(dest))
@@ -0,0 +1,274 @@
1
+ """V2 install command — AI-powered package installation."""
2
+
3
+ import shutil
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from rich.console import Console
9
+ from rich.prompt import Confirm
10
+ from rich.table import Table
11
+
12
+ from devsync.core.adapter import PracticeAdapter
13
+ from devsync.core.mcp_credential_prompter import build_mcp_config, prompt_mcp_credentials
14
+ from devsync.core.package_manifest_v2 import PackageManifestV2, detect_manifest_format, parse_manifest
15
+ from devsync.llm.config import load_config
16
+ from devsync.llm.provider import resolve_provider
17
+ from devsync.llm.response_models import AdaptationPlan
18
+ from devsync.utils.project import find_project_root
19
+
20
+ console = Console()
21
+
22
+
23
+ def install_v2_command(
24
+ source: str,
25
+ tool: Optional[list[str]] = None,
26
+ no_ai: bool = False,
27
+ conflict: str = "prompt",
28
+ project_dir: Optional[str] = None,
29
+ ) -> int:
30
+ """Install a package into the current project.
31
+
32
+ Accepts Git URLs, local paths, or package directories.
33
+
34
+ Args:
35
+ source: Package source (Git URL, local path, or directory).
36
+ tool: Target AI tool(s). Auto-detects if not specified.
37
+ no_ai: Disable AI-powered adaptation.
38
+ conflict: Conflict strategy ('prompt', 'skip', 'overwrite', 'rename').
39
+ project_dir: Target project directory. Defaults to cwd.
40
+
41
+ Returns:
42
+ Exit code (0 = success).
43
+ """
44
+ project_path = Path(project_dir) if project_dir else Path.cwd()
45
+ project_root = find_project_root(project_path)
46
+ if not project_root:
47
+ project_root = project_path
48
+
49
+ cloned_tmp: Path | None = None
50
+ package_path = _resolve_source(source)
51
+ if not package_path:
52
+ console.print(f"[red]Could not resolve source: {source}[/red]")
53
+ return 1
54
+
55
+ # Track if we cloned a temp directory so we can clean it up
56
+ if source.startswith(("http://", "https://", "git@", "github.com")):
57
+ cloned_tmp = package_path
58
+
59
+ try:
60
+ fmt = detect_manifest_format(package_path)
61
+ if not fmt:
62
+ console.print(f"[red]No manifest found in {package_path}[/red]")
63
+ console.print("Expected: devsync-package.yaml or ai-config-kit-package.yaml")
64
+ return 1
65
+
66
+ manifest = parse_manifest(package_path)
67
+ target_tools = _resolve_tools(tool)
68
+
69
+ console.print(f"\n[bold]Installing: {manifest.name} v{manifest.version}[/bold]")
70
+ console.print(f" {manifest.description}")
71
+ console.print(f" Format: {'v2 (AI-native)' if manifest.is_v2 else 'v1 (file-copy)'}")
72
+ console.print(f" Tools: {', '.join(target_tools)}")
73
+
74
+ 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)
77
+ finally:
78
+ if cloned_tmp and cloned_tmp.exists():
79
+ shutil.rmtree(cloned_tmp, ignore_errors=True)
80
+
81
+
82
+ def _resolve_source(source: str) -> Optional[Path]:
83
+ """Resolve a source string to a local package directory."""
84
+ source_path = Path(source).expanduser()
85
+ if source_path.is_dir():
86
+ return source_path
87
+
88
+ if source.startswith(("http://", "https://", "git@", "github.com")):
89
+ return _clone_source(source)
90
+
91
+ if source_path.exists():
92
+ return source_path
93
+
94
+ return None
95
+
96
+
97
+ def _clone_source(url: str) -> Optional[Path]:
98
+ """Clone a Git repository to a temp directory."""
99
+ try:
100
+ from devsync.core.git_operations import GitOperations
101
+
102
+ tmp_dir = Path(tempfile.mkdtemp(prefix="devsync-"))
103
+ GitOperations.clone_repository(url, tmp_dir)
104
+ return tmp_dir
105
+ except Exception as e:
106
+ console.print(f"[red]Failed to clone {url}: {e}[/red]")
107
+ return None
108
+
109
+
110
+ def _resolve_tools(tool_names: Optional[list[str]]) -> list[str]:
111
+ """Resolve target tools (auto-detect if not specified)."""
112
+ if tool_names:
113
+ return tool_names
114
+
115
+ from devsync.ai_tools.detector import get_detector
116
+
117
+ detected = get_detector().detect_installed_tools()
118
+ if detected:
119
+ return [t.tool_type.value for t in detected]
120
+
121
+ return ["claude"]
122
+
123
+
124
+ def _install_v2_ai(
125
+ manifest: PackageManifestV2,
126
+ project_root: Path,
127
+ target_tools: list[str],
128
+ ) -> int:
129
+ """Install using AI-powered adaptation."""
130
+ config = load_config()
131
+ llm = resolve_provider(preferred_provider=config.provider, preferred_model=config.model)
132
+
133
+ adapter = PracticeAdapter(llm_provider=llm)
134
+ plan = adapter.adapt(manifest.practices, project_root, target_tools)
135
+
136
+ _display_plan(plan)
137
+
138
+ if not Confirm.ask("\nProceed with installation?", default=True):
139
+ console.print("[yellow]Installation cancelled.[/yellow]")
140
+ return 0
141
+
142
+ _execute_plan(plan, project_root, target_tools)
143
+
144
+ if manifest.mcp_servers:
145
+ _install_mcp_servers(manifest, project_root)
146
+
147
+ console.print(f"\n[green]Installed {manifest.name} successfully.[/green]")
148
+ return 0
149
+
150
+
151
+ def _install_v2_fallback(
152
+ manifest: PackageManifestV2,
153
+ package_path: Path,
154
+ project_root: Path,
155
+ target_tools: list[str],
156
+ conflict: str,
157
+ ) -> int:
158
+ """Install using file-copy mode (v1 compat or --no-ai)."""
159
+ installed_count = 0
160
+
161
+ for component_type, refs in manifest.components.items():
162
+ if component_type != "instructions":
163
+ continue
164
+ for ref in refs:
165
+ src_file = (package_path / ref.file).resolve()
166
+ try:
167
+ src_file.relative_to(package_path.resolve())
168
+ except ValueError:
169
+ console.print(f" [red]Rejected (path traversal): {ref.file}[/red]")
170
+ continue
171
+ if not src_file.exists():
172
+ console.print(f" [yellow]Missing: {ref.file}[/yellow]")
173
+ continue
174
+
175
+ content = src_file.read_text(encoding="utf-8")
176
+ for tool_name in target_tools:
177
+ dest = _get_tool_instruction_path(tool_name, project_root, ref.name)
178
+ if not dest:
179
+ continue
180
+ if dest.exists():
181
+ if conflict == "skip":
182
+ console.print(f" [dim]Skipped (exists): {ref.name} → {dest.relative_to(project_root)}[/dim]")
183
+ continue
184
+ elif conflict == "overwrite":
185
+ dest.write_text(content, encoding="utf-8")
186
+ installed_count += 1
187
+ console.print(f" Overwritten: {ref.name} → {dest.relative_to(project_root)}")
188
+ elif conflict == "rename":
189
+ suffix = 1
190
+ renamed = dest.with_stem(f"{dest.stem}-{suffix}")
191
+ while renamed.exists():
192
+ suffix += 1
193
+ renamed = dest.with_stem(f"{dest.stem}-{suffix}")
194
+ renamed.parent.mkdir(parents=True, exist_ok=True)
195
+ renamed.write_text(content, encoding="utf-8")
196
+ installed_count += 1
197
+ console.print(f" Installed (renamed): {ref.name} → {renamed.relative_to(project_root)}")
198
+ else:
199
+ rel = dest.relative_to(project_root)
200
+ console.print(f" [yellow]Exists: {ref.name} → {rel} (skipped)[/yellow]")
201
+ else:
202
+ dest.parent.mkdir(parents=True, exist_ok=True)
203
+ dest.write_text(content, encoding="utf-8")
204
+ installed_count += 1
205
+ console.print(f" Installed: {ref.name} → {dest.relative_to(project_root)}")
206
+
207
+ if manifest.mcp_servers:
208
+ _install_mcp_servers(manifest, project_root)
209
+
210
+ console.print(f"\n[green]Installed {installed_count} instructions.[/green]")
211
+ return 0
212
+
213
+
214
+ def _display_plan(plan: AdaptationPlan) -> None:
215
+ """Display the adaptation plan for user review."""
216
+ table = Table(title="Adaptation Plan")
217
+ table.add_column("Practice", style="cyan")
218
+ table.add_column("Action", style="bold")
219
+ table.add_column("Reason")
220
+
221
+ for action in plan.actions:
222
+ style = {"install": "green", "merge": "yellow", "skip": "dim"}.get(action.action, "")
223
+ table.add_row(action.practice_name, f"[{style}]{action.action}[/{style}]", action.reason)
224
+
225
+ console.print(table)
226
+ console.print(f"\n Install: {len(plan.installs)} | Merge: {len(plan.merges)} | Skip: {len(plan.skips)}")
227
+
228
+
229
+ def _execute_plan(plan: AdaptationPlan, project_root: Path, target_tools: list[str]) -> None:
230
+ """Execute the adaptation plan — write files to tool-specific directories."""
231
+ for action in plan.actions:
232
+ if action.action == "skip":
233
+ continue
234
+ for tool_name in target_tools:
235
+ dest = _get_tool_instruction_path(tool_name, project_root, action.practice_name)
236
+ if dest:
237
+ dest.parent.mkdir(parents=True, exist_ok=True)
238
+ dest.write_text(action.content, encoding="utf-8")
239
+ console.print(f" Installed: {action.practice_name} → {dest.relative_to(project_root)}")
240
+
241
+
242
+ def _get_tool_instruction_path(tool_name: str, project_root: Path, instruction_name: str) -> Optional[Path]:
243
+ """Get the file path for an instruction in a specific tool."""
244
+ if ".." in instruction_name or "/" in instruction_name or "\\" in instruction_name:
245
+ return None
246
+ tool_paths: dict[str, tuple[str, str]] = {
247
+ "claude": (".claude/rules", ".md"),
248
+ "cursor": (".cursor/rules", ".mdc"),
249
+ "windsurf": (".windsurf/rules", ".md"),
250
+ "copilot": (".github/instructions", ".md"),
251
+ "kiro": (".kiro/steering", ".md"),
252
+ "cline": (".clinerules", ".md"),
253
+ "roo": (".roo/rules", ".md"),
254
+ }
255
+ if tool_name not in tool_paths:
256
+ return None
257
+ dir_name, ext = tool_paths[tool_name]
258
+ return project_root / dir_name / f"{instruction_name}{ext}"
259
+
260
+
261
+ def _install_mcp_servers(manifest: PackageManifestV2, project_root: Path) -> None:
262
+ """Install MCP server configurations with credential prompting."""
263
+ servers_with_creds = [s for s in manifest.mcp_servers if s.credentials]
264
+ if servers_with_creds:
265
+ env_path = project_root / ".devsync" / ".env"
266
+ credentials = prompt_mcp_credentials(servers_with_creds, env_path=env_path)
267
+
268
+ for server in manifest.mcp_servers:
269
+ server_creds = credentials.get(server.name, {})
270
+ build_mcp_config(server, server_creds)
271
+ console.print(f" MCP: {server.name} configured")
272
+ else:
273
+ for server in manifest.mcp_servers:
274
+ console.print(f" MCP: {server.name} (no credentials needed)")