lite-kits 0.1.1__py3-none-any.whl → 0.3.2__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 +782 -189
  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 +177 -0
  7. lite_kits/core/installer.py +242 -351
  8. lite_kits/core/manifest.py +146 -146
  9. lite_kits/core/validator.py +183 -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.2.dist-info/METADATA +259 -0
  38. lite_kits-0.3.2.dist-info/RECORD +41 -0
  39. {lite_kits-0.1.1.dist-info → lite_kits-0.3.2.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.2.dist-info}/WHEEL +0 -0
  63. {lite_kits-0.1.1.dist-info → lite_kits-0.3.2.dist-info}/entry_points.txt +0 -0
@@ -1,433 +1,313 @@
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
+ agents: Optional[List[str]] = None,
27
+ shells: Optional[List[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
+ agents: List of explicit agent preferences (None = auto-detect)
37
+ shells: List of explicit shell preferences (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 - validate immediately during init
58
+ self.preferred_agents = agents
59
+ self.preferred_shells = shells
60
+
61
+ # Validate agent/shell preferences early (raises ValueError if invalid)
62
+ if agents:
63
+ self.detector.detect_agents(agents)
64
+ if shells:
65
+ self.detector.detect_shells(shells)
66
+
67
+ # Kits to install
68
+ self.kits = kits or [self.manifest.get_default_kit()]
26
69
 
27
70
  # Validate kit names
28
- valid_kits = {'dev', 'multiagent'}
71
+ self._validate_kit_names()
72
+
73
+ def _validate_kit_names(self):
74
+ """Validate kit names against manifest."""
75
+ valid_kits = set(self.manifest.get_kit_names())
29
76
  invalid = set(self.kits) - valid_kits
30
77
  if invalid:
31
- raise ValueError(f"Invalid kit(s): {invalid}. Valid: {valid_kits}")
32
-
33
- # No auto-dependencies - all kits are independent
78
+ valid_list = ', '.join(sorted(valid_kits))
79
+ raise ValueError(f"Invalid kit(s): {invalid}. Valid: {valid_list}")
34
80
 
35
81
  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
- ],
82
+ """Check if target is a spec-kit project."""
83
+ return self.detector.is_spec_kit_project()
84
+
85
+ def is_kit_installed(self, kit_name: str) -> bool:
86
+ """Check if kit is installed."""
87
+ return self.validator.is_kit_installed(kit_name)
88
+
89
+ def preview_installation(self) -> Dict:
90
+ """Preview installation without making changes."""
91
+ agents = self.detector.detect_agents(self.preferred_agents)
92
+ shells = self.detector.detect_shells(self.preferred_shells)
93
+
94
+ preview = {
95
+ "kits": [],
96
+ "conflicts": [],
97
+ "warnings": [],
98
+ "agents": agents,
99
+ "shells": shells,
70
100
  }
71
101
 
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
102
+ if not agents:
103
+ supported = [
104
+ name for name, config in self.manifest.manifest.get('agents', {}).items()
105
+ if config.get('supported', False)
106
+ ]
107
+ preview['warnings'].append(f"No AI agents detected. Supported: {', '.join(supported)}")
108
+ return preview
76
109
 
77
- return False
110
+ conflicts = self.conflict_checker.check_conflicts(self.kits, agents, shells)
111
+ preview['conflicts'] = conflicts['overwrites']
78
112
 
79
- def preview_installation(self) -> Dict[str, List[str]]:
80
- """
81
- Preview what files will be created/modified.
113
+ for kit_name in self.kits:
114
+ kit_preview = self._preview_kit(kit_name, agents, shells)
115
+ preview['kits'].append(kit_preview)
82
116
 
83
- Returns:
84
- Dictionary with lists of new_files, modified_files, new_directories
85
- """
86
- changes = {
117
+ return preview
118
+
119
+ def _preview_kit(self, kit_name: str, agents: List[str], shells: List[str]) -> Dict:
120
+ """Preview installation for a single kit."""
121
+ kit_info = self.manifest.get_kit(kit_name)
122
+ kit_preview = {
123
+ "name": kit_info['name'],
87
124
  "new_files": [],
88
125
  "modified_files": [],
89
126
  "new_directories": [],
90
127
  }
91
128
 
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
129
+ for agent in agents:
130
+ files = self.manifest.get_kit_files(kit_name, agent=agent)
131
+ self._preview_files(files, kit_preview)
132
+
133
+ for shell in shells:
134
+ files = self.manifest.get_kit_files(kit_name, agent=shell)
135
+ self._preview_files(files, kit_preview)
136
+
137
+ # Get other files (not commands\prompts\scripts - those are handled above)
138
+ all_files = self.manifest.get_kit_files(kit_name, agent=None)
139
+ for file_info in all_files:
140
+ if file_info.get('type') in ['command', 'prompt', 'script']:
141
+ continue # Already handled by agent/shell sections above
142
+ self._preview_files([file_info], kit_preview)
143
+
144
+ return kit_preview
145
+
146
+ def _preview_files(self, files: List[Dict], preview: Dict):
147
+ """Preview a list of files."""
148
+ for file_info in files:
149
+ if file_info.get('status') == 'planned':
150
+ continue
151
+
152
+ # Normalize paths to use backslashes for Windows display
153
+ target_path = str(file_info['path']).replace("/", "\\")
154
+ target_full = self.target_dir / file_info['path']
155
+
156
+ if target_full.exists():
157
+ if target_path not in preview["modified_files"]:
158
+ preview["modified_files"].append(target_path)
159
+ else:
160
+ if target_path not in preview["new_files"]:
161
+ preview["new_files"].append(target_path)
162
+
163
+ parent_dir = str(target_full.parent.relative_to(self.target_dir))
164
+ if parent_dir not in preview["new_directories"]:
165
+ if not target_full.parent.exists():
166
+ preview["new_directories"].append(parent_dir)
133
167
 
134
168
  def install(self) -> Dict:
135
- """
136
- Install multiagent features to target project.
137
-
138
- Returns:
139
- Dictionary with success status and installed items
140
- """
169
+ """Install kits to target project."""
141
170
  result = {
142
171
  "success": False,
143
172
  "installed": [],
173
+ "skipped": [],
144
174
  "error": None,
145
175
  }
146
176
 
147
177
  try:
148
- # Detect which interfaces are present
149
- has_claude = (self.target_dir / ".claude").exists()
150
- has_copilot = (self.target_dir / ".github" / "prompts").exists()
178
+ agents = self.detector.detect_agents(self.preferred_agents)
179
+ shells = self.detector.detect_shells(self.preferred_shells)
151
180
 
152
- if not has_claude and not has_copilot:
153
- result["error"] = "No supported AI interface found (.claude or .github/prompts)"
181
+ if not agents:
182
+ supported = [
183
+ name for name, config in self.manifest.manifest.get('agents', {}).items()
184
+ if config.get('supported', False)
185
+ ]
186
+ result["error"] = (
187
+ f"No supported AI interface found. Supported: {', '.join(supported)}. "
188
+ r"To enable AI interface support, create a '.claude\' or '.github\prompts\' directory in your project."
189
+ )
154
190
  return result
155
191
 
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)")
192
+ if not self.force:
193
+ conflicts = self.conflict_checker.check_conflicts(self.kits, agents, shells)
194
+
195
+ if conflicts['has_conflicts']:
196
+ result["conflicts"] = conflicts['overwrites']
197
+ result["error"] = f"Found {len(conflicts['conflicts'])} file conflicts. Use --force to overwrite."
198
+ return result
199
+
200
+ options = self.manifest.manifest.get('options', {})
201
+
202
+ for kit_name in self.kits:
203
+ self._install_kit(kit_name, agents, shells, options, result)
212
204
 
213
205
  result["success"] = True
214
206
 
207
+ if options.get('validate_on_install', True):
208
+ result["validation"] = self.validator.validate_all()
209
+
215
210
  except Exception as e:
216
211
  result["error"] = str(e)
217
212
 
218
213
  return result
219
214
 
220
- def validate(self) -> Dict:
221
- """
222
- Validate kit installation.
215
+ def _install_kit(self, kit_name: str, agents: List[str], shells: List[str], options: Dict, result: Dict):
216
+ """Install a single kit."""
217
+ skip_existing = options.get('skip_existing', True)
223
218
 
224
- Returns:
225
- Dictionary with validation results
226
- """
227
- checks = {}
219
+ for agent in agents:
220
+ files = self.manifest.get_kit_files(kit_name, agent=agent)
221
+ self._install_files(files, skip_existing, result)
228
222
 
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"
223
+ for shell in shells:
224
+ files = self.manifest.get_kit_files(kit_name, agent=shell)
225
+ self._install_files(files, skip_existing, result)
232
226
 
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
- }
227
+ all_files = self.manifest.get_kit_files(kit_name, agent=None)
228
+ for file_info in all_files:
229
+ if file_info.get('type') in ['command', 'prompt', 'script']:
230
+ continue
231
+ self._install_files([file_info], skip_existing, result)
239
232
 
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
- }
233
+ def _install_files(self, files: List[Dict], skip_existing: bool, result: Dict):
234
+ """Install a list of files."""
235
+ for file_info in files:
236
+ if file_info.get('status') == 'planned':
237
+ result["skipped"].append(f"{file_info['path']} (planned)")
238
+ continue
252
239
 
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"
240
+ target_path = self.target_dir / file_info['path']
257
241
 
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
- }
242
+ if skip_existing and target_path.exists() and not self.force:
243
+ result["skipped"].append(file_info['path'])
244
+ continue
264
245
 
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"]
246
+ self._copy_file(file_info['source'], file_info['path'])
247
+ result["installed"].append(file_info['path'])
268
248
 
269
- return {
270
- "valid": all_passed,
271
- "checks": checks,
249
+ def validate(self) -> Dict:
250
+ """Validate all installed kits."""
251
+ return self.validator.validate_all()
252
+
253
+ def preview_removal(self) -> Dict:
254
+ """Preview files that would be removed."""
255
+ preview = {
256
+ "kits": [],
257
+ "total_files": 0,
272
258
  }
273
259
 
274
- # Private installation methods
260
+ for kit_name in self.kits:
261
+ kit_info = self.manifest.get_kit(kit_name)
262
+ files_to_remove = []
275
263
 
276
- def _install_file(self, kit_relative_path: str, target_relative_path: str):
277
- """
278
- Install a file from kits directory to target project.
264
+ all_files = self.manifest.get_kit_files(kit_name, agent=None)
279
265
 
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
266
+ for file_info in all_files:
267
+ target_path = self.target_dir / file_info['path']
268
+ if target_path.exists():
269
+ files_to_remove.append(file_info['path'])
286
270
 
287
- if not source.exists():
288
- raise FileNotFoundError(f"Kit file not found: {source}")
289
-
290
- target.parent.mkdir(parents=True, exist_ok=True)
291
- shutil.copy2(source, target)
271
+ if files_to_remove:
272
+ preview["kits"].append({
273
+ 'name': kit_info['name'],
274
+ 'files': files_to_remove
275
+ })
276
+ preview["total_files"] += len(files_to_remove)
292
277
 
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
278
+ return preview
325
279
 
326
280
  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
- """
281
+ """Remove kits from project."""
335
282
  result = {
336
283
  "success": False,
337
284
  "removed": [],
285
+ "not_found": [],
338
286
  "error": None,
339
287
  }
340
288
 
341
289
  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}")
290
+ for kit_name in self.kits:
291
+ kit_info = self.manifest.get_kit(kit_name)
292
+ removed_files = []
293
+ not_found_files = []
425
294
 
426
- if removed:
427
- result["removed"].append(f"multiagent-kit: {', '.join(removed)}")
295
+ all_files = self.manifest.get_kit_files(kit_name, agent=None)
428
296
 
429
- # Note: Preserve collaboration directories (user data)
430
- # Note: Preserve vanilla spec-kit files
297
+ for file_info in all_files:
298
+ target_path = self.target_dir / file_info['path']
299
+
300
+ if target_path.exists():
301
+ target_path.unlink()
302
+ removed_files.append(file_info['path'])
303
+ else:
304
+ not_found_files.append(file_info['path'])
305
+
306
+ if removed_files:
307
+ result["removed"].append({'kit': kit_info['name'], 'files': removed_files})
308
+
309
+ if not_found_files:
310
+ result["not_found"].extend(not_found_files)
431
311
 
432
312
  result["success"] = True
433
313
 
@@ -435,3 +315,14 @@ class Installer:
435
315
  result["error"] = str(e)
436
316
 
437
317
  return result
318
+
319
+ def _copy_file(self, kit_relative_path: str, target_relative_path: str):
320
+ """Copy file from kits/ to target project."""
321
+ source = self.kits_dir / kit_relative_path
322
+ target = self.target_dir / target_relative_path
323
+
324
+ if not source.exists():
325
+ raise FileNotFoundError(f"Kit file not found: {source}")
326
+
327
+ target.parent.mkdir(parents=True, exist_ok=True)
328
+ shutil.copy2(source, target)