up-cli 0.1.0__py3-none-any.whl → 0.2.0__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.
- up/cli.py +27 -1
- up/commands/dashboard.py +248 -0
- up/commands/learn.py +381 -0
- up/commands/new.py +108 -10
- up/commands/start.py +414 -0
- up/commands/status.py +205 -0
- up/commands/summarize.py +122 -0
- up/context.py +367 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +480 -21
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up_cli-0.2.0.dist-info/METADATA +374 -0
- up_cli-0.2.0.dist-info/RECORD +23 -0
- up_cli-0.1.0.dist-info/METADATA +0 -186
- up_cli-0.1.0.dist-info/RECORD +0 -14
- {up_cli-0.1.0.dist-info → up_cli-0.2.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.0.dist-info → up_cli-0.2.0.dist-info}/entry_points.txt +0 -0
up/cli.py
CHANGED
|
@@ -5,23 +5,49 @@ from rich.console import Console
|
|
|
5
5
|
|
|
6
6
|
from up.commands.init import init_cmd
|
|
7
7
|
from up.commands.new import new_cmd
|
|
8
|
+
from up.commands.status import status_cmd
|
|
9
|
+
from up.commands.learn import learn_cmd
|
|
10
|
+
from up.commands.summarize import summarize_cmd
|
|
11
|
+
from up.commands.dashboard import dashboard_cmd
|
|
12
|
+
from up.commands.start import start_cmd
|
|
8
13
|
|
|
9
14
|
console = Console()
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
@click.group()
|
|
13
|
-
@click.version_option(version="0.
|
|
18
|
+
@click.version_option(version="0.2.0", prog_name="up")
|
|
14
19
|
def main():
|
|
15
20
|
"""up - AI-powered project scaffolding.
|
|
16
21
|
|
|
17
22
|
Create projects with built-in docs, learn, and product-loop systems
|
|
18
23
|
for Claude Code and Cursor AI.
|
|
24
|
+
|
|
25
|
+
\b
|
|
26
|
+
Quick Start:
|
|
27
|
+
up new my-project Create new project
|
|
28
|
+
up init Initialize in existing project
|
|
29
|
+
up start Start the product loop
|
|
30
|
+
up status Show system health
|
|
31
|
+
up dashboard Live health dashboard
|
|
32
|
+
up learn auto Analyze project for improvements
|
|
33
|
+
up summarize Summarize AI conversations
|
|
34
|
+
|
|
35
|
+
\b
|
|
36
|
+
Project Templates:
|
|
37
|
+
up new api --template fastapi FastAPI backend
|
|
38
|
+
up new app --template nextjs Next.js frontend
|
|
39
|
+
up new lib --template python-lib Python library
|
|
19
40
|
"""
|
|
20
41
|
pass
|
|
21
42
|
|
|
22
43
|
|
|
23
44
|
main.add_command(init_cmd, name="init")
|
|
24
45
|
main.add_command(new_cmd, name="new")
|
|
46
|
+
main.add_command(start_cmd, name="start")
|
|
47
|
+
main.add_command(status_cmd, name="status")
|
|
48
|
+
main.add_command(dashboard_cmd, name="dashboard")
|
|
49
|
+
main.add_command(learn_cmd, name="learn")
|
|
50
|
+
main.add_command(summarize_cmd, name="summarize")
|
|
25
51
|
|
|
26
52
|
|
|
27
53
|
if __name__ == "__main__":
|
up/commands/dashboard.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""up dashboard - Interactive health dashboard."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.layout import Layout
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command()
|
|
19
|
+
@click.option("--refresh", "-r", default=5, help="Refresh interval in seconds")
|
|
20
|
+
@click.option("--once", is_flag=True, help="Show once without refresh")
|
|
21
|
+
def dashboard_cmd(refresh: int, once: bool):
|
|
22
|
+
"""Show interactive health dashboard.
|
|
23
|
+
|
|
24
|
+
Displays real-time status of all up systems:
|
|
25
|
+
- Context budget usage
|
|
26
|
+
- Circuit breaker states
|
|
27
|
+
- Product loop progress
|
|
28
|
+
- Recent activity
|
|
29
|
+
"""
|
|
30
|
+
if once:
|
|
31
|
+
dashboard = create_dashboard(Path.cwd())
|
|
32
|
+
console.print(dashboard)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with Live(create_dashboard(Path.cwd()), refresh_per_second=1, console=console) as live:
|
|
37
|
+
while True:
|
|
38
|
+
time.sleep(refresh)
|
|
39
|
+
live.update(create_dashboard(Path.cwd()))
|
|
40
|
+
except KeyboardInterrupt:
|
|
41
|
+
console.print("\n[dim]Dashboard stopped[/]")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_dashboard(workspace: Path) -> Panel:
|
|
45
|
+
"""Create the dashboard layout."""
|
|
46
|
+
layout = Layout()
|
|
47
|
+
|
|
48
|
+
layout.split_column(
|
|
49
|
+
Layout(name="header", size=3),
|
|
50
|
+
Layout(name="main"),
|
|
51
|
+
Layout(name="footer", size=3),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
layout["main"].split_row(
|
|
55
|
+
Layout(name="left"),
|
|
56
|
+
Layout(name="right"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Header
|
|
60
|
+
layout["header"].update(Panel(
|
|
61
|
+
Text("UP-CLI Health Dashboard", style="bold white", justify="center"),
|
|
62
|
+
style="blue"
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
# Left side - Status panels
|
|
66
|
+
left_content = Layout()
|
|
67
|
+
left_content.split_column(
|
|
68
|
+
Layout(create_context_panel(workspace), name="context"),
|
|
69
|
+
Layout(create_circuit_panel(workspace), name="circuit"),
|
|
70
|
+
)
|
|
71
|
+
layout["left"].update(left_content)
|
|
72
|
+
|
|
73
|
+
# Right side - Progress and activity
|
|
74
|
+
right_content = Layout()
|
|
75
|
+
right_content.split_column(
|
|
76
|
+
Layout(create_progress_panel(workspace), name="progress"),
|
|
77
|
+
Layout(create_skills_panel(workspace), name="skills"),
|
|
78
|
+
)
|
|
79
|
+
layout["right"].update(right_content)
|
|
80
|
+
|
|
81
|
+
# Footer
|
|
82
|
+
layout["footer"].update(Panel(
|
|
83
|
+
Text("Press Ctrl+C to exit | Refreshing every 5s", style="dim", justify="center"),
|
|
84
|
+
style="dim"
|
|
85
|
+
))
|
|
86
|
+
|
|
87
|
+
return Panel(layout, title="[bold]up-cli[/]", border_style="blue")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_context_panel(workspace: Path) -> Panel:
|
|
91
|
+
"""Create context budget panel."""
|
|
92
|
+
context_file = workspace / ".claude/context_budget.json"
|
|
93
|
+
|
|
94
|
+
if not context_file.exists():
|
|
95
|
+
return Panel(
|
|
96
|
+
"[dim]Not configured[/]",
|
|
97
|
+
title="Context Budget",
|
|
98
|
+
border_style="dim"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
data = json.loads(context_file.read_text())
|
|
103
|
+
usage = data.get("usage_percent", 0)
|
|
104
|
+
status = data.get("status", "OK")
|
|
105
|
+
remaining = data.get("remaining_tokens", 0)
|
|
106
|
+
|
|
107
|
+
# Create progress bar
|
|
108
|
+
bar_width = 20
|
|
109
|
+
filled = int(bar_width * usage / 100)
|
|
110
|
+
bar = "█" * filled + "░" * (bar_width - filled)
|
|
111
|
+
|
|
112
|
+
# Color based on status
|
|
113
|
+
if status == "CRITICAL":
|
|
114
|
+
color = "red"
|
|
115
|
+
elif status == "WARNING":
|
|
116
|
+
color = "yellow"
|
|
117
|
+
else:
|
|
118
|
+
color = "green"
|
|
119
|
+
|
|
120
|
+
content = f"""[{color}]{status}[/]
|
|
121
|
+
|
|
122
|
+
[{bar}] {usage:.1f}%
|
|
123
|
+
|
|
124
|
+
Remaining: {remaining:,} tokens
|
|
125
|
+
Entries: {data.get('entry_count', 0)}"""
|
|
126
|
+
|
|
127
|
+
return Panel(content, title="Context Budget", border_style=color)
|
|
128
|
+
|
|
129
|
+
except (json.JSONDecodeError, KeyError):
|
|
130
|
+
return Panel("[red]Error reading state[/]", title="Context Budget")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def create_circuit_panel(workspace: Path) -> Panel:
|
|
134
|
+
"""Create circuit breaker panel."""
|
|
135
|
+
loop_file = workspace / ".loop_state.json"
|
|
136
|
+
|
|
137
|
+
if not loop_file.exists():
|
|
138
|
+
return Panel(
|
|
139
|
+
"[dim]Not active[/]",
|
|
140
|
+
title="Circuit Breaker",
|
|
141
|
+
border_style="dim"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
data = json.loads(loop_file.read_text())
|
|
146
|
+
cb = data.get("circuit_breaker", {})
|
|
147
|
+
|
|
148
|
+
lines = []
|
|
149
|
+
for name, state in cb.items():
|
|
150
|
+
if isinstance(state, dict):
|
|
151
|
+
cb_state = state.get("state", "UNKNOWN")
|
|
152
|
+
failures = state.get("failures", 0)
|
|
153
|
+
|
|
154
|
+
if cb_state == "OPEN":
|
|
155
|
+
icon = "🔴"
|
|
156
|
+
color = "red"
|
|
157
|
+
elif cb_state == "HALF_OPEN":
|
|
158
|
+
icon = "🟡"
|
|
159
|
+
color = "yellow"
|
|
160
|
+
else:
|
|
161
|
+
icon = "🟢"
|
|
162
|
+
color = "green"
|
|
163
|
+
|
|
164
|
+
lines.append(f"{icon} [{color}]{name}[/]: {cb_state} ({failures} failures)")
|
|
165
|
+
|
|
166
|
+
content = "\n".join(lines) if lines else "[dim]No circuits[/]"
|
|
167
|
+
return Panel(content, title="Circuit Breaker", border_style="green")
|
|
168
|
+
|
|
169
|
+
except (json.JSONDecodeError, KeyError):
|
|
170
|
+
return Panel("[red]Error[/]", title="Circuit Breaker")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def create_progress_panel(workspace: Path) -> Panel:
|
|
174
|
+
"""Create progress panel."""
|
|
175
|
+
loop_file = workspace / ".loop_state.json"
|
|
176
|
+
|
|
177
|
+
if not loop_file.exists():
|
|
178
|
+
return Panel(
|
|
179
|
+
"[dim]No active loop[/]",
|
|
180
|
+
title="Product Loop",
|
|
181
|
+
border_style="dim"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
data = json.loads(loop_file.read_text())
|
|
186
|
+
|
|
187
|
+
iteration = data.get("iteration", 0)
|
|
188
|
+
phase = data.get("phase", "UNKNOWN")
|
|
189
|
+
current = data.get("current_task")
|
|
190
|
+
completed = len(data.get("tasks_completed", []))
|
|
191
|
+
remaining = len(data.get("tasks_remaining", []))
|
|
192
|
+
total = completed + remaining
|
|
193
|
+
|
|
194
|
+
metrics = data.get("metrics", {})
|
|
195
|
+
success_rate = metrics.get("success_rate", 1.0)
|
|
196
|
+
|
|
197
|
+
# Progress bar
|
|
198
|
+
if total > 0:
|
|
199
|
+
progress = completed / total
|
|
200
|
+
bar_width = 20
|
|
201
|
+
filled = int(bar_width * progress)
|
|
202
|
+
bar = "█" * filled + "░" * (bar_width - filled)
|
|
203
|
+
progress_line = f"[{bar}] {progress*100:.0f}%"
|
|
204
|
+
else:
|
|
205
|
+
progress_line = "[dim]No tasks[/]"
|
|
206
|
+
|
|
207
|
+
content = f"""Iteration: {iteration}
|
|
208
|
+
Phase: [cyan]{phase}[/]
|
|
209
|
+
Current: {current or '[dim]None[/]'}
|
|
210
|
+
|
|
211
|
+
{progress_line}
|
|
212
|
+
Completed: {completed}/{total}
|
|
213
|
+
Success: {success_rate*100:.0f}%"""
|
|
214
|
+
|
|
215
|
+
return Panel(content, title="Product Loop", border_style="cyan")
|
|
216
|
+
|
|
217
|
+
except (json.JSONDecodeError, KeyError):
|
|
218
|
+
return Panel("[red]Error[/]", title="Product Loop")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def create_skills_panel(workspace: Path) -> Panel:
|
|
222
|
+
"""Create skills panel."""
|
|
223
|
+
skills = []
|
|
224
|
+
|
|
225
|
+
skills_dirs = [
|
|
226
|
+
workspace / ".claude/skills",
|
|
227
|
+
workspace / ".cursor/skills",
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
for skills_dir in skills_dirs:
|
|
231
|
+
if skills_dir.exists():
|
|
232
|
+
for skill_dir in skills_dir.iterdir():
|
|
233
|
+
if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
|
|
234
|
+
skills.append(skill_dir.name)
|
|
235
|
+
|
|
236
|
+
if not skills:
|
|
237
|
+
return Panel(
|
|
238
|
+
"[dim]No skills installed[/]",
|
|
239
|
+
title="Skills",
|
|
240
|
+
border_style="dim"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
content = "\n".join(f"• {skill}" for skill in sorted(set(skills)))
|
|
244
|
+
return Panel(content, title="Skills", border_style="magenta")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
dashboard_cmd()
|
up/commands/learn.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""up learn - Learning system CLI commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def learn_cmd():
|
|
17
|
+
"""Learning system commands.
|
|
18
|
+
|
|
19
|
+
Research best practices, analyze code, and generate improvement plans.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@learn_cmd.command("auto")
|
|
25
|
+
@click.option("--workspace", "-w", type=click.Path(exists=True), help="Workspace path")
|
|
26
|
+
def learn_auto(workspace: str):
|
|
27
|
+
"""Auto-analyze project and identify improvements.
|
|
28
|
+
|
|
29
|
+
Scans the codebase to detect technologies, patterns, and
|
|
30
|
+
generate research topics for improvement.
|
|
31
|
+
"""
|
|
32
|
+
ws = Path(workspace) if workspace else Path.cwd()
|
|
33
|
+
|
|
34
|
+
console.print(Panel.fit(
|
|
35
|
+
"[bold blue]Learning System[/] - Auto Analysis",
|
|
36
|
+
border_style="blue"
|
|
37
|
+
))
|
|
38
|
+
|
|
39
|
+
# Run project analyzer
|
|
40
|
+
profile = analyze_project(ws)
|
|
41
|
+
|
|
42
|
+
if profile is None:
|
|
43
|
+
console.print("[red]Error: Could not analyze project[/]")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Display results
|
|
47
|
+
display_profile(profile)
|
|
48
|
+
|
|
49
|
+
# Save profile
|
|
50
|
+
save_path = save_profile(ws, profile)
|
|
51
|
+
console.print(f"\n[green]✓[/] Profile saved to: [cyan]{save_path}[/]")
|
|
52
|
+
|
|
53
|
+
# Suggest next steps
|
|
54
|
+
console.print("\n[bold]Next Steps:[/]")
|
|
55
|
+
if profile.get("research_topics"):
|
|
56
|
+
console.print(" 1. Research topics with: [cyan]up learn research \"topic\"[/]")
|
|
57
|
+
console.print(" 2. Generate PRD with: [cyan]up learn plan[/]")
|
|
58
|
+
console.print(" 3. Start development with: [cyan]/product-loop[/]")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@learn_cmd.command("analyze")
|
|
62
|
+
@click.option("--workspace", "-w", type=click.Path(exists=True), help="Workspace path")
|
|
63
|
+
def learn_analyze(workspace: str):
|
|
64
|
+
"""Analyze all research files and extract patterns."""
|
|
65
|
+
ws = Path(workspace) if workspace else Path.cwd()
|
|
66
|
+
|
|
67
|
+
research_dir = find_skill_dir(ws, "learning-system") / "research"
|
|
68
|
+
insights_dir = find_skill_dir(ws, "learning-system") / "insights"
|
|
69
|
+
|
|
70
|
+
if not research_dir.exists():
|
|
71
|
+
console.print("[yellow]No research files found.[/]")
|
|
72
|
+
console.print("Run [cyan]up learn research \"topic\"[/] first.")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
console.print(Panel.fit(
|
|
76
|
+
"[bold blue]Learning System[/] - Analyze Research",
|
|
77
|
+
border_style="blue"
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
# Count research files
|
|
81
|
+
research_files = list(research_dir.glob("*.md"))
|
|
82
|
+
console.print(f"Found [cyan]{len(research_files)}[/] research files")
|
|
83
|
+
|
|
84
|
+
for f in research_files:
|
|
85
|
+
console.print(f" • {f.name}")
|
|
86
|
+
|
|
87
|
+
console.print("\n[bold]Analysis:[/]")
|
|
88
|
+
console.print(" Use Claude/Cursor to analyze research files and update:")
|
|
89
|
+
console.print(f" • [cyan]{insights_dir}/patterns.md[/]")
|
|
90
|
+
console.print(f" • [cyan]{insights_dir}/gap-analysis.md[/]")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@learn_cmd.command("plan")
|
|
94
|
+
@click.option("--workspace", "-w", type=click.Path(exists=True), help="Workspace path")
|
|
95
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file path")
|
|
96
|
+
def learn_plan(workspace: str, output: str):
|
|
97
|
+
"""Generate improvement plan (PRD) from analysis."""
|
|
98
|
+
ws = Path(workspace) if workspace else Path.cwd()
|
|
99
|
+
|
|
100
|
+
console.print(Panel.fit(
|
|
101
|
+
"[bold blue]Learning System[/] - Generate PRD",
|
|
102
|
+
border_style="blue"
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
# Check for gap analysis
|
|
106
|
+
skill_dir = find_skill_dir(ws, "learning-system")
|
|
107
|
+
gap_file = skill_dir / "insights/gap-analysis.md"
|
|
108
|
+
|
|
109
|
+
if not gap_file.exists():
|
|
110
|
+
console.print("[yellow]No gap analysis found.[/]")
|
|
111
|
+
console.print("Run analysis first to identify gaps.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Load profile if exists
|
|
115
|
+
profile_file = skill_dir / "project_profile.json"
|
|
116
|
+
profile = {}
|
|
117
|
+
if profile_file.exists():
|
|
118
|
+
try:
|
|
119
|
+
profile = json.loads(profile_file.read_text())
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
# Generate PRD template
|
|
124
|
+
output_path = Path(output) if output else skill_dir / "prd.json"
|
|
125
|
+
prd = generate_prd_template(profile)
|
|
126
|
+
|
|
127
|
+
output_path.write_text(json.dumps(prd, indent=2))
|
|
128
|
+
console.print(f"[green]✓[/] PRD template created: [cyan]{output_path}[/]")
|
|
129
|
+
console.print("\nEdit the PRD to add specific user stories based on gap analysis.")
|
|
130
|
+
console.print("Then run [cyan]/product-loop[/] to start development.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@learn_cmd.command("status")
|
|
134
|
+
@click.option("--workspace", "-w", type=click.Path(exists=True), help="Workspace path")
|
|
135
|
+
def learn_status(workspace: str):
|
|
136
|
+
"""Show learning system status."""
|
|
137
|
+
ws = Path(workspace) if workspace else Path.cwd()
|
|
138
|
+
|
|
139
|
+
console.print(Panel.fit(
|
|
140
|
+
"[bold blue]Learning System[/] - Status",
|
|
141
|
+
border_style="blue"
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
skill_dir = find_skill_dir(ws, "learning-system")
|
|
145
|
+
|
|
146
|
+
if not skill_dir.exists():
|
|
147
|
+
console.print("[yellow]Learning system not initialized.[/]")
|
|
148
|
+
console.print("Run [cyan]up init[/] to set up.")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Check files
|
|
152
|
+
files = {
|
|
153
|
+
"Project Profile": skill_dir / "project_profile.json",
|
|
154
|
+
"Sources Config": skill_dir / "sources.json",
|
|
155
|
+
"Patterns": skill_dir / "insights/patterns.md",
|
|
156
|
+
"Gap Analysis": skill_dir / "insights/gap-analysis.md",
|
|
157
|
+
"PRD": skill_dir / "prd.json",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
table = Table(title="Learning System Files")
|
|
161
|
+
table.add_column("File", style="cyan")
|
|
162
|
+
table.add_column("Status")
|
|
163
|
+
|
|
164
|
+
for name, path in files.items():
|
|
165
|
+
if path.exists():
|
|
166
|
+
table.add_row(name, "[green]✓ exists[/]")
|
|
167
|
+
else:
|
|
168
|
+
table.add_row(name, "[dim]○ not created[/]")
|
|
169
|
+
|
|
170
|
+
console.print(table)
|
|
171
|
+
|
|
172
|
+
# Count research files
|
|
173
|
+
research_dir = skill_dir / "research"
|
|
174
|
+
if research_dir.exists():
|
|
175
|
+
research_count = len(list(research_dir.glob("*.md")))
|
|
176
|
+
console.print(f"\nResearch files: [cyan]{research_count}[/]")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def find_skill_dir(workspace: Path, skill_name: str) -> Path:
|
|
180
|
+
"""Find skill directory (Claude or Cursor)."""
|
|
181
|
+
claude_skill = workspace / f".claude/skills/{skill_name}"
|
|
182
|
+
cursor_skill = workspace / f".cursor/skills/{skill_name}"
|
|
183
|
+
|
|
184
|
+
if claude_skill.exists():
|
|
185
|
+
return claude_skill
|
|
186
|
+
if cursor_skill.exists():
|
|
187
|
+
return cursor_skill
|
|
188
|
+
|
|
189
|
+
# Default to Claude
|
|
190
|
+
return claude_skill
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def analyze_project(workspace: Path) -> dict:
|
|
194
|
+
"""Analyze project and return profile."""
|
|
195
|
+
import os
|
|
196
|
+
import re
|
|
197
|
+
|
|
198
|
+
profile = {
|
|
199
|
+
"name": workspace.name,
|
|
200
|
+
"languages": [],
|
|
201
|
+
"frameworks": [],
|
|
202
|
+
"patterns_detected": [],
|
|
203
|
+
"improvement_areas": [],
|
|
204
|
+
"research_topics": [],
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Extension to language mapping
|
|
208
|
+
extensions = {
|
|
209
|
+
".py": "Python",
|
|
210
|
+
".js": "JavaScript",
|
|
211
|
+
".ts": "TypeScript",
|
|
212
|
+
".tsx": "TypeScript",
|
|
213
|
+
".go": "Go",
|
|
214
|
+
".rs": "Rust",
|
|
215
|
+
".java": "Java",
|
|
216
|
+
".rb": "Ruby",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Framework indicators
|
|
220
|
+
framework_indicators = {
|
|
221
|
+
"fastapi": "FastAPI",
|
|
222
|
+
"django": "Django",
|
|
223
|
+
"flask": "Flask",
|
|
224
|
+
"react": "React",
|
|
225
|
+
"next": "Next.js",
|
|
226
|
+
"vue": "Vue.js",
|
|
227
|
+
"langchain": "LangChain",
|
|
228
|
+
"langgraph": "LangGraph",
|
|
229
|
+
"express": "Express",
|
|
230
|
+
"pytest": "pytest",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Detect languages
|
|
234
|
+
skip_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv", "build", "dist"}
|
|
235
|
+
found_languages = set()
|
|
236
|
+
|
|
237
|
+
for root, dirs, files in os.walk(workspace):
|
|
238
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs]
|
|
239
|
+
for f in files:
|
|
240
|
+
ext = Path(f).suffix.lower()
|
|
241
|
+
if ext in extensions:
|
|
242
|
+
found_languages.add(extensions[ext])
|
|
243
|
+
|
|
244
|
+
profile["languages"] = sorted(found_languages)
|
|
245
|
+
|
|
246
|
+
# Detect frameworks
|
|
247
|
+
config_files = [
|
|
248
|
+
workspace / "pyproject.toml",
|
|
249
|
+
workspace / "requirements.txt",
|
|
250
|
+
workspace / "package.json",
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
found_frameworks = set()
|
|
254
|
+
for config in config_files:
|
|
255
|
+
if config.exists():
|
|
256
|
+
try:
|
|
257
|
+
content = config.read_text().lower()
|
|
258
|
+
for key, name in framework_indicators.items():
|
|
259
|
+
if key in content:
|
|
260
|
+
found_frameworks.add(name)
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
profile["frameworks"] = sorted(found_frameworks)
|
|
265
|
+
|
|
266
|
+
# Detect patterns
|
|
267
|
+
pattern_indicators = {
|
|
268
|
+
r"class.*Repository": "Repository Pattern",
|
|
269
|
+
r"class.*Service": "Service Layer",
|
|
270
|
+
r"@dataclass": "Dataclasses",
|
|
271
|
+
r"async def": "Async/Await",
|
|
272
|
+
r"def test_": "Unit Tests",
|
|
273
|
+
r"Protocol\)": "Protocol Pattern",
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
src_dir = workspace / "src"
|
|
277
|
+
if not src_dir.exists():
|
|
278
|
+
src_dir = workspace
|
|
279
|
+
|
|
280
|
+
found_patterns = set()
|
|
281
|
+
for py_file in src_dir.rglob("*.py"):
|
|
282
|
+
try:
|
|
283
|
+
content = py_file.read_text()
|
|
284
|
+
for pattern, name in pattern_indicators.items():
|
|
285
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
286
|
+
found_patterns.add(name)
|
|
287
|
+
except Exception:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
profile["patterns_detected"] = sorted(found_patterns)
|
|
291
|
+
|
|
292
|
+
# Identify improvements
|
|
293
|
+
improvements = []
|
|
294
|
+
if "Python" in profile["languages"]:
|
|
295
|
+
if "Unit Tests" not in profile["patterns_detected"]:
|
|
296
|
+
improvements.append("add-unit-tests")
|
|
297
|
+
if "Protocol Pattern" not in profile["patterns_detected"]:
|
|
298
|
+
improvements.append("add-interfaces")
|
|
299
|
+
|
|
300
|
+
if any(f in profile["frameworks"] for f in ["FastAPI", "Django", "Flask"]):
|
|
301
|
+
improvements.append("add-caching")
|
|
302
|
+
|
|
303
|
+
profile["improvement_areas"] = improvements
|
|
304
|
+
|
|
305
|
+
# Generate research topics
|
|
306
|
+
topic_map = {
|
|
307
|
+
"add-unit-tests": "testing best practices",
|
|
308
|
+
"add-interfaces": "Python Protocol patterns",
|
|
309
|
+
"add-caching": "caching strategies",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
topics = [topic_map[i] for i in improvements if i in topic_map]
|
|
313
|
+
for fw in profile["frameworks"][:2]:
|
|
314
|
+
topics.append(f"{fw} best practices")
|
|
315
|
+
|
|
316
|
+
profile["research_topics"] = topics[:5]
|
|
317
|
+
|
|
318
|
+
return profile
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def display_profile(profile: dict) -> None:
|
|
322
|
+
"""Display profile in rich format."""
|
|
323
|
+
table = Table(title="Project Profile")
|
|
324
|
+
table.add_column("Aspect", style="cyan")
|
|
325
|
+
table.add_column("Detected")
|
|
326
|
+
|
|
327
|
+
table.add_row("Name", profile.get("name", "Unknown"))
|
|
328
|
+
table.add_row("Languages", ", ".join(profile.get("languages", [])) or "None")
|
|
329
|
+
table.add_row("Frameworks", ", ".join(profile.get("frameworks", [])) or "None")
|
|
330
|
+
table.add_row("Patterns", ", ".join(profile.get("patterns_detected", [])) or "None")
|
|
331
|
+
table.add_row("Improvements", ", ".join(profile.get("improvement_areas", [])) or "None")
|
|
332
|
+
table.add_row("Research Topics", ", ".join(profile.get("research_topics", [])) or "None")
|
|
333
|
+
|
|
334
|
+
console.print(table)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def save_profile(workspace: Path, profile: dict) -> Path:
|
|
338
|
+
"""Save profile to JSON file."""
|
|
339
|
+
skill_dir = find_skill_dir(workspace, "learning-system")
|
|
340
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
|
|
342
|
+
filepath = skill_dir / "project_profile.json"
|
|
343
|
+
filepath.write_text(json.dumps(profile, indent=2))
|
|
344
|
+
return filepath
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def generate_prd_template(profile: dict) -> dict:
|
|
348
|
+
"""Generate PRD template from profile."""
|
|
349
|
+
from datetime import date
|
|
350
|
+
|
|
351
|
+
prd = {
|
|
352
|
+
"project": profile.get("name", "Project") + " Improvements",
|
|
353
|
+
"branchName": "feature/improvements",
|
|
354
|
+
"description": "Improvements identified by learning system",
|
|
355
|
+
"createdAt": date.today().isoformat(),
|
|
356
|
+
"userStories": [],
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# Generate user stories from improvement areas
|
|
360
|
+
for i, area in enumerate(profile.get("improvement_areas", []), 1):
|
|
361
|
+
story = {
|
|
362
|
+
"id": f"US-{i:03d}",
|
|
363
|
+
"title": area.replace("-", " ").title(),
|
|
364
|
+
"description": f"Implement {area.replace('-', ' ')}",
|
|
365
|
+
"acceptanceCriteria": [
|
|
366
|
+
"Implementation complete",
|
|
367
|
+
"Tests passing",
|
|
368
|
+
"Documentation updated",
|
|
369
|
+
],
|
|
370
|
+
"priority": i,
|
|
371
|
+
"effort": "medium",
|
|
372
|
+
"passes": False,
|
|
373
|
+
"notes": "",
|
|
374
|
+
}
|
|
375
|
+
prd["userStories"].append(story)
|
|
376
|
+
|
|
377
|
+
return prd
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
learn_cmd()
|