lite-kits 0.1.1__py3-none-any.whl → 0.3.1__py3-none-any.whl
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.
- lite_kits/__init__.py +56 -4
- lite_kits/cli.py +696 -185
- lite_kits/core/__init__.py +6 -0
- lite_kits/core/banner.py +1 -1
- lite_kits/core/conflict_checker.py +115 -0
- lite_kits/core/detector.py +140 -0
- lite_kits/core/installer.py +236 -351
- lite_kits/core/manifest.py +146 -146
- lite_kits/core/validator.py +146 -0
- lite_kits/kits/README.md +6 -6
- lite_kits/kits/dev/README.md +241 -241
- lite_kits/kits/dev/{claude/commands → commands/.claude}/audit.md +143 -143
- lite_kits/kits/dev/{claude/commands → commands/.claude}/cleanup.md +2 -2
- lite_kits/kits/{git/claude/commands → dev/commands/.claude}/commit.md +2 -2
- lite_kits/kits/{project/claude/commands → dev/commands/.claude}/orient.md +3 -4
- lite_kits/kits/{git/claude/commands → dev/commands/.claude}/pr.md +1 -1
- lite_kits/kits/{git/claude/commands → dev/commands/.claude}/review.md +202 -202
- lite_kits/kits/{project/claude/commands → dev/commands/.claude}/stats.md +162 -162
- lite_kits/kits/{project/github/prompts → dev/commands/.github}/audit.prompt.md +143 -143
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/cleanup.prompt.md +2 -2
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/commit.prompt.md +2 -2
- lite_kits/kits/dev/{github/prompts → commands/.github}/orient.prompt.md +3 -4
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/pr.prompt.md +1 -1
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/review.prompt.md +202 -202
- lite_kits/kits/dev/{github/prompts → commands/.github}/stats.prompt.md +163 -163
- lite_kits/kits/kits.yaml +497 -180
- lite_kits/kits/multiagent/README.md +6 -6
- lite_kits/kits/multiagent/{claude/commands → commands/.claude}/sync.md +331 -331
- lite_kits/kits/multiagent/{github/prompts → commands/.github}/sync.prompt.md +73 -69
- lite_kits/kits/multiagent/memory/git-worktrees-protocol.md +370 -370
- lite_kits/kits/multiagent/memory/parallel-work-protocol.md +536 -536
- lite_kits/kits/multiagent/memory/pr-workflow-guide.md +275 -275
- lite_kits/kits/multiagent/templates/collaboration-structure/README.md +166 -166
- lite_kits/kits/multiagent/templates/decision.md +79 -79
- lite_kits/kits/multiagent/templates/handoff.md +95 -95
- lite_kits/kits/multiagent/templates/session-log.md +68 -68
- lite_kits-0.3.1.dist-info/METADATA +259 -0
- lite_kits-0.3.1.dist-info/RECORD +41 -0
- {lite_kits-0.1.1.dist-info → lite_kits-0.3.1.dist-info}/licenses/LICENSE +21 -21
- lite_kits/kits/dev/claude/commands/commit.md +0 -612
- lite_kits/kits/dev/claude/commands/orient.md +0 -146
- lite_kits/kits/dev/claude/commands/pr.md +0 -593
- lite_kits/kits/dev/claude/commands/review.md +0 -202
- lite_kits/kits/dev/claude/commands/stats.md +0 -162
- lite_kits/kits/dev/github/prompts/audit.prompt.md +0 -143
- lite_kits/kits/dev/github/prompts/cleanup.prompt.md +0 -382
- lite_kits/kits/dev/github/prompts/commit.prompt.md +0 -591
- lite_kits/kits/dev/github/prompts/pr.prompt.md +0 -603
- lite_kits/kits/dev/github/prompts/review.prompt.md +0 -202
- lite_kits/kits/git/README.md +0 -365
- lite_kits/kits/git/claude/commands/cleanup.md +0 -361
- lite_kits/kits/git/scripts/bash/get-git-context.sh +0 -208
- lite_kits/kits/git/scripts/powershell/Get-GitContext.ps1 +0 -242
- lite_kits/kits/project/README.md +0 -228
- lite_kits/kits/project/claude/commands/audit.md +0 -143
- lite_kits/kits/project/claude/commands/review.md +0 -112
- lite_kits/kits/project/github/prompts/orient.prompt.md +0 -150
- lite_kits/kits/project/github/prompts/review.prompt.md +0 -112
- lite_kits/kits/project/github/prompts/stats.prompt.md +0 -163
- lite_kits-0.1.1.dist-info/METADATA +0 -447
- lite_kits-0.1.1.dist-info/RECORD +0 -58
- {lite_kits-0.1.1.dist-info → lite_kits-0.3.1.dist-info}/WHEEL +0 -0
- {lite_kits-0.1.1.dist-info → lite_kits-0.3.1.dist-info}/entry_points.txt +0 -0
lite_kits/core/installer.py
CHANGED
@@ -1,433 +1,307 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Manifest-driven installer for lite-kits.
|
3
3
|
|
4
|
-
|
4
|
+
Orchestrates detection, validation, and file operations.
|
5
|
+
Delegates to specialized modules for specific tasks.
|
5
6
|
"""
|
6
7
|
|
7
8
|
import shutil
|
8
9
|
from pathlib import Path
|
9
10
|
from typing import Dict, List, Optional
|
10
11
|
|
12
|
+
from .conflict_checker import ConflictChecker
|
13
|
+
from .detector import Detector
|
14
|
+
from .manifest import KitManifest
|
15
|
+
from .validator import Validator
|
11
16
|
|
12
|
-
class Installer:
|
13
|
-
"""Manages installation of multiagent features to spec-kit projects."""
|
14
17
|
|
15
|
-
|
18
|
+
class Installer:
|
19
|
+
"""Main installer orchestrator."""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
target_dir: Path,
|
24
|
+
kits: Optional[List[str]] = None,
|
25
|
+
force: bool = False,
|
26
|
+
agent: Optional[str] = None,
|
27
|
+
shell: Optional[str] = None,
|
28
|
+
):
|
16
29
|
"""
|
17
30
|
Initialize installer.
|
18
31
|
|
19
32
|
Args:
|
20
33
|
target_dir: Target spec-kit project directory
|
21
|
-
kits: List of kits to install (
|
34
|
+
kits: List of kits to install (None = use default from manifest)
|
35
|
+
force: Skip confirmations and overwrite existing files
|
36
|
+
agent: Explicit agent preference (None = auto-detect)
|
37
|
+
shell: Explicit shell preference (None = auto-detect)
|
22
38
|
"""
|
23
39
|
self.target_dir = Path(target_dir).resolve()
|
24
40
|
self.kits_dir = Path(__file__).parent.parent / "kits"
|
25
|
-
|
41
|
+
|
42
|
+
# Load manifest
|
43
|
+
self.manifest = KitManifest(self.kits_dir)
|
44
|
+
|
45
|
+
# Initialize specialized modules
|
46
|
+
self.detector = Detector(self.target_dir, self.manifest)
|
47
|
+
self.validator = Validator(self.target_dir, self.manifest)
|
48
|
+
self.conflict_checker = ConflictChecker(
|
49
|
+
self.target_dir,
|
50
|
+
self.kits_dir,
|
51
|
+
self.manifest
|
52
|
+
)
|
53
|
+
|
54
|
+
# Operational modes
|
55
|
+
self.force = force
|
56
|
+
|
57
|
+
# Preferences
|
58
|
+
self.preferred_agent = agent
|
59
|
+
self.preferred_shell = shell
|
60
|
+
|
61
|
+
# Kits to install
|
62
|
+
self.kits = kits or [self.manifest.get_default_kit()]
|
26
63
|
|
27
64
|
# Validate kit names
|
28
|
-
|
65
|
+
self._validate_kit_names()
|
66
|
+
|
67
|
+
def _validate_kit_names(self):
|
68
|
+
"""Validate kit names against manifest."""
|
69
|
+
valid_kits = set(self.manifest.get_kit_names())
|
29
70
|
invalid = set(self.kits) - valid_kits
|
30
71
|
if invalid:
|
31
|
-
|
32
|
-
|
33
|
-
# No auto-dependencies - all kits are independent
|
72
|
+
valid_list = ', '.join(sorted(valid_kits))
|
73
|
+
raise ValueError(f"Invalid kit(s): {invalid}. Valid: {valid_list}")
|
34
74
|
|
35
75
|
def is_spec_kit_project(self) -> bool:
|
36
|
-
"""
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
True if multiagent features detected
|
55
|
-
"""
|
56
|
-
# Check for kit markers
|
57
|
-
markers = {
|
58
|
-
'project': [
|
59
|
-
self.target_dir / ".claude" / "commands" / "orient.md",
|
60
|
-
self.target_dir / ".github" / "prompts" / "orient.prompt.md",
|
61
|
-
],
|
62
|
-
'git': [
|
63
|
-
self.target_dir / ".claude" / "commands" / "commit.md",
|
64
|
-
self.target_dir / ".github" / "prompts" / "commit.prompt.md",
|
65
|
-
self.target_dir / ".claude" / "commands" / "review.md",
|
66
|
-
],
|
67
|
-
'multiagent': [
|
68
|
-
self.target_dir / ".specify" / "memory" / "pr-workflow-guide.md",
|
69
|
-
],
|
76
|
+
"""Check if target is a spec-kit project."""
|
77
|
+
return self.detector.is_spec_kit_project()
|
78
|
+
|
79
|
+
def is_kit_installed(self, kit_name: str) -> bool:
|
80
|
+
"""Check if kit is installed."""
|
81
|
+
return self.validator.is_kit_installed(kit_name)
|
82
|
+
|
83
|
+
def preview_installation(self) -> Dict:
|
84
|
+
"""Preview installation without making changes."""
|
85
|
+
agents = self.detector.detect_agents(self.preferred_agent)
|
86
|
+
shells = self.detector.detect_shells(self.preferred_shell)
|
87
|
+
|
88
|
+
preview = {
|
89
|
+
"kits": [],
|
90
|
+
"conflicts": [],
|
91
|
+
"warnings": [],
|
92
|
+
"agents": agents,
|
93
|
+
"shells": shells,
|
70
94
|
}
|
71
95
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
96
|
+
if not agents:
|
97
|
+
supported = [
|
98
|
+
name for name, config in self.manifest.manifest.get('agents', {}).items()
|
99
|
+
if config.get('supported', False)
|
100
|
+
]
|
101
|
+
preview['warnings'].append(f"No AI agents detected. Supported: {', '.join(supported)}")
|
102
|
+
return preview
|
76
103
|
|
77
|
-
|
104
|
+
conflicts = self.conflict_checker.check_conflicts(self.kits, agents, shells)
|
105
|
+
preview['conflicts'] = conflicts['overwrites']
|
78
106
|
|
79
|
-
|
80
|
-
|
81
|
-
|
107
|
+
for kit_name in self.kits:
|
108
|
+
kit_preview = self._preview_kit(kit_name, agents, shells)
|
109
|
+
preview['kits'].append(kit_preview)
|
82
110
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
111
|
+
return preview
|
112
|
+
|
113
|
+
def _preview_kit(self, kit_name: str, agents: List[str], shells: List[str]) -> Dict:
|
114
|
+
"""Preview installation for a single kit."""
|
115
|
+
kit_info = self.manifest.get_kit(kit_name)
|
116
|
+
kit_preview = {
|
117
|
+
"name": kit_info['name'],
|
87
118
|
"new_files": [],
|
88
119
|
"modified_files": [],
|
89
120
|
"new_directories": [],
|
90
121
|
}
|
91
122
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
if
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
"
|
128
|
-
|
129
|
-
|
130
|
-
changes["new_directories"].append("specs/*/collaboration/ (created with new features)")
|
131
|
-
|
132
|
-
return changes
|
123
|
+
for agent in agents:
|
124
|
+
files = self.manifest.get_kit_files(kit_name, agent=agent)
|
125
|
+
self._preview_files(files, kit_preview)
|
126
|
+
|
127
|
+
for shell in shells:
|
128
|
+
files = self.manifest.get_kit_files(kit_name, agent=shell)
|
129
|
+
self._preview_files(files, kit_preview)
|
130
|
+
|
131
|
+
# Get other files (not commands\prompts\scripts - those are handled above)
|
132
|
+
all_files = self.manifest.get_kit_files(kit_name, agent=None)
|
133
|
+
for file_info in all_files:
|
134
|
+
if file_info.get('type') in ['command', 'prompt', 'script']:
|
135
|
+
continue # Already handled by agent/shell sections above
|
136
|
+
self._preview_files([file_info], kit_preview)
|
137
|
+
|
138
|
+
return kit_preview
|
139
|
+
|
140
|
+
def _preview_files(self, files: List[Dict], preview: Dict):
|
141
|
+
"""Preview a list of files."""
|
142
|
+
for file_info in files:
|
143
|
+
if file_info.get('status') == 'planned':
|
144
|
+
continue
|
145
|
+
|
146
|
+
# Normalize paths to use backslashes for Windows display
|
147
|
+
target_path = str(file_info['path']).replace("/", "\\")
|
148
|
+
target_full = self.target_dir / file_info['path']
|
149
|
+
|
150
|
+
if target_full.exists():
|
151
|
+
if target_path not in preview["modified_files"]:
|
152
|
+
preview["modified_files"].append(target_path)
|
153
|
+
else:
|
154
|
+
if target_path not in preview["new_files"]:
|
155
|
+
preview["new_files"].append(target_path)
|
156
|
+
|
157
|
+
parent_dir = str(target_full.parent.relative_to(self.target_dir))
|
158
|
+
if parent_dir not in preview["new_directories"]:
|
159
|
+
if not target_full.parent.exists():
|
160
|
+
preview["new_directories"].append(parent_dir)
|
133
161
|
|
134
162
|
def install(self) -> Dict:
|
135
|
-
"""
|
136
|
-
Install multiagent features to target project.
|
137
|
-
|
138
|
-
Returns:
|
139
|
-
Dictionary with success status and installed items
|
140
|
-
"""
|
163
|
+
"""Install kits to target project."""
|
141
164
|
result = {
|
142
165
|
"success": False,
|
143
166
|
"installed": [],
|
167
|
+
"skipped": [],
|
144
168
|
"error": None,
|
145
169
|
}
|
146
170
|
|
147
171
|
try:
|
148
|
-
|
149
|
-
|
150
|
-
has_copilot = (self.target_dir / ".github" / "prompts").exists()
|
172
|
+
agents = self.detector.detect_agents(self.preferred_agent)
|
173
|
+
shells = self.detector.detect_shells(self.preferred_shell)
|
151
174
|
|
152
|
-
if not
|
153
|
-
|
175
|
+
if not agents:
|
176
|
+
supported = [
|
177
|
+
name for name, config in self.manifest.manifest.get('agents', {}).items()
|
178
|
+
if config.get('supported', False)
|
179
|
+
]
|
180
|
+
result["error"] = (
|
181
|
+
f"No supported AI interface found. Supported: {', '.join(supported)}. "
|
182
|
+
r"To enable AI interface support, create a '.claude\' or '.github\prompts\' directory in your project."
|
183
|
+
)
|
154
184
|
return result
|
155
185
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
self._install_file('project/github/prompts/audit.prompt.md', '.github/prompts/audit.prompt.md')
|
169
|
-
self._install_file('project/github/prompts/stats.prompt.md', '.github/prompts/stats.prompt.md')
|
170
|
-
result["installed"].append("project-kit (Copilot): /orient, /review, /audit, /stats commands")
|
171
|
-
|
172
|
-
# Install git kit
|
173
|
-
if 'git' in self.kits:
|
174
|
-
if has_claude:
|
175
|
-
self._install_file('git/claude/commands/commit.md', '.claude/commands/commit.md')
|
176
|
-
self._install_file('git/claude/commands/pr.md', '.claude/commands/pr.md')
|
177
|
-
self._install_file('git/claude/commands/review.md', '.claude/commands/review.md')
|
178
|
-
self._install_file('git/claude/commands/cleanup.md', '.claude/commands/cleanup.md')
|
179
|
-
result["installed"].append("git-kit (Claude): /commit, /pr, /review, /cleanup commands")
|
180
|
-
|
181
|
-
if has_copilot:
|
182
|
-
self._install_file('git/github/prompts/commit.prompt.md', '.github/prompts/commit.prompt.md')
|
183
|
-
self._install_file('git/github/prompts/pr.prompt.md', '.github/prompts/pr.prompt.md')
|
184
|
-
self._install_file('git/github/prompts/review.prompt.md', '.github/prompts/review.prompt.md')
|
185
|
-
self._install_file('git/github/prompts/cleanup.prompt.md', '.github/prompts/cleanup.prompt.md')
|
186
|
-
result["installed"].append("git-kit (Copilot): /commit, /pr, /review, /cleanup commands")
|
187
|
-
|
188
|
-
# Install multiagent kit
|
189
|
-
if 'multiagent' in self.kits and (self.target_dir / ".specify").exists():
|
190
|
-
# Commands
|
191
|
-
if has_claude:
|
192
|
-
self._install_file('multiagent/claude/commands/sync.md', '.claude/commands/sync.md')
|
193
|
-
if has_copilot:
|
194
|
-
self._install_file('multiagent/github/prompts/sync.prompt.md', '.github/prompts/sync.prompt.md')
|
195
|
-
|
196
|
-
# Memory guides
|
197
|
-
self._install_file('multiagent/memory/pr-workflow-guide.md', '.specify/memory/pr-workflow-guide.md')
|
198
|
-
self._install_file('multiagent/memory/git-worktrees-protocol.md', '.specify/memory/git-worktrees-protocol.md')
|
199
|
-
self._install_file('multiagent/memory/parallel-work-protocol.md', '.specify/memory/parallel-work-protocol.md')
|
200
|
-
|
201
|
-
# Templates
|
202
|
-
templates_dir = self.target_dir / ".specify" / "templates"
|
203
|
-
templates_dir.mkdir(parents=True, exist_ok=True)
|
204
|
-
self._install_file('multiagent/templates/session-log.md', '.specify/templates/session-log.md')
|
205
|
-
self._install_file('multiagent/templates/handoff.md', '.specify/templates/handoff.md')
|
206
|
-
self._install_file('multiagent/templates/decision.md', '.specify/templates/decision.md')
|
207
|
-
self._install_file('multiagent/templates/collaboration-structure/README.md', '.specify/templates/collaboration-README.md')
|
208
|
-
|
209
|
-
result["installed"].append("multiagent-kit: /sync command")
|
210
|
-
result["installed"].append("multiagent-kit: Memory guides (PR workflow, git worktrees, parallel work)")
|
211
|
-
result["installed"].append("multiagent-kit: Templates (session-log, handoff, decision, collaboration)")
|
186
|
+
if not self.force:
|
187
|
+
conflicts = self.conflict_checker.check_conflicts(self.kits, agents, shells)
|
188
|
+
|
189
|
+
if conflicts['has_conflicts']:
|
190
|
+
result["conflicts"] = conflicts['overwrites']
|
191
|
+
result["error"] = f"Found {len(conflicts['conflicts'])} file conflicts. Use --force to overwrite."
|
192
|
+
return result
|
193
|
+
|
194
|
+
options = self.manifest.manifest.get('options', {})
|
195
|
+
|
196
|
+
for kit_name in self.kits:
|
197
|
+
self._install_kit(kit_name, agents, shells, options, result)
|
212
198
|
|
213
199
|
result["success"] = True
|
214
200
|
|
201
|
+
if options.get('validate_on_install', True):
|
202
|
+
result["validation"] = self.validator.validate_all()
|
203
|
+
|
215
204
|
except Exception as e:
|
216
205
|
result["error"] = str(e)
|
217
206
|
|
218
207
|
return result
|
219
208
|
|
220
|
-
def
|
221
|
-
"""
|
222
|
-
|
209
|
+
def _install_kit(self, kit_name: str, agents: List[str], shells: List[str], options: Dict, result: Dict):
|
210
|
+
"""Install a single kit."""
|
211
|
+
skip_existing = options.get('skip_existing', True)
|
223
212
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
checks = {}
|
213
|
+
for agent in agents:
|
214
|
+
files = self.manifest.get_kit_files(kit_name, agent=agent)
|
215
|
+
self._install_files(files, skip_existing, result)
|
228
216
|
|
229
|
-
|
230
|
-
|
231
|
-
|
217
|
+
for shell in shells:
|
218
|
+
files = self.manifest.get_kit_files(kit_name, agent=shell)
|
219
|
+
self._install_files(files, skip_existing, result)
|
232
220
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
}
|
221
|
+
all_files = self.manifest.get_kit_files(kit_name, agent=None)
|
222
|
+
for file_info in all_files:
|
223
|
+
if file_info.get('type') in ['command', 'prompt', 'script']:
|
224
|
+
continue
|
225
|
+
self._install_files([file_info], skip_existing, result)
|
239
226
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
git_kit_installed = claude_commit.exists() or claude_pr.exists() or claude_review.exists() or claude_cleanup.exists()
|
247
|
-
checks["git_kit"] = {
|
248
|
-
"passed": git_kit_installed,
|
249
|
-
"message": "git-kit: /commit, /pr, /review, /cleanup commands found" if git_kit_installed
|
250
|
-
else "git-kit not installed - run: lite-kits add --here --kit git",
|
251
|
-
}
|
227
|
+
def _install_files(self, files: List[Dict], skip_existing: bool, result: Dict):
|
228
|
+
"""Install a list of files."""
|
229
|
+
for file_info in files:
|
230
|
+
if file_info.get('status') == 'planned':
|
231
|
+
result["skipped"].append(f"{file_info['path']} (planned)")
|
232
|
+
continue
|
252
233
|
|
253
|
-
|
254
|
-
claude_sync = self.target_dir / ".claude" / "commands" / "sync.md"
|
255
|
-
pr_guide = self.target_dir / ".specify" / "memory" / "pr-workflow-guide.md"
|
256
|
-
worktree_guide = self.target_dir / ".specify" / "memory" / "git-worktrees-protocol.md"
|
234
|
+
target_path = self.target_dir / file_info['path']
|
257
235
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
"message": "multiagent-kit: /sync command and memory guides found" if multiagent_kit_installed
|
262
|
-
else "multiagent-kit not installed - run: lite-kits add --here --kit multiagent",
|
263
|
-
}
|
236
|
+
if skip_existing and target_path.exists() and not self.force:
|
237
|
+
result["skipped"].append(file_info['path'])
|
238
|
+
continue
|
264
239
|
|
265
|
-
|
266
|
-
|
267
|
-
all_passed = checks["project_kit"]["passed"] or checks["git_kit"]["passed"] or checks["multiagent_kit"]["passed"]
|
240
|
+
self._copy_file(file_info['source'], file_info['path'])
|
241
|
+
result["installed"].append(file_info['path'])
|
268
242
|
|
269
|
-
|
270
|
-
|
271
|
-
|
243
|
+
def validate(self) -> Dict:
|
244
|
+
"""Validate all installed kits."""
|
245
|
+
return self.validator.validate_all()
|
246
|
+
|
247
|
+
def preview_removal(self) -> Dict:
|
248
|
+
"""Preview files that would be removed."""
|
249
|
+
preview = {
|
250
|
+
"kits": [],
|
251
|
+
"total_files": 0,
|
272
252
|
}
|
273
253
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
"""
|
278
|
-
Install a file from kits directory to target project.
|
254
|
+
for kit_name in self.kits:
|
255
|
+
kit_info = self.manifest.get_kit(kit_name)
|
256
|
+
files_to_remove = []
|
279
257
|
|
280
|
-
|
281
|
-
kit_relative_path: Path relative to kits/ directory (e.g., 'project/claude/commands/orient.md')
|
282
|
-
target_relative_path: Path relative to target directory (e.g., '.claude/commands/orient.md')
|
283
|
-
"""
|
284
|
-
source = self.kits_dir / kit_relative_path
|
285
|
-
target = self.target_dir / target_relative_path
|
258
|
+
all_files = self.manifest.get_kit_files(kit_name, agent=None)
|
286
259
|
|
287
|
-
|
288
|
-
|
260
|
+
for file_info in all_files:
|
261
|
+
target_path = self.target_dir / file_info['path']
|
262
|
+
if target_path.exists():
|
263
|
+
files_to_remove.append(file_info['path'])
|
289
264
|
|
290
|
-
|
291
|
-
|
265
|
+
if files_to_remove:
|
266
|
+
preview["kits"].append({
|
267
|
+
'name': kit_info['name'],
|
268
|
+
'files': files_to_remove
|
269
|
+
})
|
270
|
+
preview["total_files"] += len(files_to_remove)
|
292
271
|
|
293
|
-
|
294
|
-
|
295
|
-
def _merge_constitution(self):
|
296
|
-
"""
|
297
|
-
Merge multiagent sections into existing constitution.
|
298
|
-
|
299
|
-
Strategy:
|
300
|
-
1. Read existing constitution
|
301
|
-
2. Check for multiagent marker (<!-- MULTIAGENT-START -->)
|
302
|
-
3. If marker exists, replace section
|
303
|
-
4. If marker missing, append section
|
304
|
-
5. Preserve user edits outside markers
|
305
|
-
"""
|
306
|
-
# TODO: Implement smart merge logic
|
307
|
-
# - Read templates/enhancements/constitution-multiagent.md
|
308
|
-
# - Merge into .specify/memory/constitution.md
|
309
|
-
# - Use marker comments for idempotent updates
|
310
|
-
pass
|
311
|
-
|
312
|
-
def _install_collaboration_template(self):
|
313
|
-
"""
|
314
|
-
Create collaboration directory template.
|
315
|
-
|
316
|
-
Creates:
|
317
|
-
- .specify/templates/collaboration-template/
|
318
|
-
- Scripts reference this when creating new features
|
319
|
-
"""
|
320
|
-
# TODO: Create collaboration directory template
|
321
|
-
# - active/
|
322
|
-
# - archive/
|
323
|
-
# - results/
|
324
|
-
pass
|
272
|
+
return preview
|
325
273
|
|
326
274
|
def remove(self) -> Dict:
|
327
|
-
"""
|
328
|
-
Remove multiagent features from project.
|
329
|
-
|
330
|
-
Returns to vanilla spec-kit state.
|
331
|
-
|
332
|
-
Returns:
|
333
|
-
Dictionary with success status and removed items
|
334
|
-
"""
|
275
|
+
"""Remove kits from project."""
|
335
276
|
result = {
|
336
277
|
"success": False,
|
337
278
|
"removed": [],
|
279
|
+
"not_found": [],
|
338
280
|
"error": None,
|
339
281
|
}
|
340
282
|
|
341
283
|
try:
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
# Claude
|
348
|
-
for cmd in project_commands:
|
349
|
-
cmd_file = self.target_dir / ".claude" / "commands" / f"{cmd}.md"
|
350
|
-
if cmd_file.exists():
|
351
|
-
cmd_file.unlink()
|
352
|
-
removed.append(f".claude/commands/{cmd}.md")
|
353
|
-
|
354
|
-
# Copilot
|
355
|
-
for cmd in project_commands:
|
356
|
-
cmd_file = self.target_dir / ".github" / "prompts" / f"{cmd}.prompt.md"
|
357
|
-
if cmd_file.exists():
|
358
|
-
cmd_file.unlink()
|
359
|
-
removed.append(f".github/prompts/{cmd}.prompt.md")
|
360
|
-
|
361
|
-
if removed:
|
362
|
-
result["removed"].append(f"project-kit: {', '.join(removed)}")
|
363
|
-
|
364
|
-
# Remove git kit files
|
365
|
-
if 'git' in self.kits:
|
366
|
-
removed = []
|
367
|
-
git_commands = ['commit', 'pr', 'review', 'cleanup']
|
368
|
-
|
369
|
-
# Claude
|
370
|
-
for cmd in git_commands:
|
371
|
-
cmd_file = self.target_dir / ".claude" / "commands" / f"{cmd}.md"
|
372
|
-
if cmd_file.exists():
|
373
|
-
cmd_file.unlink()
|
374
|
-
removed.append(f".claude/commands/{cmd}.md")
|
375
|
-
|
376
|
-
# Copilot
|
377
|
-
for cmd in git_commands:
|
378
|
-
cmd_file = self.target_dir / ".github" / "prompts" / f"{cmd}.prompt.md"
|
379
|
-
if cmd_file.exists():
|
380
|
-
cmd_file.unlink()
|
381
|
-
removed.append(f".github/prompts/{cmd}.prompt.md")
|
382
|
-
|
383
|
-
if removed:
|
384
|
-
result["removed"].append(f"git-kit: {', '.join(removed)}")
|
385
|
-
|
386
|
-
# Remove multiagent kit files
|
387
|
-
if 'multiagent' in self.kits:
|
388
|
-
removed = []
|
389
|
-
|
390
|
-
# Sync command
|
391
|
-
sync_claude = self.target_dir / ".claude" / "commands" / "sync.md"
|
392
|
-
if sync_claude.exists():
|
393
|
-
sync_claude.unlink()
|
394
|
-
removed.append(".claude/commands/sync.md")
|
395
|
-
|
396
|
-
sync_copilot = self.target_dir / ".github" / "prompts" / "sync.prompt.md"
|
397
|
-
if sync_copilot.exists():
|
398
|
-
sync_copilot.unlink()
|
399
|
-
removed.append(".github/prompts/sync.prompt.md")
|
400
|
-
|
401
|
-
# Memory guides
|
402
|
-
memory_files = [
|
403
|
-
'pr-workflow-guide.md',
|
404
|
-
'git-worktrees-protocol.md',
|
405
|
-
'parallel-work-protocol.md',
|
406
|
-
]
|
407
|
-
for file in memory_files:
|
408
|
-
file_path = self.target_dir / ".specify" / "memory" / file
|
409
|
-
if file_path.exists():
|
410
|
-
file_path.unlink()
|
411
|
-
removed.append(f".specify/memory/{file}")
|
412
|
-
|
413
|
-
# Templates
|
414
|
-
template_files = [
|
415
|
-
'session-log.md',
|
416
|
-
'handoff.md',
|
417
|
-
'decision.md',
|
418
|
-
'collaboration-README.md',
|
419
|
-
]
|
420
|
-
for file in template_files:
|
421
|
-
file_path = self.target_dir / ".specify" / "templates" / file
|
422
|
-
if file_path.exists():
|
423
|
-
file_path.unlink()
|
424
|
-
removed.append(f".specify/templates/{file}")
|
284
|
+
for kit_name in self.kits:
|
285
|
+
kit_info = self.manifest.get_kit(kit_name)
|
286
|
+
removed_files = []
|
287
|
+
not_found_files = []
|
425
288
|
|
426
|
-
|
427
|
-
result["removed"].append(f"multiagent-kit: {', '.join(removed)}")
|
289
|
+
all_files = self.manifest.get_kit_files(kit_name, agent=None)
|
428
290
|
|
429
|
-
|
430
|
-
|
291
|
+
for file_info in all_files:
|
292
|
+
target_path = self.target_dir / file_info['path']
|
293
|
+
|
294
|
+
if target_path.exists():
|
295
|
+
target_path.unlink()
|
296
|
+
removed_files.append(file_info['path'])
|
297
|
+
else:
|
298
|
+
not_found_files.append(file_info['path'])
|
299
|
+
|
300
|
+
if removed_files:
|
301
|
+
result["removed"].append({'kit': kit_info['name'], 'files': removed_files})
|
302
|
+
|
303
|
+
if not_found_files:
|
304
|
+
result["not_found"].extend(not_found_files)
|
431
305
|
|
432
306
|
result["success"] = True
|
433
307
|
|
@@ -435,3 +309,14 @@ class Installer:
|
|
435
309
|
result["error"] = str(e)
|
436
310
|
|
437
311
|
return result
|
312
|
+
|
313
|
+
def _copy_file(self, kit_relative_path: str, target_relative_path: str):
|
314
|
+
"""Copy file from kits/ to target project."""
|
315
|
+
source = self.kits_dir / kit_relative_path
|
316
|
+
target = self.target_dir / target_relative_path
|
317
|
+
|
318
|
+
if not source.exists():
|
319
|
+
raise FileNotFoundError(f"Kit file not found: {source}")
|
320
|
+
|
321
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
322
|
+
shutil.copy2(source, target)
|