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.
- {devsync-0.11.0 → devsync-0.12.0}/PKG-INFO +41 -13
- {devsync-0.11.0 → devsync-0.12.0}/README.md +39 -12
- devsync-0.12.0/devsync/cli/extract.py +232 -0
- devsync-0.12.0/devsync/cli/install_v2.py +274 -0
- devsync-0.12.0/devsync/cli/list_v2.py +92 -0
- devsync-0.12.0/devsync/cli/main.py +251 -0
- devsync-0.12.0/devsync/cli/setup.py +69 -0
- devsync-0.12.0/devsync/core/adapter.py +181 -0
- devsync-0.12.0/devsync/core/extractor.py +178 -0
- devsync-0.12.0/devsync/core/mcp_credential_prompter.py +129 -0
- devsync-0.12.0/devsync/core/package_manifest_v2.py +240 -0
- devsync-0.12.0/devsync/core/practice.py +174 -0
- devsync-0.12.0/devsync/llm/__init__.py +13 -0
- devsync-0.12.0/devsync/llm/anthropic.py +97 -0
- devsync-0.12.0/devsync/llm/config.py +91 -0
- devsync-0.12.0/devsync/llm/openai_provider.py +98 -0
- devsync-0.12.0/devsync/llm/openrouter.py +100 -0
- devsync-0.12.0/devsync/llm/prompts.py +116 -0
- devsync-0.12.0/devsync/llm/provider.py +137 -0
- devsync-0.12.0/devsync/llm/response_models.py +186 -0
- devsync-0.12.0/devsync/tui/__init__.py +1 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/PKG-INFO +41 -13
- {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/SOURCES.txt +17 -25
- {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/requires.txt +1 -0
- {devsync-0.11.0 → devsync-0.12.0}/pyproject.toml +2 -1
- devsync-0.11.0/devsync/cli/delete.py +0 -118
- devsync-0.11.0/devsync/cli/download.py +0 -274
- devsync-0.11.0/devsync/cli/install.py +0 -237
- devsync-0.11.0/devsync/cli/install_new.py +0 -937
- devsync-0.11.0/devsync/cli/list.py +0 -275
- devsync-0.11.0/devsync/cli/main.py +0 -454
- devsync-0.11.0/devsync/cli/mcp_configure.py +0 -233
- devsync-0.11.0/devsync/cli/mcp_install.py +0 -167
- devsync-0.11.0/devsync/cli/mcp_sync.py +0 -166
- devsync-0.11.0/devsync/cli/package.py +0 -386
- devsync-0.11.0/devsync/cli/package_create.py +0 -323
- devsync-0.11.0/devsync/cli/package_install.py +0 -474
- devsync-0.11.0/devsync/cli/template.py +0 -19
- devsync-0.11.0/devsync/cli/template_backup.py +0 -262
- devsync-0.11.0/devsync/cli/template_init.py +0 -499
- devsync-0.11.0/devsync/cli/template_install.py +0 -263
- devsync-0.11.0/devsync/cli/template_list.py +0 -172
- devsync-0.11.0/devsync/cli/template_uninstall.py +0 -146
- devsync-0.11.0/devsync/cli/template_update.py +0 -225
- devsync-0.11.0/devsync/cli/template_validate.py +0 -234
- devsync-0.11.0/devsync/cli/update.py +0 -309
- devsync-0.11.0/devsync/core/template_manifest.py +0 -283
- devsync-0.11.0/devsync/storage/library.py +0 -429
- devsync-0.11.0/devsync/storage/template_library.py +0 -231
- devsync-0.11.0/devsync/storage/template_tracker.py +0 -297
- devsync-0.11.0/devsync/tui/__init__.py +0 -5
- devsync-0.11.0/devsync/tui/installer.py +0 -511
- {devsync-0.11.0 → devsync-0.12.0}/LICENSE +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/__init__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/__main__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/__init__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/aider.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/amazonq.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/amp.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/anteroom.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/antigravity.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/augment.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/base.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/capability_registry.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/claude.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/claude_desktop.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/cline.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/codex.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/continuedev.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/copilot.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/cursor.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/detector.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/gemini.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/jetbrains.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/junie.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/kiro.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/mcp_syncer.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/opencode.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/openhands.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/roo.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/tabnine.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/trae.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/translator.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/winsurf.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/ai_tools/zed.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/cli/__init__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/cli/tools.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/cli/uninstall.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/__init__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/checksum.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/component_detector.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/conflict_resolution.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/git_operations.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/__init__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/credentials.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/manager.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/set_manager.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/mcp/validator.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/models.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/package_creator.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/package_manifest.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/repository.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/secret_detector.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/core/version.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/__init__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/mcp_tracker.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/package_tracker.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/storage/tracker.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/__init__.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/atomic_write.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/backup.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/dotenv.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/git_helpers.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/logging.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/namespace.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/paths.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/project.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/streaming.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/ui.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync/utils/validation.py +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/dependency_links.txt +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/entry_points.txt +0 -0
- {devsync-0.11.0 → devsync-0.12.0}/devsync.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
**
|
|
44
|
+
**AI-powered config distribution for AI coding assistants**
|
|
44
45
|
|
|
45
46
|
[](https://github.com/troylar/devsync/actions/workflows/ci.yml)
|
|
46
47
|
[](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
|
|
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
|
-
#
|
|
69
|
-
devsync
|
|
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
|
|
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
|
-
- **
|
|
78
|
-
- **
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
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
|
-
**
|
|
5
|
+
**AI-powered config distribution for AI coding assistants**
|
|
6
6
|
|
|
7
7
|
[](https://github.com/troylar/devsync/actions/workflows/ci.yml)
|
|
8
8
|
[](https://devsync.readthedocs.io)
|
|
@@ -17,31 +17,58 @@
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
DevSync
|
|
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
|
-
#
|
|
31
|
-
devsync
|
|
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
|
|
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
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
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)")
|