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.
Files changed (48) hide show
  1. lite_kits/__init__.py +61 -9
  2. lite_kits/cli.py +788 -262
  3. lite_kits/core/__init__.py +19 -0
  4. lite_kits/core/banner.py +160 -0
  5. lite_kits/core/conflict_checker.py +115 -0
  6. lite_kits/core/detector.py +140 -0
  7. lite_kits/core/installer.py +322 -0
  8. lite_kits/core/manifest.py +146 -0
  9. lite_kits/core/validator.py +146 -0
  10. lite_kits/kits/README.md +14 -15
  11. lite_kits/kits/dev/README.md +241 -0
  12. lite_kits/kits/dev/commands/.claude/audit.md +143 -0
  13. lite_kits/kits/{git/claude/commands → dev/commands/.claude}/cleanup.md +2 -2
  14. lite_kits/kits/{git/claude/commands → dev/commands/.claude}/commit.md +2 -2
  15. lite_kits/kits/{project/claude/commands → dev/commands/.claude}/orient.md +30 -48
  16. lite_kits/kits/{git/claude/commands → dev/commands/.claude}/pr.md +1 -1
  17. lite_kits/kits/dev/commands/.claude/review.md +202 -0
  18. lite_kits/kits/dev/commands/.claude/stats.md +162 -0
  19. lite_kits/kits/dev/commands/.github/audit.prompt.md +143 -0
  20. lite_kits/kits/{git/github/prompts → dev/commands/.github}/cleanup.prompt.md +2 -2
  21. lite_kits/kits/{git/github/prompts → dev/commands/.github}/commit.prompt.md +2 -2
  22. lite_kits/kits/{project/github/prompts → dev/commands/.github}/orient.prompt.md +34 -48
  23. lite_kits/kits/{git/github/prompts → dev/commands/.github}/pr.prompt.md +1 -1
  24. lite_kits/kits/dev/commands/.github/review.prompt.md +202 -0
  25. lite_kits/kits/dev/commands/.github/stats.prompt.md +163 -0
  26. lite_kits/kits/kits.yaml +497 -0
  27. lite_kits/kits/multiagent/README.md +28 -17
  28. lite_kits/kits/multiagent/{claude/commands → commands/.claude}/sync.md +331 -331
  29. lite_kits/kits/multiagent/{github/prompts → commands/.github}/sync.prompt.md +73 -69
  30. lite_kits/kits/multiagent/memory/git-worktrees-protocol.md +370 -370
  31. lite_kits/kits/multiagent/memory/parallel-work-protocol.md +536 -536
  32. lite_kits/kits/multiagent/memory/pr-workflow-guide.md +275 -281
  33. lite_kits/kits/multiagent/templates/collaboration-structure/README.md +166 -166
  34. lite_kits/kits/multiagent/templates/decision.md +79 -79
  35. lite_kits/kits/multiagent/templates/handoff.md +95 -95
  36. lite_kits/kits/multiagent/templates/session-log.md +68 -68
  37. lite_kits-0.3.1.dist-info/METADATA +259 -0
  38. lite_kits-0.3.1.dist-info/RECORD +41 -0
  39. {lite_kits-0.1.0.dist-info → lite_kits-0.3.1.dist-info}/licenses/LICENSE +21 -21
  40. lite_kits/installer.py +0 -417
  41. lite_kits/kits/git/README.md +0 -374
  42. lite_kits/kits/git/scripts/bash/get-git-context.sh +0 -208
  43. lite_kits/kits/git/scripts/powershell/Get-GitContext.ps1 +0 -242
  44. lite_kits/kits/project/README.md +0 -244
  45. lite_kits-0.1.0.dist-info/METADATA +0 -415
  46. lite_kits-0.1.0.dist-info/RECORD +0 -31
  47. {lite_kits-0.1.0.dist-info → lite_kits-0.3.1.dist-info}/WHEEL +0 -0
  48. {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
+ ]
@@ -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)