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.
- lite_kits/__init__.py +56 -4
- lite_kits/cli.py +782 -189
- lite_kits/core/__init__.py +6 -0
- lite_kits/core/banner.py +1 -1
- lite_kits/core/conflict_checker.py +115 -0
- lite_kits/core/detector.py +177 -0
- lite_kits/core/installer.py +242 -351
- lite_kits/core/manifest.py +146 -146
- lite_kits/core/validator.py +183 -0
- lite_kits/kits/README.md +6 -6
- lite_kits/kits/dev/README.md +241 -241
- lite_kits/kits/dev/{claude/commands → commands/.claude}/audit.md +143 -143
- lite_kits/kits/dev/{claude/commands → commands/.claude}/cleanup.md +2 -2
- lite_kits/kits/{git/claude/commands → dev/commands/.claude}/commit.md +2 -2
- lite_kits/kits/{project/claude/commands → dev/commands/.claude}/orient.md +3 -4
- lite_kits/kits/{git/claude/commands → dev/commands/.claude}/pr.md +1 -1
- lite_kits/kits/{git/claude/commands → dev/commands/.claude}/review.md +202 -202
- lite_kits/kits/{project/claude/commands → dev/commands/.claude}/stats.md +162 -162
- lite_kits/kits/{project/github/prompts → dev/commands/.github}/audit.prompt.md +143 -143
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/cleanup.prompt.md +2 -2
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/commit.prompt.md +2 -2
- lite_kits/kits/dev/{github/prompts → commands/.github}/orient.prompt.md +3 -4
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/pr.prompt.md +1 -1
- lite_kits/kits/{git/github/prompts → dev/commands/.github}/review.prompt.md +202 -202
- lite_kits/kits/dev/{github/prompts → commands/.github}/stats.prompt.md +163 -163
- lite_kits/kits/kits.yaml +497 -180
- lite_kits/kits/multiagent/README.md +6 -6
- lite_kits/kits/multiagent/{claude/commands → commands/.claude}/sync.md +331 -331
- lite_kits/kits/multiagent/{github/prompts → commands/.github}/sync.prompt.md +73 -69
- lite_kits/kits/multiagent/memory/git-worktrees-protocol.md +370 -370
- lite_kits/kits/multiagent/memory/parallel-work-protocol.md +536 -536
- lite_kits/kits/multiagent/memory/pr-workflow-guide.md +275 -275
- lite_kits/kits/multiagent/templates/collaboration-structure/README.md +166 -166
- lite_kits/kits/multiagent/templates/decision.md +79 -79
- lite_kits/kits/multiagent/templates/handoff.md +95 -95
- lite_kits/kits/multiagent/templates/session-log.md +68 -68
- lite_kits-0.3.2.dist-info/METADATA +259 -0
- lite_kits-0.3.2.dist-info/RECORD +41 -0
- {lite_kits-0.1.1.dist-info → lite_kits-0.3.2.dist-info}/licenses/LICENSE +21 -21
- lite_kits/kits/dev/claude/commands/commit.md +0 -612
- lite_kits/kits/dev/claude/commands/orient.md +0 -146
- lite_kits/kits/dev/claude/commands/pr.md +0 -593
- lite_kits/kits/dev/claude/commands/review.md +0 -202
- lite_kits/kits/dev/claude/commands/stats.md +0 -162
- lite_kits/kits/dev/github/prompts/audit.prompt.md +0 -143
- lite_kits/kits/dev/github/prompts/cleanup.prompt.md +0 -382
- lite_kits/kits/dev/github/prompts/commit.prompt.md +0 -591
- lite_kits/kits/dev/github/prompts/pr.prompt.md +0 -603
- lite_kits/kits/dev/github/prompts/review.prompt.md +0 -202
- lite_kits/kits/git/README.md +0 -365
- lite_kits/kits/git/claude/commands/cleanup.md +0 -361
- lite_kits/kits/git/scripts/bash/get-git-context.sh +0 -208
- lite_kits/kits/git/scripts/powershell/Get-GitContext.ps1 +0 -242
- lite_kits/kits/project/README.md +0 -228
- lite_kits/kits/project/claude/commands/audit.md +0 -143
- lite_kits/kits/project/claude/commands/review.md +0 -112
- lite_kits/kits/project/github/prompts/orient.prompt.md +0 -150
- lite_kits/kits/project/github/prompts/review.prompt.md +0 -112
- lite_kits/kits/project/github/prompts/stats.prompt.md +0 -163
- lite_kits-0.1.1.dist-info/METADATA +0 -447
- lite_kits-0.1.1.dist-info/RECORD +0 -58
- {lite_kits-0.1.1.dist-info → lite_kits-0.3.2.dist-info}/WHEEL +0 -0
- {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
|
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]
|
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
|
62
|
-
console.print(f" [cyan]2. {APP_NAME} status[/cyan]
|
63
|
-
console.print(f" [cyan]3. {APP_NAME}
|
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
|
-
|
72
|
-
|
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}
|
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
|
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
|
-
|
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
|
-
|
229
|
+
all_kits: bool = typer.Option(
|
157
230
|
False,
|
158
|
-
"--
|
159
|
-
help=
|
231
|
+
"--all",
|
232
|
+
help="Add all kits",
|
160
233
|
),
|
161
|
-
|
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
|
-
"--
|
164
|
-
|
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
|
177
|
-
kits =
|
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
|
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(
|
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
|
-
|
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
|
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
|
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
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
218
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
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
|
252
|
-
lite-kits remove --kit
|
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 --
|
267
|
-
console.print(f" {APP_NAME} remove --
|
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
|
-
#
|
277
|
-
if
|
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
|
-
#
|
282
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
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
|
291
|
-
|
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
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
-
-
|
317
|
-
-
|
318
|
-
-
|
319
|
-
-
|
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
|
-
|
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
|
-
|
508
|
+
print_spec_kit_error()
|
334
509
|
raise typer.Exit(1)
|
335
510
|
|
336
511
|
# Check if any kits are installed
|
337
|
-
|
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
|
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
|
-
|
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
|
539
|
+
"""Show enhancement kit installation status.
|
361
540
|
|
362
541
|
Displays:
|
363
|
-
-
|
364
|
-
-
|
365
|
-
-
|
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
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
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
|
397
|
-
"""
|
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
|
-
|
403
|
-
|
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
|
-
|
407
|
-
|
408
|
-
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
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
|
418
|
-
console.print(
|
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
|
-
|
422
|
-
""
|
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
|
-
|
429
|
-
|
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
|
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
|
-
|
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",
|
446
|
-
info_table.add_row("License",
|
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(
|
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(
|
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)."""
|