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.
Files changed (63) hide show
  1. lite_kits/__init__.py +56 -4
  2. lite_kits/cli.py +696 -185
  3. lite_kits/core/__init__.py +6 -0
  4. lite_kits/core/banner.py +1 -1
  5. lite_kits/core/conflict_checker.py +115 -0
  6. lite_kits/core/detector.py +140 -0
  7. lite_kits/core/installer.py +236 -351
  8. lite_kits/core/manifest.py +146 -146
  9. lite_kits/core/validator.py +146 -0
  10. lite_kits/kits/README.md +6 -6
  11. lite_kits/kits/dev/README.md +241 -241
  12. lite_kits/kits/dev/{claude/commands → commands/.claude}/audit.md +143 -143
  13. lite_kits/kits/dev/{claude/commands → commands/.claude}/cleanup.md +2 -2
  14. lite_kits/kits/{git/claude/commands → dev/commands/.claude}/commit.md +2 -2
  15. lite_kits/kits/{project/claude/commands → dev/commands/.claude}/orient.md +3 -4
  16. lite_kits/kits/{git/claude/commands → dev/commands/.claude}/pr.md +1 -1
  17. lite_kits/kits/{git/claude/commands → dev/commands/.claude}/review.md +202 -202
  18. lite_kits/kits/{project/claude/commands → dev/commands/.claude}/stats.md +162 -162
  19. lite_kits/kits/{project/github/prompts → dev/commands/.github}/audit.prompt.md +143 -143
  20. lite_kits/kits/{git/github/prompts → dev/commands/.github}/cleanup.prompt.md +2 -2
  21. lite_kits/kits/{git/github/prompts → dev/commands/.github}/commit.prompt.md +2 -2
  22. lite_kits/kits/dev/{github/prompts → commands/.github}/orient.prompt.md +3 -4
  23. lite_kits/kits/{git/github/prompts → dev/commands/.github}/pr.prompt.md +1 -1
  24. lite_kits/kits/{git/github/prompts → dev/commands/.github}/review.prompt.md +202 -202
  25. lite_kits/kits/dev/{github/prompts → commands/.github}/stats.prompt.md +163 -163
  26. lite_kits/kits/kits.yaml +497 -180
  27. lite_kits/kits/multiagent/README.md +6 -6
  28. lite_kits/kits/multiagent/{claude/commands → commands/.claude}/sync.md +331 -331
  29. lite_kits/kits/multiagent/{github/prompts → commands/.github}/sync.prompt.md +73 -69
  30. lite_kits/kits/multiagent/memory/git-worktrees-protocol.md +370 -370
  31. lite_kits/kits/multiagent/memory/parallel-work-protocol.md +536 -536
  32. lite_kits/kits/multiagent/memory/pr-workflow-guide.md +275 -275
  33. lite_kits/kits/multiagent/templates/collaboration-structure/README.md +166 -166
  34. lite_kits/kits/multiagent/templates/decision.md +79 -79
  35. lite_kits/kits/multiagent/templates/handoff.md +95 -95
  36. lite_kits/kits/multiagent/templates/session-log.md +68 -68
  37. lite_kits-0.3.1.dist-info/METADATA +259 -0
  38. lite_kits-0.3.1.dist-info/RECORD +41 -0
  39. {lite_kits-0.1.1.dist-info → lite_kits-0.3.1.dist-info}/licenses/LICENSE +21 -21
  40. lite_kits/kits/dev/claude/commands/commit.md +0 -612
  41. lite_kits/kits/dev/claude/commands/orient.md +0 -146
  42. lite_kits/kits/dev/claude/commands/pr.md +0 -593
  43. lite_kits/kits/dev/claude/commands/review.md +0 -202
  44. lite_kits/kits/dev/claude/commands/stats.md +0 -162
  45. lite_kits/kits/dev/github/prompts/audit.prompt.md +0 -143
  46. lite_kits/kits/dev/github/prompts/cleanup.prompt.md +0 -382
  47. lite_kits/kits/dev/github/prompts/commit.prompt.md +0 -591
  48. lite_kits/kits/dev/github/prompts/pr.prompt.md +0 -603
  49. lite_kits/kits/dev/github/prompts/review.prompt.md +0 -202
  50. lite_kits/kits/git/README.md +0 -365
  51. lite_kits/kits/git/claude/commands/cleanup.md +0 -361
  52. lite_kits/kits/git/scripts/bash/get-git-context.sh +0 -208
  53. lite_kits/kits/git/scripts/powershell/Get-GitContext.ps1 +0 -242
  54. lite_kits/kits/project/README.md +0 -228
  55. lite_kits/kits/project/claude/commands/audit.md +0 -143
  56. lite_kits/kits/project/claude/commands/review.md +0 -112
  57. lite_kits/kits/project/github/prompts/orient.prompt.md +0 -150
  58. lite_kits/kits/project/github/prompts/review.prompt.md +0 -112
  59. lite_kits/kits/project/github/prompts/stats.prompt.md +0 -163
  60. lite_kits-0.1.1.dist-info/METADATA +0 -447
  61. lite_kits-0.1.1.dist-info/RECORD +0 -58
  62. {lite_kits-0.1.1.dist-info → lite_kits-0.3.1.dist-info}/WHEEL +0 -0
  63. {lite_kits-0.1.1.dist-info → lite_kits-0.3.1.dist-info}/entry_points.txt +0 -0
@@ -1,433 +1,307 @@
1
1
  """
2
- Installer logic for spec-kit-multiagent
2
+ Manifest-driven installer for lite-kits.
3
3
 
4
- Handles installation, removal, and validation of multiagent features.
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
- def __init__(self, target_dir: Path, kits: Optional[List[str]] = None):
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 (project, git, multiagent). Defaults to ['project']
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
- self.kits = kits or ['dev'] # Default to dev kit only
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
- valid_kits = {'dev', 'multiagent'}
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
- raise ValueError(f"Invalid kit(s): {invalid}. Valid: {valid_kits}")
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
- Check if target directory is a spec-kit project.
38
-
39
- Returns:
40
- True if directory contains spec-kit markers
41
- """
42
- markers = [
43
- self.target_dir / ".specify",
44
- self.target_dir / ".claude",
45
- self.target_dir / ".github" / "prompts",
46
- ]
47
- return any(marker.exists() for marker in markers)
48
-
49
- def is_multiagent_installed(self) -> bool:
50
- """
51
- Check if multiagent features are already installed.
52
-
53
- Returns:
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
- # Check if any requested kit is already installed
73
- for kit in self.kits:
74
- if any(marker.exists() for marker in markers.get(kit, [])):
75
- return True
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
- return False
104
+ conflicts = self.conflict_checker.check_conflicts(self.kits, agents, shells)
105
+ preview['conflicts'] = conflicts['overwrites']
78
106
 
79
- def preview_installation(self) -> Dict[str, List[str]]:
80
- """
81
- Preview what files will be created/modified.
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
- Returns:
84
- Dictionary with lists of new_files, modified_files, new_directories
85
- """
86
- changes = {
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
- # Check which interface(s) exist
93
- has_claude = (self.target_dir / ".claude").exists()
94
- has_copilot = (self.target_dir / ".github" / "prompts").exists()
95
-
96
- # Project kit files
97
- if 'project' in self.kits:
98
- if has_claude:
99
- changes["new_files"].extend([
100
- ".claude/commands/orient.md",
101
- ".claude/commands/review.md",
102
- ".claude/commands/audit.md",
103
- ".claude/commands/stats.md",
104
- ])
105
- if has_copilot:
106
- changes["new_files"].extend([
107
- ".github/prompts/orient.prompt.md",
108
- ".github/prompts/review.prompt.md",
109
- ".github/prompts/audit.prompt.md",
110
- ".github/prompts/stats.prompt.md",
111
- ])
112
-
113
- # Git kit files
114
- if 'git' in self.kits:
115
- if has_claude:
116
- changes["new_files"].append(".claude/commands/commit.md")
117
- changes["new_files"].append(".claude/commands/pr.md")
118
- changes["new_files"].append(".claude/commands/review.md")
119
- if has_copilot:
120
- changes["new_files"].append(".github/prompts/commit.prompt.md")
121
- changes["new_files"].append(".github/prompts/pr.prompt.md")
122
- changes["new_files"].append(".github/prompts/review.prompt.md")
123
-
124
- # Multiagent kit files
125
- if 'multiagent' in self.kits and (self.target_dir / ".specify").exists():
126
- changes["new_files"].extend([
127
- ".specify/memory/pr-workflow-guide.md",
128
- ".specify/memory/git-worktrees-protocol.md",
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
- # Detect which interfaces are present
149
- has_claude = (self.target_dir / ".claude").exists()
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 has_claude and not has_copilot:
153
- result["error"] = "No supported AI interface found (.claude or .github/prompts)"
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
- # Install project kit
157
- if 'project' in self.kits:
158
- if has_claude:
159
- self._install_file('project/claude/commands/orient.md', '.claude/commands/orient.md')
160
- self._install_file('project/claude/commands/review.md', '.claude/commands/review.md')
161
- self._install_file('project/claude/commands/audit.md', '.claude/commands/audit.md')
162
- self._install_file('project/claude/commands/stats.md', '.claude/commands/stats.md')
163
- result["installed"].append("project-kit (Claude): /orient, /review, /audit, /stats commands")
164
-
165
- if has_copilot:
166
- self._install_file('project/github/prompts/orient.prompt.md', '.github/prompts/orient.prompt.md')
167
- self._install_file('project/github/prompts/review.prompt.md', '.github/prompts/review.prompt.md')
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 validate(self) -> Dict:
221
- """
222
- Validate kit installation.
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
- Returns:
225
- Dictionary with validation results
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
- # Check project-kit files
230
- claude_orient = self.target_dir / ".claude" / "commands" / "orient.md"
231
- copilot_orient = self.target_dir / ".github" / "prompts" / "orient.prompt.md"
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
- project_kit_installed = claude_orient.exists() or copilot_orient.exists()
234
- checks["project_kit"] = {
235
- "passed": project_kit_installed,
236
- "message": "project-kit: /orient command found" if project_kit_installed
237
- else "project-kit not installed - run: lite-kits add --here --kit project",
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
- # Check git-kit files
241
- claude_commit = self.target_dir / ".claude" / "commands" / "commit.md"
242
- claude_pr = self.target_dir / ".claude" / "commands" / "pr.md"
243
- claude_review = self.target_dir / ".claude" / "commands" / "review.md"
244
- claude_cleanup = self.target_dir / ".claude" / "commands" / "cleanup.md"
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
- # Check multiagent-kit files (only if user is checking for them)
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
- multiagent_kit_installed = claude_sync.exists() or pr_guide.exists() or worktree_guide.exists()
259
- checks["multiagent_kit"] = {
260
- "passed": multiagent_kit_installed,
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
- # Only fail validation if NO kits are installed
266
- # If they only installed project+git, don't fail on missing multiagent
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
- return {
270
- "valid": all_passed,
271
- "checks": checks,
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
- # Private installation methods
275
-
276
- def _install_file(self, kit_relative_path: str, target_relative_path: str):
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
- Args:
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
- if not source.exists():
288
- raise FileNotFoundError(f"Kit file not found: {source}")
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
- target.parent.mkdir(parents=True, exist_ok=True)
291
- shutil.copy2(source, target)
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
- # TODO: Implement these methods
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
- # Remove project kit files
343
- if 'project' in self.kits:
344
- removed = []
345
- project_commands = ['orient', 'review', 'audit', 'stats']
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
- if removed:
427
- result["removed"].append(f"multiagent-kit: {', '.join(removed)}")
289
+ all_files = self.manifest.get_kit_files(kit_name, agent=None)
428
290
 
429
- # Note: Preserve collaboration directories (user data)
430
- # Note: Preserve vanilla spec-kit files
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)