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
@@ -0,0 +1,19 @@
|
|
1
|
+
"""Core modules for lite-kits."""
|
2
|
+
|
3
|
+
from .banner import diagonal_reveal_banner, show_loading_spinner, show_static_banner
|
4
|
+
from .conflict_checker import ConflictChecker
|
5
|
+
from .detector import Detector
|
6
|
+
from .installer import Installer
|
7
|
+
from .manifest import KitManifest
|
8
|
+
from .validator import Validator
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"diagonal_reveal_banner",
|
12
|
+
"show_loading_spinner",
|
13
|
+
"show_static_banner",
|
14
|
+
"ConflictChecker",
|
15
|
+
"Detector",
|
16
|
+
"Installer",
|
17
|
+
"KitManifest",
|
18
|
+
"Validator",
|
19
|
+
]
|
lite_kits/core/banner.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.text import Text
|
3
|
+
from rich.live import Live
|
4
|
+
import time
|
5
|
+
import sys
|
6
|
+
|
7
|
+
console = Console()
|
8
|
+
|
9
|
+
BANNER = """
|
10
|
+
██╗ ██╗████████╗███████╗ ██╗ ██╗██╗████████╗███████╗
|
11
|
+
██║ ██║╚══██╔══╝██╔════╝ ██║ ██╔╝██║╚══██╔══╝██╔════╝
|
12
|
+
██║ ██║ ██║ █████╗ █████╗█████╔╝ ██║ ██║ ███████╗
|
13
|
+
██║ ██║ ██║ ██╔══╝ ╚════╝██╔═██╗ ██║ ██║ ╚════██║
|
14
|
+
███████╗██║ ██║ ███████╗ ██║ ██╗██║ ██║ ███████║
|
15
|
+
╚══════╝╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝
|
16
|
+
"""
|
17
|
+
|
18
|
+
TAGLINE = "Lightweight enhancement kits for spec-driven development."
|
19
|
+
|
20
|
+
RAINBOW_STOPS = [
|
21
|
+
(255, 0, 0), # Red
|
22
|
+
(255, 127, 0), # Orange
|
23
|
+
(255, 255, 0), # Yellow
|
24
|
+
(0, 255, 0), # Green
|
25
|
+
(0, 0, 255), # Blue
|
26
|
+
(75, 0, 130), # Indigo
|
27
|
+
(148, 0, 211), # Violet
|
28
|
+
(255, 105, 180), # Pink
|
29
|
+
]
|
30
|
+
|
31
|
+
def interpolate_multi_color(stops, steps):
|
32
|
+
gradient = []
|
33
|
+
n_segments = len(stops) - 1
|
34
|
+
steps_per_segment = steps // n_segments
|
35
|
+
for i in range(n_segments):
|
36
|
+
start = stops[i]
|
37
|
+
end = stops[i+1]
|
38
|
+
for j in range(steps_per_segment):
|
39
|
+
t = j / steps_per_segment
|
40
|
+
r = int(start[0] + (end[0] - start[0]) * t)
|
41
|
+
g = int(start[1] + (end[1] - start[1]) * t)
|
42
|
+
b = int(start[2] + (end[2] - start[2]) * t)
|
43
|
+
gradient.append(f"#{r:02X}{g:02X}{b:02X}")
|
44
|
+
gradient.append(f"#{stops[-1][0]:02X}{stops[-1][1]:02X}{stops[-1][2]:02X}")
|
45
|
+
return gradient
|
46
|
+
|
47
|
+
def get_diagonal_steps(text=BANNER):
|
48
|
+
lines = text.strip().split('\n')
|
49
|
+
height = len(lines)
|
50
|
+
width = max(len(line) for line in lines)
|
51
|
+
return height + width - 2
|
52
|
+
|
53
|
+
def apply_diagonal_gradient(text=BANNER, offset=0, steps_override=None):
|
54
|
+
lines = text.strip().split('\n')
|
55
|
+
height = len(lines)
|
56
|
+
width = max(len(line) for line in lines)
|
57
|
+
steps = steps_override if steps_override else height + width - 2
|
58
|
+
gradient = interpolate_multi_color(RAINBOW_STOPS, steps + 1)
|
59
|
+
result = Text()
|
60
|
+
for line_idx, line in enumerate(lines):
|
61
|
+
for char_idx, char in enumerate(line):
|
62
|
+
diag_idx = line_idx + char_idx + offset
|
63
|
+
color_idx = min(diag_idx, len(gradient) - 1)
|
64
|
+
color = gradient[color_idx]
|
65
|
+
result.append(char, style=f"bold {color}")
|
66
|
+
result.append('\n')
|
67
|
+
return result
|
68
|
+
|
69
|
+
def typewriter_effect(text=TAGLINE, delay=0.02, cursor_blink_rate=0.4, blink_cycles=2):
|
70
|
+
"""Display retro terminal typewriter animation with dim text via ANSI codes."""
|
71
|
+
DIM = '\033[2m'
|
72
|
+
RESET = '\033[0m'
|
73
|
+
for i in range(len(text) + 1):
|
74
|
+
sys.stdout.write('\r' + DIM + text[:i] + RESET)
|
75
|
+
sys.stdout.flush()
|
76
|
+
time.sleep(delay)
|
77
|
+
for _ in range(blink_cycles):
|
78
|
+
sys.stdout.write('\r' + DIM + text + '█' + RESET)
|
79
|
+
sys.stdout.flush()
|
80
|
+
time.sleep(cursor_blink_rate)
|
81
|
+
sys.stdout.write('\r' + DIM + text + ' ' + RESET)
|
82
|
+
sys.stdout.flush()
|
83
|
+
time.sleep(cursor_blink_rate)
|
84
|
+
sys.stdout.write('\n')
|
85
|
+
|
86
|
+
|
87
|
+
def diagonal_reveal_banner(text=BANNER, steps_override=None, fps=56):
|
88
|
+
"""Reveal the banner diagonally from top-left to bottom-right, with gradient following the reveal."""
|
89
|
+
console.print()
|
90
|
+
lines = text.strip().split('\n')
|
91
|
+
height = len(lines)
|
92
|
+
width = max(len(line) for line in lines)
|
93
|
+
steps = steps_override if steps_override else height + width - 2
|
94
|
+
gradient = interpolate_multi_color(RAINBOW_STOPS, steps + 1)
|
95
|
+
|
96
|
+
# Prepare a matrix of characters and their diagonal indices
|
97
|
+
char_matrix = []
|
98
|
+
diag_indices = []
|
99
|
+
for line_idx, line in enumerate(lines):
|
100
|
+
row = []
|
101
|
+
diag_row = []
|
102
|
+
for char_idx, char in enumerate(line):
|
103
|
+
row.append(char)
|
104
|
+
diag_row.append(line_idx + char_idx)
|
105
|
+
char_matrix.append(row)
|
106
|
+
diag_indices.append(diag_row)
|
107
|
+
|
108
|
+
# Reveal animation
|
109
|
+
try:
|
110
|
+
with Live(console=console, refresh_per_second=fps, transient=True) as live:
|
111
|
+
for reveal_diag in range(steps + 1):
|
112
|
+
result = Text()
|
113
|
+
for line_idx, row in enumerate(char_matrix):
|
114
|
+
for char_idx, char in enumerate(row):
|
115
|
+
diag_idx = diag_indices[line_idx][char_idx]
|
116
|
+
if diag_idx <= reveal_diag:
|
117
|
+
color = gradient[min(diag_idx, len(gradient)-1)]
|
118
|
+
result.append(char, style=f"bold {color}")
|
119
|
+
else:
|
120
|
+
result.append(" ")
|
121
|
+
result.append('\n')
|
122
|
+
live.update(result)
|
123
|
+
time.sleep(1.0 / fps)
|
124
|
+
except KeyboardInterrupt:
|
125
|
+
pass
|
126
|
+
|
127
|
+
# Show final static gradient
|
128
|
+
result = Text()
|
129
|
+
for line_idx, row in enumerate(char_matrix):
|
130
|
+
for char_idx, char in enumerate(row):
|
131
|
+
diag_idx = diag_indices[line_idx][char_idx]
|
132
|
+
color = gradient[min(diag_idx, len(gradient)-1)]
|
133
|
+
result.append(char, style=f"bold {color}")
|
134
|
+
result.append('\n')
|
135
|
+
console.print(result)
|
136
|
+
typewriter_effect()
|
137
|
+
|
138
|
+
def show_static_banner():
|
139
|
+
console.print()
|
140
|
+
steps = get_diagonal_steps()
|
141
|
+
gradient_text = apply_diagonal_gradient(offset=0, steps_override=steps)
|
142
|
+
console.print(gradient_text)
|
143
|
+
console.print(f"{TAGLINE}", style="dim")
|
144
|
+
|
145
|
+
def show_loading_spinner(message="Loading kits..."):
|
146
|
+
with console.status(f"[bold bright_cyan]{message}", spinner="dots"):
|
147
|
+
time.sleep(1.5)
|
148
|
+
console.print("[green][OK] Done![/green]")
|
149
|
+
|
150
|
+
if __name__ == "__main__":
|
151
|
+
console.clear()
|
152
|
+
console.print("[bold yellow]\nDemo 1: Loading Spinner[/bold yellow]\n")
|
153
|
+
show_loading_spinner()
|
154
|
+
time.sleep(1)
|
155
|
+
console.print("[bold yellow]Demo 2: Diagonal Reveal Animation[/bold yellow]\n")
|
156
|
+
diagonal_reveal_banner()
|
157
|
+
time.sleep(1)
|
158
|
+
console.print("[bold yellow]Demo 3: Status Banner[/bold yellow]\n")
|
159
|
+
show_static_banner()
|
160
|
+
console.print()
|
@@ -0,0 +1,115 @@
|
|
1
|
+
"""
|
2
|
+
File conflict detection for installer.
|
3
|
+
|
4
|
+
Checks for existing files that would be overwritten.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Dict, List
|
9
|
+
|
10
|
+
from .manifest import KitManifest
|
11
|
+
|
12
|
+
|
13
|
+
class ConflictChecker:
|
14
|
+
"""Detects file conflicts before installation."""
|
15
|
+
|
16
|
+
def __init__(self, target_dir: Path, kits_dir: Path, manifest: KitManifest):
|
17
|
+
"""
|
18
|
+
Initialize conflict checker.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
target_dir: Target project directory
|
22
|
+
kits_dir: Kits source directory
|
23
|
+
manifest: Loaded kit manifest
|
24
|
+
"""
|
25
|
+
self.target_dir = target_dir
|
26
|
+
self.kits_dir = kits_dir
|
27
|
+
self.manifest = manifest
|
28
|
+
|
29
|
+
def check_conflicts(
|
30
|
+
self,
|
31
|
+
kits: List[str],
|
32
|
+
agents: List[str],
|
33
|
+
shells: List[str]
|
34
|
+
) -> Dict:
|
35
|
+
"""
|
36
|
+
Check for file conflicts.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
kits: List of kit names to check
|
40
|
+
agents: List of agent names
|
41
|
+
shells: List of shell names
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
Dict with conflict details
|
45
|
+
"""
|
46
|
+
result = {
|
47
|
+
'conflicts': [],
|
48
|
+
'overwrites': [],
|
49
|
+
'safe': [],
|
50
|
+
'has_conflicts': False
|
51
|
+
}
|
52
|
+
|
53
|
+
for kit_name in kits:
|
54
|
+
# Check agent files
|
55
|
+
for agent in agents:
|
56
|
+
self._check_file_group(kit_name, agent, result)
|
57
|
+
|
58
|
+
# Check shell files
|
59
|
+
for shell in shells:
|
60
|
+
self._check_file_group(kit_name, shell, result)
|
61
|
+
|
62
|
+
# Check agent-agnostic files
|
63
|
+
all_files = self.manifest.get_kit_files(kit_name, agent=None)
|
64
|
+
for file_info in all_files:
|
65
|
+
# Skip agent/shell-specific
|
66
|
+
if file_info.get('type') in ['command', 'prompt', 'script']:
|
67
|
+
continue
|
68
|
+
|
69
|
+
self._check_file(file_info, result)
|
70
|
+
|
71
|
+
result['has_conflicts'] = len(result['conflicts']) > 0
|
72
|
+
return result
|
73
|
+
|
74
|
+
def _check_file_group(self, kit_name: str, agent_or_shell: str, result: Dict):
|
75
|
+
"""Check a group of files for an agent/shell."""
|
76
|
+
files = self.manifest.get_kit_files(kit_name, agent=agent_or_shell)
|
77
|
+
|
78
|
+
for file_info in files:
|
79
|
+
if file_info.get('status') == 'planned':
|
80
|
+
continue
|
81
|
+
|
82
|
+
self._check_file(file_info, result)
|
83
|
+
|
84
|
+
def _check_file(self, file_info: Dict, result: Dict):
|
85
|
+
"""Check a single file for conflicts."""
|
86
|
+
target_path = self.target_dir / file_info['path']
|
87
|
+
|
88
|
+
if not target_path.exists():
|
89
|
+
if file_info['path'] not in result['safe']:
|
90
|
+
result['safe'].append(file_info['path'])
|
91
|
+
return
|
92
|
+
|
93
|
+
# File exists, check if content differs
|
94
|
+
source_path = self.kits_dir / file_info['source']
|
95
|
+
|
96
|
+
if not source_path.exists():
|
97
|
+
return
|
98
|
+
|
99
|
+
try:
|
100
|
+
source_content = source_path.read_text(encoding='utf-8')
|
101
|
+
target_content = target_path.read_text(encoding='utf-8')
|
102
|
+
|
103
|
+
if source_content != target_content:
|
104
|
+
if file_info['path'] not in result['conflicts']:
|
105
|
+
result['conflicts'].append(file_info['path'])
|
106
|
+
result['overwrites'].append({
|
107
|
+
'path': file_info['path'],
|
108
|
+
'source': file_info['source'],
|
109
|
+
'size_current': target_path.stat().st_size,
|
110
|
+
'size_new': source_path.stat().st_size,
|
111
|
+
})
|
112
|
+
except Exception:
|
113
|
+
# If can't read/compare, treat as conflict
|
114
|
+
if file_info['path'] not in result['conflicts']:
|
115
|
+
result['conflicts'].append(file_info['path'])
|
@@ -0,0 +1,140 @@
|
|
1
|
+
"""
|
2
|
+
Agent and shell detection for lite-kits installer.
|
3
|
+
|
4
|
+
Detects which AI agents and shell environments are present in a project.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import List, Optional
|
9
|
+
|
10
|
+
from .manifest import KitManifest
|
11
|
+
|
12
|
+
|
13
|
+
class Detector:
|
14
|
+
"""Detects agents and shells in target project."""
|
15
|
+
|
16
|
+
def __init__(self, target_dir: Path, manifest: KitManifest):
|
17
|
+
"""
|
18
|
+
Initialize detector.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
target_dir: Target project directory
|
22
|
+
manifest: Loaded kit manifest
|
23
|
+
"""
|
24
|
+
self.target_dir = target_dir
|
25
|
+
self.manifest = manifest
|
26
|
+
|
27
|
+
def detect_agents(self, preferred: Optional[str] = None) -> List[str]:
|
28
|
+
"""
|
29
|
+
Auto-detect which AI agents are present.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
preferred: Explicit agent preference (overrides auto-detection)
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
List of agent names sorted by priority
|
36
|
+
"""
|
37
|
+
# If explicit preference, validate and return
|
38
|
+
if preferred:
|
39
|
+
config = self.manifest.get_agent_config(preferred)
|
40
|
+
if not config:
|
41
|
+
raise ValueError(f"Unknown agent: {preferred}")
|
42
|
+
if not config.get('supported', False):
|
43
|
+
raise ValueError(f"Agent not supported: {preferred}")
|
44
|
+
return [preferred]
|
45
|
+
|
46
|
+
# Auto-detect from manifest
|
47
|
+
detected = []
|
48
|
+
agents = self.manifest.manifest.get('agents', {})
|
49
|
+
|
50
|
+
for agent_name, config in agents.items():
|
51
|
+
if not config.get('supported', False):
|
52
|
+
continue
|
53
|
+
|
54
|
+
marker_dir = self.target_dir / config['marker_dir']
|
55
|
+
# Check if marker dir exists OR its parent exists (for nested dirs like .github/prompts)
|
56
|
+
# This allows detection even if subdirectory doesn't exist yet (will be created on install)
|
57
|
+
parent_dir = marker_dir.parent
|
58
|
+
if marker_dir.exists() or (parent_dir != self.target_dir and parent_dir.exists()):
|
59
|
+
detected.append({
|
60
|
+
'name': agent_name,
|
61
|
+
'priority': config.get('priority', 999)
|
62
|
+
})
|
63
|
+
|
64
|
+
# Sort by priority (lower = higher)
|
65
|
+
detected.sort(key=lambda x: x['priority'])
|
66
|
+
return [agent['name'] for agent in detected]
|
67
|
+
|
68
|
+
def detect_shells(self, preferred: Optional[str] = None) -> List[str]:
|
69
|
+
"""
|
70
|
+
Determine which shells to install for.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
preferred: Explicit shell preference (overrides auto-detection)
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
List of shell names
|
77
|
+
"""
|
78
|
+
# If explicit preference, validate and return
|
79
|
+
if preferred:
|
80
|
+
config = self.manifest.manifest.get('shells', {}).get(preferred)
|
81
|
+
if not config:
|
82
|
+
raise ValueError(f"Unknown shell: {preferred}")
|
83
|
+
if not config.get('supported', False):
|
84
|
+
raise ValueError(f"Shell not supported: {preferred}")
|
85
|
+
return [preferred]
|
86
|
+
|
87
|
+
# Check if shell detection is enabled
|
88
|
+
options = self.manifest.manifest.get('options', {})
|
89
|
+
if not options.get('auto_detect_shells', True):
|
90
|
+
return []
|
91
|
+
|
92
|
+
# Get all supported shells
|
93
|
+
shells_config = self.manifest.manifest.get('shells', {})
|
94
|
+
detected = []
|
95
|
+
|
96
|
+
for shell_name, config in shells_config.items():
|
97
|
+
if not config.get('supported', False):
|
98
|
+
continue
|
99
|
+
|
100
|
+
detected.append({
|
101
|
+
'name': shell_name,
|
102
|
+
'priority': config.get('priority', 999)
|
103
|
+
})
|
104
|
+
|
105
|
+
# Sort by priority
|
106
|
+
detected.sort(key=lambda x: x['priority'])
|
107
|
+
|
108
|
+
# Return all or just primary based on options
|
109
|
+
if options.get('prefer_all_shells', False):
|
110
|
+
return [shell['name'] for shell in detected]
|
111
|
+
|
112
|
+
return [detected[0]['name']] if detected else []
|
113
|
+
|
114
|
+
def is_spec_kit_project(self) -> bool:
|
115
|
+
"""
|
116
|
+
Check if target is a spec-kit project.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
True if spec-kit markers found
|
120
|
+
"""
|
121
|
+
spec_config = self.manifest.manifest.get('spec_kit', {})
|
122
|
+
markers = spec_config.get('markers', [])
|
123
|
+
require_any = spec_config.get('require_any', True)
|
124
|
+
|
125
|
+
found = []
|
126
|
+
for marker in markers:
|
127
|
+
path = self.target_dir / marker['path']
|
128
|
+
|
129
|
+
if marker.get('type') == 'directory':
|
130
|
+
if path.is_dir():
|
131
|
+
found.append(marker['path'])
|
132
|
+
else:
|
133
|
+
if path.exists():
|
134
|
+
found.append(marker['path'])
|
135
|
+
|
136
|
+
# Check requirement
|
137
|
+
if require_any:
|
138
|
+
return len(found) > 0
|
139
|
+
|
140
|
+
return len(found) == len(markers)
|