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