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/commands/new.py CHANGED
@@ -1,13 +1,14 @@
1
1
  """up new - Create a new project with up systems."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
 
6
5
  import click
7
6
  from rich.console import Console
8
7
  from rich.panel import Panel
8
+ from rich.table import Table
9
9
 
10
10
  from up.templates import scaffold_project
11
+ from up.templates.projects import get_available_templates, create_project_from_template
11
12
 
12
13
  console = Console()
13
14
 
@@ -23,15 +24,33 @@ console = Console()
23
24
  @click.option(
24
25
  "--template",
25
26
  "-t",
26
- type=click.Choice(["minimal", "standard", "full"]),
27
+ type=click.Choice(["minimal", "standard", "full", "fastapi", "nextjs", "python-lib"]),
27
28
  default="standard",
28
29
  help="Project template",
29
30
  )
30
- def new_cmd(name: str, ai: str, template: str):
31
+ @click.option(
32
+ "--list-templates",
33
+ is_flag=True,
34
+ help="List available templates",
35
+ )
36
+ def new_cmd(name: str, ai: str, template: str, list_templates: bool):
31
37
  """Create a new project with up systems.
32
38
 
33
39
  NAME is the project directory name.
40
+
41
+ \b
42
+ Templates:
43
+ minimal Basic structure with docs only
44
+ standard Full up systems (docs, learn, loop)
45
+ full Everything including MCP
46
+ fastapi FastAPI backend template
47
+ nextjs Next.js frontend template
48
+ python-lib Python library template
34
49
  """
50
+ if list_templates:
51
+ _show_templates()
52
+ return
53
+
35
54
  target = Path.cwd() / name
36
55
 
37
56
  if target.exists():
@@ -39,17 +58,29 @@ def new_cmd(name: str, ai: str, template: str):
39
58
  raise SystemExit(1)
40
59
 
41
60
  console.print(Panel.fit(
42
- f"[bold blue]up new[/] - Creating [cyan]{name}[/]",
61
+ f"[bold blue]up new[/] - Creating [cyan]{name}[/] ({template})",
43
62
  border_style="blue"
44
63
  ))
45
64
 
46
65
  # Create directory
47
66
  target.mkdir(parents=True)
48
67
 
49
- # Determine systems based on template
50
- systems = _get_systems_for_template(template)
68
+ # Check if it's a project-type template
69
+ project_templates = ["fastapi", "nextjs", "python-lib"]
70
+
71
+ if template in project_templates:
72
+ # Create project from template first
73
+ console.print(f" [dim]Creating {template} project structure...[/]")
74
+ create_project_from_template(target, template, name, force=True)
75
+
76
+ # Then add up systems
77
+ systems = ["docs", "learn", "loop"]
78
+ console.print(" [dim]Adding up systems...[/]")
79
+ else:
80
+ # Determine systems based on template
81
+ systems = _get_systems_for_template(template)
51
82
 
52
- # Scaffold
83
+ # Scaffold up systems
53
84
  scaffold_project(
54
85
  target_dir=target,
55
86
  ai_target=ai,
@@ -58,8 +89,7 @@ def new_cmd(name: str, ai: str, template: str):
58
89
  )
59
90
 
60
91
  console.print(f"\n[green]✓[/] Project created at [cyan]{target}[/]")
61
- console.print(f"\n cd {name}")
62
- console.print(" up init --help")
92
+ _print_next_steps(name, template)
63
93
 
64
94
 
65
95
  def _get_systems_for_template(template: str) -> list:
@@ -67,6 +97,74 @@ def _get_systems_for_template(template: str) -> list:
67
97
  templates = {
68
98
  "minimal": ["docs"],
69
99
  "standard": ["docs", "learn", "loop"],
70
- "full": ["docs", "learn", "loop"],
100
+ "full": ["docs", "learn", "loop", "mcp"],
71
101
  }
72
102
  return templates.get(template, ["docs"])
103
+
104
+
105
+ def _show_templates():
106
+ """Show available templates."""
107
+ console.print("\n[bold]Available Templates[/]\n")
108
+
109
+ table = Table()
110
+ table.add_column("Template", style="cyan")
111
+ table.add_column("Description")
112
+ table.add_column("Systems")
113
+
114
+ table.add_row(
115
+ "minimal",
116
+ "Basic structure with documentation",
117
+ "docs"
118
+ )
119
+ table.add_row(
120
+ "standard",
121
+ "Full up systems (default)",
122
+ "docs, learn, loop"
123
+ )
124
+ table.add_row(
125
+ "full",
126
+ "Everything including MCP server",
127
+ "docs, learn, loop, mcp"
128
+ )
129
+ table.add_row(
130
+ "fastapi",
131
+ "FastAPI backend with SQLAlchemy",
132
+ "fastapi + all systems"
133
+ )
134
+ table.add_row(
135
+ "nextjs",
136
+ "Next.js frontend with TypeScript",
137
+ "nextjs + all systems"
138
+ )
139
+ table.add_row(
140
+ "python-lib",
141
+ "Python library with packaging",
142
+ "lib + all systems"
143
+ )
144
+
145
+ console.print(table)
146
+
147
+ console.print("\n[bold]Usage:[/]")
148
+ console.print(" up new my-project --template fastapi")
149
+ console.print(" up new my-app -t nextjs")
150
+
151
+
152
+ def _print_next_steps(name: str, template: str):
153
+ """Print next steps after creation."""
154
+ console.print(f"\n cd {name}")
155
+
156
+ if template == "fastapi":
157
+ console.print(" pip install -e .[dev]")
158
+ console.print(" uvicorn src.{name}.main:app --reload".replace("{name}", name.replace("-", "_")))
159
+ elif template == "nextjs":
160
+ console.print(" npm install")
161
+ console.print(" npm run dev")
162
+ elif template == "python-lib":
163
+ console.print(" pip install -e .[dev]")
164
+ console.print(" pytest")
165
+ else:
166
+ console.print(" up status")
167
+
168
+ console.print("\n[bold]Available commands:[/]")
169
+ console.print(" up status Show system health")
170
+ console.print(" up learn auto Analyze project")
up/commands/start.py ADDED
@@ -0,0 +1,414 @@
1
+ """up start - Start the product loop."""
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.panel import Panel
10
+ from rich.table import Table
11
+ from tqdm import tqdm
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.command()
17
+ @click.option("--resume", "-r", is_flag=True, help="Resume from last checkpoint")
18
+ @click.option("--dry-run", is_flag=True, help="Preview without executing")
19
+ @click.option("--task", "-t", help="Start with specific task ID")
20
+ @click.option("--prd", "-p", type=click.Path(exists=True), help="Path to PRD file")
21
+ @click.option("--interactive", "-i", is_flag=True, help="Interactive mode with confirmations")
22
+ def start_cmd(resume: bool, dry_run: bool, task: str, prd: str, interactive: bool):
23
+ """Start the product loop for autonomous development.
24
+
25
+ The product loop implements SESRC principles:
26
+ - Stable: Graceful degradation
27
+ - Efficient: Token budgets
28
+ - Safe: Input validation
29
+ - Reliable: Checkpoints & rollback
30
+ - Cost-effective: Early termination
31
+
32
+ \b
33
+ Examples:
34
+ up start # Start fresh
35
+ up start --resume # Resume from checkpoint
36
+ up start --task US-003 # Start specific task
37
+ up start --dry-run # Preview mode
38
+ up start -i # Interactive mode
39
+ """
40
+ cwd = Path.cwd()
41
+
42
+ # Check if initialized with progress
43
+ console.print()
44
+ with tqdm(total=4, desc="Initializing", bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}") as pbar:
45
+
46
+ # Step 1: Check initialization
47
+ pbar.set_description("Checking project")
48
+ if not _is_initialized(cwd):
49
+ pbar.close()
50
+ console.print("\n[red]Error:[/] Project not initialized.")
51
+ console.print("Run [cyan]up init[/] first.")
52
+ raise SystemExit(1)
53
+ pbar.update(1)
54
+ time.sleep(0.2)
55
+
56
+ # Step 2: Find task source
57
+ pbar.set_description("Finding tasks")
58
+ task_source = _find_task_source(cwd, prd)
59
+ pbar.update(1)
60
+ time.sleep(0.2)
61
+
62
+ # Step 3: Load state
63
+ pbar.set_description("Loading state")
64
+ state = _load_loop_state(cwd)
65
+ pbar.update(1)
66
+ time.sleep(0.2)
67
+
68
+ # Step 4: Check circuit breaker
69
+ pbar.set_description("Checking circuits")
70
+ cb_status = _check_circuit_breaker(state)
71
+ pbar.update(1)
72
+ time.sleep(0.1)
73
+
74
+ console.print()
75
+ console.print(Panel.fit(
76
+ "[bold blue]Product Loop[/] - SESRC Autonomous Development",
77
+ border_style="blue"
78
+ ))
79
+
80
+ # Display status table
81
+ _display_status_table(state, task_source, cwd, resume)
82
+
83
+ # Check for task sources
84
+ if not task_source and not resume:
85
+ console.print("\n[yellow]Warning:[/] No task source found.")
86
+ console.print("\nCreate one of:")
87
+ console.print(" • [cyan]prd.json[/] - Structured user stories")
88
+ console.print(" • [cyan]TODO.md[/] - Task list")
89
+ console.print("\nOr run [cyan]up learn plan[/] to generate a PRD.")
90
+ raise SystemExit(1)
91
+
92
+ # Check circuit breaker
93
+ if cb_status.get("open"):
94
+ console.print(f"\n[red]Circuit breaker OPEN:[/] {cb_status.get('reason')}")
95
+ console.print("Run [cyan]up start --resume[/] after fixing the issue.")
96
+ raise SystemExit(1)
97
+
98
+ # Dry run mode
99
+ if dry_run:
100
+ console.print("\n[yellow]DRY RUN MODE[/] - No changes will be made")
101
+ _preview_loop(cwd, state, task_source, task)
102
+ return
103
+
104
+ # Interactive confirmation
105
+ if interactive:
106
+ if not click.confirm("\nStart the product loop?"):
107
+ console.print("[dim]Cancelled[/]")
108
+ return
109
+
110
+ # Start the loop with progress
111
+ console.print("\n[bold green]Starting product loop...[/]")
112
+ _run_product_loop_with_progress(cwd, state, task_source, task, resume)
113
+
114
+
115
+ def _display_status_table(state: dict, task_source: str, workspace: Path, resume: bool):
116
+ """Display status table."""
117
+ table = Table(show_header=False, box=None, padding=(0, 2))
118
+ table.add_column("Key", style="dim")
119
+ table.add_column("Value")
120
+
121
+ # Iteration
122
+ iteration = state.get("iteration", 0)
123
+ table.add_row("Iteration", f"[cyan]{iteration}[/]")
124
+
125
+ # Phase
126
+ phase = state.get("phase", "INIT")
127
+ table.add_row("Phase", f"[cyan]{phase}[/]")
128
+
129
+ # Task source
130
+ if task_source:
131
+ task_count = _count_tasks(workspace, task_source)
132
+ table.add_row("Tasks", f"[cyan]{task_count}[/] remaining from {task_source}")
133
+ else:
134
+ table.add_row("Tasks", "[dim]No task source[/]")
135
+
136
+ # Completed
137
+ completed = len(state.get("tasks_completed", []))
138
+ table.add_row("Completed", f"[green]{completed}[/]")
139
+
140
+ # Success rate
141
+ success_rate = state.get("metrics", {}).get("success_rate", 1.0)
142
+ table.add_row("Success Rate", f"[green]{success_rate*100:.0f}%[/]")
143
+
144
+ # Mode
145
+ mode = "Resume" if resume else "Fresh Start"
146
+ table.add_row("Mode", mode)
147
+
148
+ console.print(table)
149
+
150
+
151
+ def _is_initialized(workspace: Path) -> bool:
152
+ """Check if project is initialized with up systems."""
153
+ return (
154
+ (workspace / ".claude").exists() or
155
+ (workspace / ".cursor").exists() or
156
+ (workspace / "CLAUDE.md").exists()
157
+ )
158
+
159
+
160
+ def _find_task_source(workspace: Path, prd_path: str = None) -> str:
161
+ """Find task source file."""
162
+ if prd_path:
163
+ return prd_path
164
+
165
+ # Check common locations
166
+ sources = [
167
+ "prd.json",
168
+ ".claude/skills/learning-system/prd.json",
169
+ ".cursor/skills/learning-system/prd.json",
170
+ "TODO.md",
171
+ "docs/todo/TODO.md",
172
+ ]
173
+
174
+ for source in sources:
175
+ if (workspace / source).exists():
176
+ return source
177
+
178
+ return None
179
+
180
+
181
+ def _load_loop_state(workspace: Path) -> dict:
182
+ """Load loop state from file."""
183
+ state_file = workspace / ".loop_state.json"
184
+
185
+ if state_file.exists():
186
+ try:
187
+ return json.loads(state_file.read_text())
188
+ except json.JSONDecodeError:
189
+ pass
190
+
191
+ return {
192
+ "version": "1.0",
193
+ "iteration": 0,
194
+ "phase": "INIT",
195
+ "tasks_completed": [],
196
+ "tasks_remaining": [],
197
+ "circuit_breaker": {},
198
+ "metrics": {"total_edits": 0, "total_rollbacks": 0, "success_rate": 1.0},
199
+ }
200
+
201
+
202
+ def _save_loop_state(workspace: Path, state: dict) -> None:
203
+ """Save loop state to file."""
204
+ from datetime import datetime
205
+ state["last_updated"] = datetime.now().isoformat()
206
+ (workspace / ".loop_state.json").write_text(json.dumps(state, indent=2))
207
+
208
+
209
+ def _count_tasks(workspace: Path, task_source: str) -> int:
210
+ """Count tasks in source file."""
211
+ filepath = workspace / task_source
212
+
213
+ if not filepath.exists():
214
+ return 0
215
+
216
+ if task_source.endswith(".json"):
217
+ try:
218
+ data = json.loads(filepath.read_text())
219
+ stories = data.get("userStories", [])
220
+ return len([s for s in stories if not s.get("passes", False)])
221
+ except json.JSONDecodeError:
222
+ return 0
223
+
224
+ elif task_source.endswith(".md"):
225
+ content = filepath.read_text()
226
+ # Count unchecked items
227
+ import re
228
+ return len(re.findall(r"- \[ \]", content))
229
+
230
+ return 0
231
+
232
+
233
+ def _check_circuit_breaker(state: dict) -> dict:
234
+ """Check circuit breaker status."""
235
+ cb = state.get("circuit_breaker", {})
236
+
237
+ for name, circuit in cb.items():
238
+ if isinstance(circuit, dict) and circuit.get("state") == "OPEN":
239
+ return {
240
+ "open": True,
241
+ "circuit": name,
242
+ "reason": f"{name} circuit opened after {circuit.get('failures', 0)} failures",
243
+ }
244
+
245
+ return {"open": False}
246
+
247
+
248
+ def _preview_loop(workspace: Path, state: dict, task_source: str, specific_task: str = None):
249
+ """Preview what the loop would do."""
250
+ console.print("\n[bold]Preview:[/]")
251
+
252
+ # Show phases with progress simulation
253
+ phases = [
254
+ ("OBSERVE", "Read task and understand requirements"),
255
+ ("CHECKPOINT", "Create git stash checkpoint"),
256
+ ("EXECUTE", "Implement the task"),
257
+ ("VERIFY", "Run tests, types, lint"),
258
+ ("COMMIT", "Update state and commit"),
259
+ ]
260
+
261
+ console.print()
262
+ for phase, desc in tqdm(phases, desc="Phases", bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}"):
263
+ time.sleep(0.3)
264
+
265
+ console.print()
266
+ for phase, desc in phases:
267
+ console.print(f" [cyan]{phase}[/]: {desc}")
268
+
269
+ # Show next task
270
+ if specific_task:
271
+ console.print(f"\n Target task: [cyan]{specific_task}[/]")
272
+ elif task_source and task_source.endswith(".json"):
273
+ next_task = _get_next_task_from_prd(workspace / task_source)
274
+ if next_task:
275
+ console.print(f"\n Next task: [cyan]{next_task.get('id')}[/] - {next_task.get('title')}")
276
+
277
+
278
+ def _get_next_task_from_prd(prd_path: Path) -> dict:
279
+ """Get next incomplete task from PRD."""
280
+ if not prd_path.exists():
281
+ return None
282
+
283
+ try:
284
+ data = json.loads(prd_path.read_text())
285
+ stories = data.get("userStories", [])
286
+
287
+ # Find first incomplete task
288
+ for story in sorted(stories, key=lambda s: s.get("priority", 999)):
289
+ if not story.get("passes", False):
290
+ return story
291
+
292
+ return None
293
+ except json.JSONDecodeError:
294
+ return None
295
+
296
+
297
+ def _run_product_loop_with_progress(
298
+ workspace: Path,
299
+ state: dict,
300
+ task_source: str,
301
+ specific_task: str = None,
302
+ resume: bool = False
303
+ ):
304
+ """Run the product loop with progress indicators."""
305
+ from datetime import datetime
306
+
307
+ # Update state
308
+ if not resume:
309
+ state["iteration"] = state.get("iteration", 0) + 1
310
+ state["phase"] = "OBSERVE"
311
+ state["started_at"] = datetime.now().isoformat()
312
+
313
+ # Get task info
314
+ next_task = None
315
+ if specific_task:
316
+ next_task = {"id": specific_task, "title": specific_task}
317
+ elif task_source and task_source.endswith(".json"):
318
+ next_task = _get_next_task_from_prd(workspace / task_source)
319
+
320
+ # Show task info
321
+ if next_task:
322
+ console.print(f"\n[bold]Task:[/] [cyan]{next_task.get('id')}[/] - {next_task.get('title', 'N/A')}")
323
+
324
+ # Simulate loop phases with progress
325
+ phases = [
326
+ ("OBSERVE", "Reading task requirements"),
327
+ ("CHECKPOINT", "Creating checkpoint"),
328
+ ("EXECUTE", "Ready for implementation"),
329
+ ("VERIFY", "Verification pending"),
330
+ ("COMMIT", "Awaiting completion"),
331
+ ]
332
+
333
+ console.print("\n[bold]Loop Progress:[/]")
334
+
335
+ with tqdm(total=len(phases), desc="Initializing loop",
336
+ bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]") as pbar:
337
+
338
+ for i, (phase, desc) in enumerate(phases):
339
+ state["phase"] = phase
340
+ pbar.set_description(f"{phase}: {desc}")
341
+ pbar.update(1)
342
+
343
+ # Only run through OBSERVE and CHECKPOINT automatically
344
+ if i >= 2:
345
+ break
346
+
347
+ time.sleep(0.5)
348
+
349
+ # Save state
350
+ _save_loop_state(workspace, state)
351
+
352
+ # Generate instructions for AI
353
+ console.print("\n" + "─" * 50)
354
+ console.print("\n[bold green]✓[/] Loop initialized at [cyan]EXECUTE[/] phase")
355
+
356
+ # Show instructions panel
357
+ instructions = _generate_loop_instructions(workspace, state, task_source, specific_task)
358
+ console.print(Panel(
359
+ instructions,
360
+ title="[bold]AI Instructions[/]",
361
+ border_style="green"
362
+ ))
363
+
364
+ # Show next steps
365
+ console.print("\n[bold]Next Steps:[/]")
366
+ console.print(" 1. Use [cyan]/product-loop[/] in your AI assistant")
367
+ console.print(" 2. Or implement the task manually")
368
+ console.print(" 3. Run [cyan]up status[/] to check progress")
369
+ console.print(" 4. Run [cyan]up dashboard[/] for live monitoring")
370
+
371
+
372
+ def _generate_loop_instructions(
373
+ workspace: Path,
374
+ state: dict,
375
+ task_source: str,
376
+ specific_task: str = None
377
+ ) -> str:
378
+ """Generate instructions for the AI to execute the loop."""
379
+
380
+ task_info = ""
381
+ if specific_task:
382
+ task_info = f"Task: {specific_task}"
383
+ elif task_source:
384
+ next_task = None
385
+ if task_source.endswith(".json"):
386
+ next_task = _get_next_task_from_prd(workspace / task_source)
387
+
388
+ if next_task:
389
+ task_info = f"Task: {next_task.get('id')} - {next_task.get('title')}"
390
+ if next_task.get("acceptanceCriteria"):
391
+ criteria = next_task.get("acceptanceCriteria", [])[:3]
392
+ task_info += "\n\nAcceptance Criteria:"
393
+ for c in criteria:
394
+ task_info += f"\n • {c}"
395
+ else:
396
+ task_info = f"Source: {task_source}"
397
+
398
+ return f"""Iteration #{state.get('iteration', 1)} - Phase: EXECUTE
399
+
400
+ {task_info}
401
+
402
+ SESRC Loop Commands:
403
+ ├─ Checkpoint: git stash push -m "cp-{state.get('iteration', 1)}"
404
+ ├─ Verify: pytest && mypy src/ && ruff check src/
405
+ ├─ Rollback: git stash pop
406
+ └─ Complete: Update .loop_state.json
407
+
408
+ Circuit Breaker: 3 failures → OPEN
409
+ State File: .loop_state.json
410
+ """
411
+
412
+
413
+ if __name__ == "__main__":
414
+ start_cmd()