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
lite_kits/cli.py CHANGED
@@ -14,31 +14,28 @@ from rich.console import Console
14
14
  from rich.panel import Panel
15
15
  from rich.table import Table
16
16
 
17
- from . import __version__
17
+ from . import (
18
+ __version__,
19
+ APP_NAME,
20
+ APP_DESCRIPTION,
21
+ REPOSITORY_URL,
22
+ LICENSE,
23
+ KIT_DEV,
24
+ KIT_MULTIAGENT,
25
+ KITS_ALL,
26
+ KIT_DESC_DEV,
27
+ KIT_DESC_MULTIAGENT,
28
+ DIR_CLAUDE_COMMANDS,
29
+ DIR_GITHUB_PROMPTS,
30
+ DIR_SPECIFY_MEMORY,
31
+ DIR_SPECIFY_SCRIPTS_BASH,
32
+ DIR_SPECIFY_SCRIPTS_POWERSHELL,
33
+ DIR_SPECIFY_TEMPLATES,
34
+ ERROR_NOT_SPEC_KIT,
35
+ ERROR_SPEC_KIT_HINT,
36
+ )
18
37
  from .core import diagonal_reveal_banner, show_loading_spinner, show_static_banner, Installer
19
38
 
20
- # Constants
21
- APP_NAME = "lite-kits"
22
- APP_DESCRIPTION = "Lightweight enhancement kits for spec-driven development."
23
-
24
- # Kit names
25
- KIT_DEV = "dev"
26
- KIT_MULTIAGENT = "multiagent"
27
- KITS_ALL = [KIT_DEV, KIT_MULTIAGENT]
28
- KITS_RECOMMENDED = [KIT_DEV] # dev-kit is the default, multiagent is optional
29
-
30
- # Kit descriptions for help
31
- KIT_DESC_DEV = "Solo development essentials: /orient, /commit, /pr, /review, /cleanup, /audit, /stats"
32
- KIT_DESC_MULTIAGENT = "Multi-agent coordination: /sync, collaboration dirs, memory guides (optional)"
33
-
34
- # Marker files for kit detection
35
- MARKER_DEV_KIT = ".claude/commands/orient.md" # If orient exists, dev-kit is installed
36
- MARKER_MULTIAGENT_KIT = ".specify/memory/pr-workflow-guide.md"
37
-
38
- # Error messages
39
- ERROR_NOT_SPEC_KIT = "does not appear to be a spec-kit project!"
40
- ERROR_SPEC_KIT_HINT = "Looking for one of: .specify/, .claude/, or .github/prompts/"
41
-
42
39
  app = typer.Typer(
43
40
  name=APP_NAME,
44
41
  help=APP_DESCRIPTION, # Restore original description for --help
@@ -54,34 +51,102 @@ def print_help_hint():
54
51
  def print_version_info():
55
52
  """Print version information."""
56
53
  console.print(f"[bold]Version:[/bold]")
57
- console.print(f" [bold cyan]{APP_NAME} version {__version__}[/bold cyan]\n")
54
+ console.print(f" [bold cyan]{APP_NAME} version {__version__}[/bold cyan]")
58
55
 
59
56
  def print_quick_start():
60
57
  console.print("[bold]Quick Start:[/bold]")
61
- console.print(f" [cyan]1. {APP_NAME} add --recommended[/cyan] # Add project + git kits")
62
- console.print(f" [cyan]2. {APP_NAME} status[/cyan] # Check installation")
63
- console.print(f" [cyan]3. {APP_NAME} info[/cyan] # Package details\n")
58
+ console.print(f" [cyan]1. {APP_NAME} add[/cyan] # Add dev-kit (default)")
59
+ console.print(f" [cyan]2. {APP_NAME} status[/cyan] # Check installation")
60
+ console.print(f" [cyan]3. {APP_NAME} validate[/cyan] # Validate kit files\n")
61
+
62
+ def print_spec_kit_error():
63
+ """Print standardized spec-kit not found error message with installation instructions."""
64
+ console.print()
65
+ console.print(
66
+ f"[red]Error:[/red] {ERROR_NOT_SPEC_KIT}",
67
+ style="bold",
68
+ )
69
+ console.print(
70
+ f"\n {ERROR_SPEC_KIT_HINT}",
71
+ style="dim",
72
+ )
73
+ console.print("\n[bold yellow]lite-kits requires GitHub Spec-Kit:[/bold yellow]")
74
+ console.print(" lite-kits enhances vanilla spec-kit projects with additional commands.")
75
+ console.print(" You must install spec-kit first before adding lite-kits enhancements.\n")
76
+ console.print("[bold cyan]Install Spec-Kit:[/bold cyan]")
77
+ console.print(" 1. Install Node.js: https://nodejs.org/")
78
+ console.print(" 2. Install spec-kit: npm install -g @github/spec-kit")
79
+ console.print(" 3. Create project: specify init your-project-name")
80
+ console.print(" 4. More info: https://github.com/github/spec-kit\n")
81
+ console.print()
82
+
83
+ def _build_kit_breakdown_table(target_dir: Path, kits: list[str]) -> Table:
84
+ """Build agent/shell breakdown table for kits.
85
+
86
+ Args:
87
+ target_dir: Target project directory
88
+ kits: List of kit names (e.g., ["dev", "multiagent"])
89
+
90
+ Returns:
91
+ Rich Table with agent/shell breakdown
92
+ """
93
+ from rich.box import ROUNDED
94
+
95
+ # Detect which agents/shells have files
96
+ agent_dirs = {
97
+ "Claude Code": target_dir / ".claude" / "commands",
98
+ "GitHub Copilot": target_dir / ".github" / "prompts"
99
+ }
100
+
101
+ shell_dirs = {
102
+ "Bash": target_dir / ".specify" / "scripts" / "bash",
103
+ "PowerShell": target_dir / ".specify" / "scripts" / "powershell"
104
+ }
105
+
106
+ # Build table
107
+ table = Table(show_header=True, header_style="bold cyan", box=ROUNDED, title=f"[bold magenta]Kit Breakdown[/bold magenta]")
108
+ table.add_column("Kit", style="cyan")
109
+ table.add_column("Agents", style="green")
110
+ table.add_column("Shells", style="white")
111
+
112
+ for kit in kits:
113
+ # Check which agents have this kit's files
114
+ agents_with_kit = []
115
+ for agent_name, agent_dir in agent_dirs.items():
116
+ if agent_dir.exists() and any(agent_dir.glob("*.md")):
117
+ # Check for at least one non-spec-kit file (not speckit.*)
118
+ non_speckit_files = [f for f in agent_dir.glob("*.md") if not f.stem.startswith("speckit.")]
119
+ if non_speckit_files:
120
+ agents_with_kit.append(agent_name)
121
+
122
+ # Check which shells have scripts
123
+ shells_with_kit = []
124
+ for shell_name, shell_dir in shell_dirs.items():
125
+ if shell_dir.exists() and (any(shell_dir.glob("*.sh")) or any(shell_dir.glob("*.ps1"))):
126
+ shells_with_kit.append(shell_name)
127
+
128
+ # Format output
129
+ agents_display = ", ".join(agents_with_kit) if agents_with_kit else "[dim]none[/dim]"
130
+ shells_display = ", ".join(shells_with_kit) if shells_with_kit else "[dim]none[/dim]"
131
+
132
+ # Use consistent kit naming format: "kit-name" not "kit-name-kit"
133
+ table.add_row(kit, agents_display, shells_display)
134
+
135
+ return table
64
136
 
65
137
  def print_kit_info(target_dir: Path, is_spec_kit: bool, installed_kits: list):
66
- """Print kit installation info."""
138
+ """Print kit installation info with agent/shell breakdown."""
67
139
  console.print()
68
140
  if is_spec_kit:
69
141
  console.print(f"[bold green][OK] Spec-kit project detected in {target_dir}.[/bold green]\n")
70
142
  if installed_kits:
71
- console.print("Installed kits:", style="bold")
72
- kit_icons = {
73
- "project": "🎯",
74
- "git": "🔧",
75
- "multiagent": "🤝"
76
- }
77
- for kit in installed_kits:
78
- icon = kit_icons.get(kit, "📦")
79
- console.print(f" {icon} {kit}-kit", style="green")
143
+ table = _build_kit_breakdown_table(target_dir, installed_kits)
144
+ console.print(table)
80
145
  else:
81
146
  console.print("No kits installed.", style="dim yellow")
82
147
  else:
83
- console.print(f"[bold red][X] {target_dir} {ERROR_NOT_SPEC_KIT}[/bold red]")
84
- console.print(f"{ERROR_SPEC_KIT_HINT}", style="dim")
148
+ console.print(f"[bold red][X] {target_dir} is not a spec-kit project[/bold red]")
149
+ console.print(f" {ERROR_SPEC_KIT_HINT}", style="dim")
85
150
  console.print()
86
151
 
87
152
  def version_callback(value: bool):
@@ -91,21 +156,13 @@ def version_callback(value: bool):
91
156
  print_version_info()
92
157
  raise typer.Exit()
93
158
 
94
- def banner_callback(value: bool):
95
- """Show banner + hint and exit."""
96
- if value:
97
- diagonal_reveal_banner()
98
- print_help_hint()
99
- print_quick_start()
100
- raise typer.Exit()
101
-
102
159
  @app.callback(invoke_without_command=True)
103
160
  def main(
104
161
  ctx: typer.Context,
105
162
  version: Optional[bool] = typer.Option(
106
163
  None,
107
164
  "--version",
108
- "-V",
165
+ "-V",
109
166
  help="Display the lite-kits version",
110
167
  callback=version_callback,
111
168
  is_eager=True,
@@ -113,9 +170,7 @@ def main(
113
170
  banner: Optional[bool] = typer.Option(
114
171
  None,
115
172
  "--banner",
116
- help="Show the lite-kits banner",
117
- callback=banner_callback,
118
- is_eager=True,
173
+ help="Show the animated banner (can be combined with other commands)",
119
174
  ),
120
175
  quiet: Optional[bool] = typer.Option(
121
176
  None,
@@ -125,7 +180,7 @@ def main(
125
180
  ),
126
181
  verbose: Optional[bool] = typer.Option(
127
182
  None,
128
- "--verbose",
183
+ "--verbose",
129
184
  "-v",
130
185
  help="Use verbose output",
131
186
  ),
@@ -139,13 +194,31 @@ def main(
139
194
  if directory:
140
195
  import os
141
196
  os.chdir(directory)
142
-
197
+
198
+ # Store banner flag in context for commands to use
199
+ ctx.obj = {"show_banner": banner}
200
+
201
+ # Show banner if requested
202
+ if banner:
203
+ try:
204
+ diagonal_reveal_banner()
205
+ except UnicodeEncodeError:
206
+ # Windows console doesn't support Unicode box characters
207
+ console.print("[bold cyan]LITE-KITS[/bold cyan]")
208
+ console.print("[dim]Lightweight enhancement kits for spec-driven development[/dim]\n")
209
+
143
210
  # Show banner + hint and quick-start when no command is given
144
211
  if ctx.invoked_subcommand is None:
145
- show_static_banner()
212
+ if not banner: # Only show static banner if animated not already shown
213
+ show_static_banner()
146
214
  print_help_hint()
147
215
  print_quick_start()
148
216
 
217
+ @app.command(hidden=True)
218
+ def help(ctx: typer.Context):
219
+ """Show help information (alias for --help)."""
220
+ console.print(ctx.parent.get_help())
221
+
149
222
  @app.command(name="add")
150
223
  def add_kits(
151
224
  kit: Optional[str] = typer.Option(
@@ -153,84 +226,152 @@ def add_kits(
153
226
  "--kit",
154
227
  help=f"Comma-separated list of kits to add: {','.join(KITS_ALL)}",
155
228
  ),
156
- recommended: bool = typer.Option(
229
+ all_kits: bool = typer.Option(
157
230
  False,
158
- "--recommended",
159
- help=f"Add recommended kits: {' + '.join(KITS_RECOMMENDED)}",
231
+ "--all",
232
+ help="Add all kits",
160
233
  ),
161
- dry_run: bool = typer.Option(
234
+ agent: Optional[str] = typer.Option(
235
+ None,
236
+ "--agent",
237
+ help="Explicit agent preference (claude, copilot, etc.)",
238
+ ),
239
+ shell: Optional[str] = typer.Option(
240
+ None,
241
+ "--shell",
242
+ help="Explicit shell preference (bash, powershell)",
243
+ ),
244
+ verbose: bool = typer.Option(
162
245
  False,
163
- "--dry-run",
164
- help="Preview changes without applying them",
246
+ "--verbose",
247
+ "-v",
248
+ help="Show detailed file listings in preview",
249
+ ),
250
+ force: bool = typer.Option(
251
+ False,
252
+ "--force",
253
+ help="Skip preview and confirmations, overwrite existing files",
165
254
  ),
166
255
  target: Optional[Path] = typer.Argument(
167
256
  None,
168
257
  help="Target directory (defaults to current directory)",
169
258
  ),
170
259
  ):
171
- """Add enhancement kits to a spec-kit project."""
260
+ """Add enhancement kits to a spec-kit project.
261
+
262
+ Shows a preview of changes before installation and asks for confirmation.
263
+ Use --verbose/-v to see detailed file listings.
264
+ Use --force to skip preview and install immediately.
265
+ """
172
266
  target_dir = Path.cwd() if target is None else target
173
267
 
174
268
  # Determine which kits to install
175
269
  kits = None
176
- if recommended:
177
- kits = KITS_RECOMMENDED
270
+ if all_kits:
271
+ kits = KITS_ALL
178
272
  elif kit:
179
273
  kits = [k.strip() for k in kit.split(',')]
180
- # else: kits=None will use default [KIT_PROJECT] in Installer
274
+ # else: kits=None will use default from manifest
275
+
276
+ # Parse comma-separated agents and shells
277
+ agents = [a.strip() for a in agent.split(',')] if agent else None
278
+ shells = [s.strip() for s in shell.split(',')] if shell else None
181
279
 
182
280
  try:
183
- installer = Installer(target_dir, kits=kits)
281
+ installer = Installer(
282
+ target_dir,
283
+ kits=kits,
284
+ force=force,
285
+ agents=agents,
286
+ shells=shells
287
+ )
184
288
  except ValueError as e:
289
+ console.print()
185
290
  console.print(f"[red]Error:[/red] {e}", style="bold")
291
+ console.print()
186
292
  raise typer.Exit(1)
187
293
 
188
294
  # Validate target is a spec-kit project
189
295
  if not installer.is_spec_kit_project():
190
- console.print(
191
- f"[red]Error:[/red] {target_dir} {ERROR_NOT_SPEC_KIT}",
192
- style="bold",
193
- )
194
- console.print(
195
- f"\n{ERROR_SPEC_KIT_HINT}",
196
- style="dim",
197
- )
296
+ print_spec_kit_error()
198
297
  raise typer.Exit(1)
199
298
 
200
- # Check if already installed
201
- if installer.is_multiagent_installed():
299
+ # Check if already installed (check for dev-kit as default)
300
+ skip_preview = force # Track if we should skip preview (only when --force flag used)
301
+ reinstalling = False
302
+
303
+ if installer.is_kit_installed(KIT_DEV):
304
+ console.print()
202
305
  console.print(
203
306
  "[yellow]Warning:[/yellow] Enhancement kits appear to be already installed",
204
307
  style="bold",
205
308
  )
206
- if not typer.confirm("Reinstall anyway?"):
309
+ if not force:
310
+ if not typer.confirm("Reinstall anyway?"):
311
+ console.print()
312
+ raise typer.Exit(0)
313
+ # User confirmed reinstall - mark as reinstalling but still show preview
314
+ reinstalling = True
315
+
316
+ # Always show preview unless --force flag was used
317
+ if not skip_preview:
318
+ try:
319
+ preview = installer.preview_installation()
320
+ except ValueError as e:
321
+ console.print()
322
+ console.print(f"[red]Error:[/red] {e}", style="bold")
323
+ console.print()
324
+ raise typer.Exit(1)
325
+
326
+ normalized_preview = _normalize_preview_for_display(preview, operation="install")
327
+ _display_changes(normalized_preview, target_dir, verbose=verbose)
328
+
329
+ # Show warnings/conflicts
330
+ if preview.get("warnings"):
331
+ console.print("\n[bold yellow]Warnings:[/bold yellow]")
332
+ for warning in preview["warnings"]:
333
+ console.print(f" ⚠ {warning}")
334
+
335
+ if preview.get("conflicts"):
336
+ console.print("\n[bold yellow]Conflicts (will overwrite):[/bold yellow]")
337
+ for conflict in preview["conflicts"]:
338
+ console.print(f" ⚠ {conflict['path']}")
339
+
340
+ # Ask for confirmation
341
+ console.print()
342
+ if not typer.confirm("Proceed with installation?"):
343
+ console.print("[dim]Installation cancelled[/dim]")
344
+ console.print()
207
345
  raise typer.Exit(0)
208
346
 
209
- # Preview or install
210
- if dry_run:
211
- console.print("\n[bold cyan]Dry run - no changes will be made[/bold cyan]\n")
212
- changes = installer.preview_installation()
213
- _display_changes(changes)
214
- else:
215
- console.print(f"\n[bold green]Adding enhancement kits to {target_dir}[/bold green]\n")
347
+ # User confirmed preview - enable force to bypass conflict checks during install
348
+ # (This recreates installer with force=True to skip conflict detection)
349
+ installer = Installer(
350
+ target_dir,
351
+ kits=kits,
352
+ force=True, # Skip conflict checks after user confirmed
353
+ agents=agents,
354
+ shells=shells
355
+ )
216
356
 
217
- show_loading_spinner("Adding kits...")
218
- result = installer.install()
357
+ # Install
358
+ console.print(f"\n[bold green]Installing kits to {target_dir}[/bold green]\n")
359
+ show_loading_spinner("Installing...")
360
+ result = installer.install()
219
361
 
220
- if result["success"]:
221
- console.print("\n[bold green][OK] Kits added successfully![/bold green]\n")
222
- _display_installation_summary(result)
223
- # Show static banner after successful install
224
- show_static_banner()
225
- else:
226
- console.print(f"\n[bold red][X] Failed to add kits:[/bold red] {result['error']}\n")
227
- raise typer.Exit(1)
362
+ if result["success"]:
363
+ _display_installation_summary(result, verbose=verbose)
364
+ console.print("[bold green][OK] Kits installed successfully![/bold green]\n")
365
+ else:
366
+ console.print(f"\n[bold red][X] Installation failed:[/bold red] {result['error']}\n")
367
+ console.print()
368
+ raise typer.Exit(1)
228
369
 
229
370
  @app.command()
230
371
  def remove(
231
372
  kit: Optional[str] = typer.Option(
232
373
  None,
233
- "--kit",
374
+ "--kit",
234
375
  help=f"Comma-separated list of kits to remove: {','.join(KITS_ALL)}",
235
376
  ),
236
377
  all_kits: bool = typer.Option(
@@ -238,6 +379,17 @@ def remove(
238
379
  "--all",
239
380
  help="Remove all kits",
240
381
  ),
382
+ verbose: bool = typer.Option(
383
+ False,
384
+ "--verbose",
385
+ "-v",
386
+ help="Show detailed file listings in preview",
387
+ ),
388
+ force: bool = typer.Option(
389
+ False,
390
+ "--force",
391
+ help="Skip preview and confirmations",
392
+ ),
241
393
  target: Optional[Path] = typer.Argument(
242
394
  None,
243
395
  help="Target directory (defaults to current directory)",
@@ -245,12 +397,16 @@ def remove(
245
397
  ):
246
398
  """Remove enhancement kits from a spec-kit project.
247
399
 
248
- Returns the project to vanilla spec-kit state.
400
+ Removes kit files and returns the project to vanilla spec-kit state.
401
+ Shows preview of files to be removed before confirmation.
402
+ Use --verbose/-v to see detailed file listings.
403
+ Use --force to skip preview and remove immediately.
249
404
 
250
405
  Examples:
251
- lite-kits remove --kit git # Remove git-kit only
252
- lite-kits remove --kit project,git # Remove specific kits
406
+ lite-kits remove --kit dev # Remove dev-kit
407
+ lite-kits remove --kit dev,multiagent # Remove multiple kits
253
408
  lite-kits remove --all # Remove all kits
409
+ lite-kits remove --all --force # Remove all kits without confirmation
254
410
  """
255
411
  target_dir = Path.cwd() if target is None else target
256
412
 
@@ -261,46 +417,66 @@ def remove(
261
417
  elif kit:
262
418
  kits = [k.strip() for k in kit.split(',')]
263
419
  else:
420
+ console.print()
264
421
  console.print("[yellow]Error:[/yellow] Specify --kit or --all", style="bold")
265
422
  console.print("\nExamples:", style="dim")
266
- console.print(f" {APP_NAME} remove --here --kit {KIT_GIT}", style="dim")
267
- console.print(f" {APP_NAME} remove --here --all", style="dim")
423
+ console.print(f" {APP_NAME} remove --kit {KIT_DEV}", style="dim")
424
+ console.print(f" {APP_NAME} remove --all", style="dim")
425
+ console.print()
268
426
  raise typer.Exit(1)
269
427
 
270
428
  try:
271
429
  installer = Installer(target_dir, kits=kits)
272
430
  except ValueError as e:
431
+ console.print()
273
432
  console.print(f"[red]Error:[/red] {e}", style="bold")
433
+ console.print()
274
434
  raise typer.Exit(1)
275
435
 
276
- # Check if kits are installed
277
- if not installer.is_multiagent_installed():
436
+ # Filter to only actually installed kits
437
+ installed_kits = [k for k in kits if installer.is_kit_installed(k)]
438
+ if not installed_kits:
439
+ console.print()
278
440
  console.print("[yellow]Warning:[/yellow] No kits detected to remove", style="bold")
441
+ console.print()
279
442
  raise typer.Exit(0)
280
443
 
281
- # Confirm removal
282
- console.print(f"\n[bold yellow]Remove kits from {target_dir}[/bold yellow]")
283
- console.print(f"Kits to remove: {', '.join(kits)}\n")
444
+ # Update installer with filtered list
445
+ installer = Installer(target_dir, kits=installed_kits)
284
446
 
285
- if not typer.confirm("Continue with removal?"):
286
- console.print("Cancelled")
287
- raise typer.Exit(0)
447
+ # Show preview and confirmation unless --force is used
448
+ if not force:
449
+ # Show preview of files to be removed
450
+ preview = installer.preview_removal()
451
+
452
+ if preview["total_files"] == 0:
453
+ console.print("[dim]No files found to remove[/dim]")
454
+ console.print()
455
+ raise typer.Exit(0)
456
+
457
+ # Normalize removal preview to standard format for DRY display
458
+ normalized_preview = _normalize_preview_for_display(preview, operation="remove")
459
+ _display_changes(normalized_preview, target_dir, verbose=verbose)
460
+
461
+ # Confirm removal
462
+ if not typer.confirm("Continue with removal?"):
463
+ console.print("[dim]Cancelled[/dim]")
464
+ console.print()
465
+ raise typer.Exit(0)
288
466
 
289
467
  # Remove kits
290
- console.print("\n[bold]Removing kits...[/bold]\n")
291
- with console.status("[bold yellow]Removing..."):
292
- result = installer.remove()
468
+ console.print(f"\n[bold yellow]Removing files...[/bold yellow]")
469
+ result = installer.remove()
293
470
 
294
471
  if result["success"]:
295
- console.print("[bold green]Removal complete![/bold green]\n")
296
- if result["removed"]:
297
- console.print("[bold]Removed:[/bold]")
298
- for item in result["removed"]:
299
- console.print(f" - {item}")
300
- else:
301
- console.print("[dim]No files found to remove[/dim]")
472
+ _display_removal_summary(result, verbose=verbose)
473
+
474
+ # Clean up empty directories
475
+ _cleanup_empty_directories(target_dir)
476
+ console.print("\n[bold green][OK] Removal complete![/bold green]\n")
302
477
  else:
303
- console.print(f"\n[bold red]Removal failed:[/bold red] {result['error']}\n")
478
+ console.print(f"\n[bold red][X] Removal failed:[/bold red] {result['error']}\n")
479
+ console.print()
304
480
  raise typer.Exit(1)
305
481
 
306
482
  @app.command()
@@ -310,37 +486,40 @@ def validate(
310
486
  help="Target directory (defaults to current directory)",
311
487
  ),
312
488
  ):
313
- """Validate enhancement kit installation.
489
+ """Validate enhancement kit installation integrity.
314
490
 
315
491
  Checks:
316
- - Kit files are present and correctly installed
317
- - Collaboration directory structure (if multiagent-kit installed)
318
- - Required files present
319
- - Cross-kit consistency
492
+ - All required kit files are present
493
+ - Files are not corrupted or empty
494
+ - Kit structure is correct
495
+ - Collaboration directories (for multiagent-kit)
320
496
 
321
497
  Example:
322
- lite-kits validate
498
+ lite-kits validate # Validate current directory
499
+ lite-kits validate path/to/dir # Validate specific directory
323
500
  """
324
501
  target_dir = Path.cwd() if target is None else target
325
502
 
326
503
  # For validation, we don't know which kits are installed yet, so check for all
327
504
  installer = Installer(target_dir, kits=KITS_ALL)
328
505
 
329
- console.print(f"\n[bold cyan]Validating {target_dir}[/bold cyan]\n")
330
-
331
- # Check if it's a spec-kit project
506
+ # Check if it's a spec-kit project first
332
507
  if not installer.is_spec_kit_project():
333
- console.print("[red][X] Not a spec-kit project[/red]")
508
+ print_spec_kit_error()
334
509
  raise typer.Exit(1)
335
510
 
336
511
  # Check if any kits are installed
337
- if not installer.is_multiagent_installed():
512
+ any_installed = any(installer.is_kit_installed(k) for k in KITS_ALL)
513
+ if not any_installed:
514
+ console.print()
338
515
  console.print("[yellow]⚠ No enhancement kits installed[/yellow]")
339
- console.print(f" Run: {APP_NAME} add --here --recommended", style="dim")
516
+ console.print(f" Run: {APP_NAME} add", style="dim")
517
+ console.print()
340
518
  raise typer.Exit(1)
341
519
 
342
520
  # Validate structure
343
- validation_result = show_loading_spinner("Validating...", installer.validate)
521
+ console.print(f"\n[bold cyan]Validating {target_dir}[/bold cyan]\n")
522
+ validation_result = installer.validate()
344
523
  _display_validation_results(validation_result)
345
524
 
346
525
  if validation_result["valid"]:
@@ -357,15 +536,16 @@ def status(
357
536
  help="Target directory (defaults to current directory)",
358
537
  ),
359
538
  ):
360
- """Show enhancement kit installation status for the project.
539
+ """Show enhancement kit installation status.
361
540
 
362
541
  Displays:
363
- - Spec-kit project detection
364
- - Installed kits
365
- - Installation health
542
+ - Whether directory is a spec-kit project
543
+ - Which kits are installed (dev, multiagent)
544
+ - Quick summary of installation state
366
545
 
367
546
  Example:
368
- lite-kits status
547
+ lite-kits status # Check current directory
548
+ lite-kits status path/to/dir # Check specific directory
369
549
  """
370
550
  target_dir = Path.cwd() if target is None else target
371
551
 
@@ -375,75 +555,461 @@ def status(
375
555
  # Basic checks
376
556
  is_spec_kit = installer.is_spec_kit_project()
377
557
 
378
- # Check individual kits
379
- project_kit_installed = (target_dir / MARKER_PROJECT_KIT).exists()
380
- git_kit_installed = (target_dir / MARKER_GIT_KIT).exists()
381
- multiagent_kit_installed = (target_dir / MARKER_MULTIAGENT_KIT).exists()
382
-
383
- # Build list of installed kits for banner
558
+ # Check individual kits using the installer's validator
384
559
  installed_kits = []
385
- if project_kit_installed:
386
- installed_kits.append("project")
387
- if git_kit_installed:
388
- installed_kits.append("git")
389
- if multiagent_kit_installed:
390
- installed_kits.append("multiagent")
391
-
392
- # Show banner + kit info
393
- show_static_banner()
560
+ for kit_name in KITS_ALL:
561
+ if installer.is_kit_installed(kit_name):
562
+ installed_kits.append(kit_name)
563
+
564
+ # Show kit info (skip banner to avoid Windows console Unicode issues)
394
565
  print_kit_info(target_dir, is_spec_kit, installed_kits)
395
566
 
396
- def _display_changes(changes: dict):
397
- """Display preview of changes."""
398
- console.print("[bold]Files to be created:[/bold]")
399
- for file in changes.get("new_files", []):
400
- console.print(f" [green]+[/green] {file}")
567
+ def _normalize_preview_for_display(preview: dict, operation: str = "install") -> dict:
568
+ """Normalize preview data to standard format for display.
401
569
 
402
- console.print("\n[bold]Files to be modified:[/bold]")
403
- for file in changes.get("modified_files", []):
404
- console.print(f" [yellow]~[/yellow] {file}")
570
+ Converts both installation and removal previews to a unified format
571
+ that _display_changes can handle.
405
572
 
406
- console.print("\n[bold]Directories to be created:[/bold]")
407
- for dir in changes.get("new_directories", []):
408
- console.print(f" [blue]+[/blue] {dir}")
573
+ Args:
574
+ preview: Raw preview dict from installer (preview_installation or preview_removal)
575
+ operation: "install" or "remove" to determine how to process the preview
409
576
 
410
- def _display_installation_summary(result: dict):
411
- """Display kit addition summary."""
412
- console.print("[bold]Added:[/bold]")
413
- for item in result.get("installed", []):
414
- console.print(f" [OK] {item}")
577
+ Returns:
578
+ Normalized preview dict with standard keys (new_files, modified_files,
579
+ files_to_remove, new_directories, directories_to_remove)
580
+ """
581
+ if operation == "install":
582
+ # Installation preview is already in the right format
583
+ return preview
584
+ elif operation == "remove":
585
+ # Removal preview needs conversion
586
+ normalized = {"kits": []}
587
+ for kit in preview.get("kits", []):
588
+ # Calculate unique directories that will be affected
589
+ directories = set()
590
+ for file_path in kit.get("files", []):
591
+ parent = str(Path(file_path).parent)
592
+ if parent and parent != ".":
593
+ directories.add(parent)
594
+
595
+ normalized["kits"].append({
596
+ "name": kit["name"],
597
+ "new_files": [],
598
+ "modified_files": [],
599
+ "files_to_remove": kit["files"],
600
+ "new_directories": [],
601
+ "directories_to_remove": sorted(directories)
602
+ })
603
+ return normalized
604
+ else:
605
+ raise ValueError(f"Unknown operation: {operation}")
606
+
607
+ def _display_changes(changes: dict, target_dir: Path, verbose: bool = False):
608
+ """Display preview of changes.
609
+
610
+ Args:
611
+ changes: Normalized preview dict with file/directory changes
612
+ target_dir: Target directory being modified
613
+ verbose: If True, show detailed file listings; if False, show only tables
614
+ """
615
+ from collections import defaultdict
616
+
617
+ # Show preview header
618
+ console.print(f"\n[bold magenta]Previewing changes for:[/bold magenta]\n[bold yellow]{target_dir}[/bold yellow]\n")
619
+
620
+ # Collect stats for each kit
621
+ kit_stats = {}
622
+ total_stats = defaultdict(int)
623
+
624
+ for kit in changes.get("kits", []):
625
+ kit_name = kit.get("name", "Unknown Kit")
626
+ stats = defaultdict(int)
627
+
628
+ # Count files by type based on path (normalize to forward slashes for matching)
629
+ # Include new_files, modified_files, and files_to_remove
630
+ all_files = (
631
+ kit.get("new_files", []) +
632
+ kit.get("modified_files", []) +
633
+ kit.get("files_to_remove", [])
634
+ )
635
+
636
+ for file_path in all_files:
637
+ # Normalize path separators for cross-platform matching
638
+ normalized_path = str(file_path).replace("\\", "/")
639
+
640
+ # Track by file type
641
+ if "/commands/" in normalized_path or "/prompts/" in normalized_path:
642
+ stats["commands"] += 1
643
+ elif "/scripts/" in normalized_path:
644
+ stats["scripts"] += 1
645
+ elif "/memory/" in normalized_path:
646
+ stats["memory"] += 1
647
+ elif "/templates/" in normalized_path:
648
+ stats["templates"] += 1
649
+ else:
650
+ stats["other"] += 1
651
+
652
+ # Track by agent (for agent breakdown table)
653
+ if ".claude/" in normalized_path:
654
+ stats["agent_claude"] += 1
655
+ elif ".github/" in normalized_path:
656
+ stats["agent_copilot"] += 1
657
+ else:
658
+ stats["agent_shared"] += 1
659
+
660
+ # Count directories (both new and to-be-removed)
661
+ stats["directories"] = len(kit.get("new_directories", [])) + len(kit.get("directories_to_remove", []))
662
+
663
+ # Total files (excluding directories)
664
+ stats["files"] = len(all_files)
665
+
666
+ kit_stats[kit_name.lower().replace(" kit", "")] = stats
667
+
668
+ # Accumulate totals
669
+ for key, value in stats.items():
670
+ total_stats[key] += value
671
+
672
+ # Display kit details (only if verbose)
673
+ if verbose:
674
+ for kit in changes.get("kits", []):
675
+ kit_name = kit.get("name", "Unknown Kit")
676
+ kit_key = kit_name.lower().replace(" kit", "")
677
+ stats = kit_stats[kit_key]
678
+
679
+ console.print(f"[bold magenta]=== {kit_name} ===[/bold magenta]")
680
+
681
+ # Only show sections that have items
682
+ new_files = kit.get("new_files", [])
683
+ modified_files = kit.get("modified_files", [])
684
+ files_to_remove = kit.get("files_to_remove", [])
685
+ new_directories = kit.get("new_directories", [])
686
+
687
+ if new_files:
688
+ console.print("Files to be created:")
689
+ for file in new_files:
690
+ console.print(f" [green]+[/green] {file}")
691
+ console.print() # Blank line after section
692
+
693
+ if modified_files:
694
+ console.print("Files to be modified:")
695
+ for file in modified_files:
696
+ console.print(f" [yellow]~[/yellow] {file}")
697
+ console.print() # Blank line after section
698
+
699
+ if files_to_remove:
700
+ console.print("Files to be removed:")
701
+ for file in files_to_remove:
702
+ console.print(f" [red]-[/red] {file}")
703
+ console.print() # Blank line after section
704
+
705
+ if new_directories:
706
+ console.print("Directories to be created:")
707
+ for dir in new_directories:
708
+ console.print(f" [blue]+[/blue] {dir}")
709
+ console.print() # Blank line after section
710
+
711
+ # Show kit summary
712
+ summary_parts = []
713
+ if stats["files"] > 0:
714
+ summary_parts.append(f"{stats['files']} files")
715
+ if stats["directories"] > 0:
716
+ summary_parts.append(f"{stats['directories']} directory")
717
+ if summary_parts:
718
+ console.print(f"{kit_name}: {', '.join(summary_parts)}")
719
+ console.print() # Blank line after kit
720
+
721
+ # Display summary tables
722
+ _display_preview_tables(kit_stats, changes)
723
+
724
+ def _display_preview_tables(kit_stats: dict, changes: dict):
725
+ """Display preview summary tables with color-coded values.
726
+
727
+ Colors:
728
+ - Green +N: Additions (new files)
729
+ - Yellow ~N: Modifications (changed files)
730
+ - Red -N: Removals (deleted files)
731
+ - White 0: No changes
732
+ """
733
+ from rich.box import ROUNDED
734
+
735
+ if not kit_stats:
736
+ return
737
+
738
+ # Helper to format kit names
739
+ def format_kit_name(kit_key: str) -> str:
740
+ return "Dev Kit" if kit_key == "dev" else "Multiagent Kit"
741
+
742
+ # Helper to style values based on operation type
743
+ def style_value(value: int, is_addition: bool = True, is_modification: bool = False) -> str:
744
+ """Style numeric values with colors and prefix symbols."""
745
+ if value == 0:
746
+ return "0"
747
+ elif is_modification:
748
+ return f"[yellow]~{value}[/yellow]"
749
+ elif is_addition:
750
+ return f"[green]+{value}[/green]"
751
+ else: # removal
752
+ return f"[red]-{value}[/red]"
753
+
754
+ # Determine operation type from changes dict
755
+ has_new_files = any(kit.get("new_files") for kit in changes.get("kits", []))
756
+ has_modified_files = any(kit.get("modified_files") for kit in changes.get("kits", []))
757
+ is_removal = not has_new_files and not has_modified_files # If no new/modified, it's a removal
758
+
759
+ # Agent breakdown table (show which agents get which files)
760
+ agent_categories = ["agent_claude", "agent_copilot", "agent_shared"]
761
+ has_agent_breakdown = any(
762
+ any(stats.get(cat, 0) > 0 for cat in agent_categories)
763
+ for stats in kit_stats.values()
764
+ )
765
+
766
+ if has_agent_breakdown:
767
+ console.print("[bold magenta]Agent Breakdown:[/bold magenta]")
768
+ table = Table(show_header=True, header_style="bold cyan", box=ROUNDED)
769
+ table.add_column("Agent", style="cyan", no_wrap=True)
770
+
771
+ # Add kit columns
772
+ for kit_key in kit_stats.keys():
773
+ table.add_column(format_kit_name(kit_key), justify="right")
774
+
775
+ # Add Total column if multiple kits
776
+ show_total = len(kit_stats) > 1
777
+ if show_total:
778
+ table.add_column("Total", justify="right")
779
+
780
+ # Add rows (only agents with non-zero values)
781
+ agent_labels = {
782
+ "agent_claude": "Claude Code",
783
+ "agent_copilot": "GitHub Copilot",
784
+ "agent_shared": "Shared"
785
+ }
786
+
787
+ for cat in agent_categories:
788
+ values = [kit_stats[kit_key].get(cat, 0) for kit_key in kit_stats.keys()]
789
+ if sum(values) > 0: # Only show row if at least one kit has files for this agent
790
+ styled_values = [style_value(v, not is_removal, has_modified_files) for v in values]
791
+ row = [agent_labels[cat]] + styled_values
792
+ if show_total:
793
+ row.append(style_value(sum(values), not is_removal, has_modified_files))
794
+ table.add_row(*row)
795
+
796
+ console.print(table)
797
+ console.print()
798
+
799
+ # Kit contents table (Commands, Scripts, Memory, Templates)
800
+ file_type_categories = ["commands", "scripts", "memory", "templates"]
801
+ has_file_types = any(
802
+ any(stats.get(cat, 0) > 0 for cat in file_type_categories)
803
+ for stats in kit_stats.values()
804
+ )
805
+
806
+ if has_file_types:
807
+ console.print("[bold magenta]Kit Contents:[/bold magenta]")
808
+ table = Table(show_header=True, header_style="bold cyan", box=ROUNDED)
809
+ table.add_column("Type", style="cyan", no_wrap=True)
810
+
811
+ # Add kit columns (only kits that have file types)
812
+ active_kits = [
813
+ kit_key for kit_key in kit_stats.keys()
814
+ if any(kit_stats[kit_key].get(cat, 0) > 0 for cat in file_type_categories)
815
+ ]
816
+
817
+ for kit_key in active_kits:
818
+ table.add_column(format_kit_name(kit_key), justify="right")
819
+
820
+ # Add Total column if multiple kits
821
+ show_total = len(active_kits) > 1
822
+ if show_total:
823
+ table.add_column("Total", justify="right")
824
+
825
+ # Add rows (only categories with non-zero values)
826
+ for cat in file_type_categories:
827
+ values = [kit_stats[kit_key].get(cat, 0) for kit_key in active_kits]
828
+ if sum(values) > 0: # Only show row if at least one kit has this type
829
+ styled_values = [style_value(v, not is_removal, has_modified_files) for v in values]
830
+ row = [cat.capitalize()] + styled_values
831
+ if show_total:
832
+ row.append(style_value(sum(values), not is_removal, has_modified_files))
833
+ table.add_row(*row)
834
+
835
+ console.print(table)
836
+ console.print()
837
+
838
+ # Totals table (Files and Directories)
839
+ console.print("[bold magenta]File Totals:[/bold magenta]")
840
+ table = Table(show_header=True, header_style="bold cyan", box=ROUNDED)
841
+ table.add_column("Category", style="cyan", no_wrap=True)
842
+
843
+ # Add kit columns
844
+ for kit_key in kit_stats.keys():
845
+ table.add_column(format_kit_name(kit_key), justify="right")
846
+
847
+ # Add Total column if multiple kits
848
+ show_total = len(kit_stats) > 1
849
+ if show_total:
850
+ table.add_column("Total", justify="right")
851
+
852
+ # Add rows (only if totals > 0)
853
+ for cat in ["files", "directories"]:
854
+ values = [kit_stats[kit_key].get(cat, 0) for kit_key in kit_stats.keys()]
855
+ if sum(values) > 0:
856
+ styled_values = [style_value(v, not is_removal, has_modified_files) for v in values]
857
+ row = [cat.capitalize()] + styled_values
858
+ if show_total:
859
+ row.append(style_value(sum(values), not is_removal, has_modified_files))
860
+ table.add_row(*row)
861
+
862
+ console.print(table)
863
+ console.print()
864
+
865
+ def _display_installation_summary(result: dict, verbose: bool = False):
866
+ """Display kit addition summary.
867
+
868
+ Args:
869
+ result: Install result dict with 'installed' and 'skipped' lists
870
+ verbose: If True, show full file list; if False, show only count
871
+ """
872
+ installed = result.get("installed", [])
873
+ skipped = result.get("skipped", [])
874
+
875
+ if verbose and installed:
876
+ console.print("[bold]\nInstalled files:[/bold]")
877
+ for item in installed:
878
+ # Normalize to backslashes for Windows display
879
+ display_path = str(item).replace("/", "\\")
880
+ console.print(f" [green]+[/green] {display_path}")
881
+
882
+ if verbose and skipped:
883
+ console.print("\n[bold]Skipped (already exists):[/bold]")
884
+ for item in skipped:
885
+ # Normalize to backslashes for Windows display
886
+ display_path = str(item).replace("/", "\\")
887
+ console.print(f" [dim]-[/dim] {display_path}")
888
+
889
+ # Summary count (always show)
890
+ if not verbose:
891
+ if installed:
892
+ console.print(f"\nInstalled {len(installed)} files")
893
+ if skipped:
894
+ console.print(f"Skipped {len(skipped)} files (already exist)")
415
895
 
416
896
  console.print("\n[bold cyan]Next steps:[/bold cyan]")
417
- console.print(f" 1. Run: /orient (in your AI assistant)")
418
- console.print(f" 2. Check: {MARKER_PROJECT_KIT} or .github/prompts/orient.prompt.md")
897
+ console.print(f" 1. Run: /orient (in GitHub Copilot or Claude Code)")
898
+ console.print(r" 2. Check: .github\prompts\orient.prompt.md or .claude\commands\orient.md")
419
899
  console.print(f" 3. Validate: {APP_NAME} validate")
900
+ console.print("\n[dim]Note: Commands are markdown prompt files that work with any compatible AI assistant.[/dim]")
901
+ console.print()
902
+
903
+ def _display_removal_summary(result: dict, verbose: bool = False):
904
+ """Display kit removal summary.
905
+
906
+ Args:
907
+ result: Removal result dict with 'removed' list of kit dicts
908
+ verbose: If True, show full file list; if False, show only count
909
+ """
910
+ # Flatten all removed files from all kits
911
+ all_removed = []
912
+ for kit_result in result.get("removed", []):
913
+ all_removed.extend(kit_result.get("files", []))
914
+
915
+ if verbose and all_removed:
916
+ console.print("\n[bold]Removed files:[/bold]")
917
+ for item in all_removed:
918
+ # Normalize to backslashes for Windows display
919
+ display_path = str(item).replace("/", "\\")
920
+ console.print(f" [red]-[/red] {display_path}")
921
+
922
+ # Summary count (always show)
923
+ if not verbose and all_removed:
924
+ console.print(f"\nRemoved {len(all_removed)} files")
925
+
926
+ def _display_validation_results(validation_result: dict):
927
+ """Display validation results with per-kit status and breakdown table.
928
+
929
+ Shows validation-specific information (file checks, missing files, integrity issues)
930
+ followed by the agent/shell breakdown table for kits that passed validation.
931
+
932
+ Args:
933
+ validation_result: Dict with 'valid' (bool), 'checks' (dict of kit results), and 'target_dir' (Path)
934
+ """
935
+ checks = validation_result.get("checks", {})
936
+ target_dir = validation_result.get("target_dir", Path.cwd())
937
+
938
+ # Show per-kit validation status with detailed issues
939
+ for kit_name, result in checks.items():
940
+ status = result.get("status", "unknown")
941
+
942
+ if status == "installed":
943
+ console.print(f"[green][OK] {kit_name}[/green]")
944
+ elif status == "not_installed":
945
+ console.print(f"[dim][-] {kit_name} (not installed)[/dim]")
946
+ elif status == "partial":
947
+ console.print(f"[yellow][!] {kit_name} (partial - issues found)[/yellow]")
948
+ # Show detailed issues
949
+ missing = result.get("missing_files", [])
950
+ corrupted = result.get("corrupted_files", [])
951
+ if missing:
952
+ console.print(f"[dim] Missing: {', '.join(missing[:3])}" + (" ..." if len(missing) > 3 else "") + "[/dim]")
953
+ if corrupted:
954
+ console.print(f"[dim] Corrupted: {', '.join(corrupted[:3])}" + (" ..." if len(corrupted) > 3 else "") + "[/dim]")
955
+ else:
956
+ console.print(f"[red][X] {kit_name} ({status})[/red]")
420
957
 
421
- def _display_validation_results(result: dict):
422
- """Display validation results."""
423
- for check_name, check_result in result.get("checks", {}).items():
424
- status = "[OK]" if check_result["passed"] else "[X]"
425
- color = "green" if check_result["passed"] else "red"
426
- console.print(f"[{color}]{status}[/{color}] {check_name}")
958
+ # Show agent/shell breakdown table for validated kits
959
+ validated_kits = [kit_name for kit_name, result in checks.items() if result.get("status") == "installed"]
427
960
 
428
- if not check_result["passed"] and "message" in check_result:
429
- console.print(f" {check_result['message']}", style="dim")
961
+ if validated_kits:
962
+ console.print()
963
+ table = _build_kit_breakdown_table(target_dir, validated_kits)
964
+ console.print(table)
965
+
966
+ def _cleanup_empty_directories(target_dir: Path):
967
+ """Clean up empty directories created by lite-kits."""
968
+ directories_to_check = [
969
+ DIR_CLAUDE_COMMANDS,
970
+ DIR_GITHUB_PROMPTS,
971
+ DIR_SPECIFY_MEMORY,
972
+ DIR_SPECIFY_SCRIPTS_BASH,
973
+ DIR_SPECIFY_SCRIPTS_POWERSHELL,
974
+ DIR_SPECIFY_TEMPLATES,
975
+ ]
976
+
977
+ cleaned = []
978
+ for dir_path in directories_to_check:
979
+ full_path = target_dir / dir_path
980
+ if full_path.exists() and full_path.is_dir():
981
+ # Check if directory is empty (no files or subdirs)
982
+ try:
983
+ if not any(full_path.iterdir()):
984
+ full_path.rmdir()
985
+ cleaned.append(dir_path)
986
+ except OSError:
987
+ # Directory not empty or permission error, skip
988
+ pass
989
+
990
+ if cleaned:
991
+ console.print(f"\nCleaned up empty directories: [dim]{', '.join(cleaned)}[/dim]")
430
992
 
431
993
  @app.command(name="info")
432
994
  def package_info():
433
- """Show package information and installation details."""
434
- # Show the static banner for visual appeal
435
- show_static_banner()
436
- console.print()
995
+ """Show package information and available kits.
437
996
 
438
- # Package info
997
+ Displays:
998
+ - Package version and repository
999
+ - Available kits (dev, multiagent)
1000
+ - Kit descriptions and commands
1001
+ - Package management commands
1002
+ """
1003
+ # Package info (banner removed to avoid duplication with --banner flag)
1004
+ console.print()
439
1005
  console.print("[bold]Info:[/bold]")
440
1006
  info_table = Table(show_header=False, box=None, padding=(0, 2))
441
1007
  info_table.add_column("Key", style="cyan")
442
1008
  info_table.add_column("Value")
443
1009
 
444
1010
  info_table.add_row("Version", __version__)
445
- info_table.add_row("Repository", "https://github.com/tmorgan181/lite-kits")
446
- info_table.add_row("License", "MIT")
1011
+ info_table.add_row("Repository", REPOSITORY_URL)
1012
+ info_table.add_row("License", LICENSE)
447
1013
 
448
1014
  console.print(info_table)
449
1015
  console.print()
@@ -453,11 +1019,10 @@ def package_info():
453
1019
  kits_table = Table(show_header=False, box=None, padding=(0, 2))
454
1020
  kits_table.add_column("Kit", style="cyan")
455
1021
  kits_table.add_column("Description")
456
-
457
- kits_table.add_row(KIT_PROJECT, KIT_DESC_PROJECT)
458
- kits_table.add_row(KIT_GIT, KIT_DESC_GIT)
1022
+
1023
+ kits_table.add_row(KIT_DEV, KIT_DESC_DEV)
459
1024
  kits_table.add_row(KIT_MULTIAGENT, KIT_DESC_MULTIAGENT)
460
-
1025
+
461
1026
  console.print(kits_table)
462
1027
  console.print()
463
1028
 
@@ -476,7 +1041,8 @@ def package_info():
476
1041
  @app.command(name="uninstall")
477
1042
  def package_uninstall():
478
1043
  """Instructions for uninstalling the lite-kits package."""
479
- console.print(f"\n[bold yellow]Uninstall {APP_NAME}[/bold yellow]\n")
1044
+ console.print()
1045
+ console.print(f"[bold yellow]Uninstall {APP_NAME}[/bold yellow]\n")
480
1046
 
481
1047
  console.print("To uninstall the package, run:\n")
482
1048
  console.print(f" [cyan]uv tool uninstall {APP_NAME}[/cyan]\n")
@@ -487,6 +1053,33 @@ def package_uninstall():
487
1053
  console.print("[bold]Note:[/bold] This will remove the package but NOT the kits you've added to projects.")
488
1054
  console.print(f"To remove kits from a project, first run: [cyan]{APP_NAME} remove --all[/cyan]\n")
489
1055
 
1056
+ @app.command(name="help")
1057
+ def show_help(
1058
+ ctx: typer.Context,
1059
+ command_name: Optional[str] = typer.Argument(
1060
+ None,
1061
+ help="Command to get help for (e.g., 'add', 'remove')",
1062
+ ),
1063
+ ):
1064
+ """Show help and available commands.
1065
+
1066
+ Usage:
1067
+ lite-kits help # Show general help
1068
+ lite-kits help add # Show help for 'add' command
1069
+ """
1070
+ if command_name:
1071
+ # Show help for specific command by invoking it with --help
1072
+ import sys
1073
+ sys.argv = [APP_NAME, command_name, "--help"]
1074
+ try:
1075
+ app()
1076
+ except SystemExit:
1077
+ pass
1078
+ else:
1079
+ # Show general help
1080
+ console.print(ctx.parent.get_help())
1081
+ raise typer.Exit(0)
1082
+
490
1083
  @app.command(name="banner", hidden=True)
491
1084
  def show_banner():
492
1085
  """Show the lite-kits banner (hidden easter egg command)."""