up-cli 0.1.1__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 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.1.0", prog_name="up")
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__":
@@ -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()