up-cli 0.1.1__py3-none-any.whl → 0.5.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/__init__.py +1 -1
- up/ai_cli.py +229 -0
- up/cli.py +75 -4
- up/commands/agent.py +521 -0
- up/commands/bisect.py +343 -0
- up/commands/branch.py +350 -0
- up/commands/dashboard.py +248 -0
- up/commands/init.py +195 -6
- up/commands/learn.py +1741 -0
- up/commands/memory.py +545 -0
- up/commands/new.py +108 -10
- up/commands/provenance.py +267 -0
- up/commands/review.py +239 -0
- up/commands/start.py +1124 -0
- up/commands/status.py +360 -0
- up/commands/summarize.py +122 -0
- up/commands/sync.py +317 -0
- up/commands/vibe.py +304 -0
- up/context.py +421 -0
- up/core/__init__.py +69 -0
- up/core/checkpoint.py +479 -0
- up/core/provenance.py +364 -0
- up/core/state.py +678 -0
- up/events.py +512 -0
- up/git/__init__.py +37 -0
- up/git/utils.py +270 -0
- up/git/worktree.py +331 -0
- up/learn/__init__.py +155 -0
- up/learn/analyzer.py +227 -0
- up/learn/plan.py +374 -0
- up/learn/research.py +511 -0
- up/learn/utils.py +117 -0
- up/memory.py +1096 -0
- up/parallel.py +551 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/docs/SKILL.md +28 -0
- up/templates/docs/__init__.py +341 -0
- up/templates/docs/standards/HEADERS.md +24 -0
- up/templates/docs/standards/STRUCTURE.md +18 -0
- up/templates/docs/standards/TEMPLATES.md +19 -0
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +546 -27
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up/ui/__init__.py +14 -0
- up/ui/loop_display.py +650 -0
- up/ui/theme.py +137 -0
- up_cli-0.5.0.dist-info/METADATA +519 -0
- up_cli-0.5.0.dist-info/RECORD +55 -0
- up_cli-0.1.1.dist-info/METADATA +0 -186
- up_cli-0.1.1.dist-info/RECORD +0 -14
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/entry_points.txt +0 -0
up/commands/sync.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""up sync - Sync all systems."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.option("--memory/--no-memory", default=True, help="Sync memory system")
|
|
14
|
+
@click.option("--docs/--no-docs", default=True, help="Update docs")
|
|
15
|
+
@click.option("--full", is_flag=True, help="Full sync including file re-indexing")
|
|
16
|
+
def sync_cmd(memory: bool, docs: bool, full: bool):
|
|
17
|
+
"""Sync all up systems.
|
|
18
|
+
|
|
19
|
+
Updates memory index, refreshes docs, and ensures all systems
|
|
20
|
+
are in sync with current project state.
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
Examples:
|
|
24
|
+
up sync # Standard sync
|
|
25
|
+
up sync --full # Full sync with file re-indexing
|
|
26
|
+
up sync --no-docs # Memory only
|
|
27
|
+
"""
|
|
28
|
+
cwd = Path.cwd()
|
|
29
|
+
|
|
30
|
+
console.print(Panel.fit(
|
|
31
|
+
"[bold blue]Syncing Systems[/]",
|
|
32
|
+
border_style="blue"
|
|
33
|
+
))
|
|
34
|
+
|
|
35
|
+
results = {}
|
|
36
|
+
|
|
37
|
+
# Initialize event system
|
|
38
|
+
from up.events import initialize_event_system
|
|
39
|
+
initialize_event_system(cwd)
|
|
40
|
+
|
|
41
|
+
steps = []
|
|
42
|
+
if memory:
|
|
43
|
+
steps.append(("Memory", _sync_memory))
|
|
44
|
+
if docs:
|
|
45
|
+
steps.append(("Docs", _sync_docs))
|
|
46
|
+
if full:
|
|
47
|
+
steps.append(("Files", _sync_files))
|
|
48
|
+
|
|
49
|
+
for name, func in steps:
|
|
50
|
+
console.print(f"[dim]Syncing {name}...[/]", end=" ")
|
|
51
|
+
try:
|
|
52
|
+
result = func(cwd, full)
|
|
53
|
+
results[name.lower()] = result
|
|
54
|
+
details = ", ".join(f"{k}={v}" for k, v in result.items() if v)
|
|
55
|
+
console.print(f"[green]✓[/] {details or 'done'}")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
results[name.lower()] = {"error": str(e)}
|
|
58
|
+
console.print(f"[red]✗[/] {e}")
|
|
59
|
+
|
|
60
|
+
# Summary
|
|
61
|
+
console.print()
|
|
62
|
+
errors = [k for k, v in results.items() if "error" in v]
|
|
63
|
+
if errors:
|
|
64
|
+
console.print(f"[yellow]Completed with {len(errors)} error(s)[/]")
|
|
65
|
+
else:
|
|
66
|
+
console.print("[green]✓[/] All systems synced")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _sync_memory(workspace: Path, full: bool) -> dict:
|
|
70
|
+
"""Sync memory system."""
|
|
71
|
+
import os
|
|
72
|
+
import sys
|
|
73
|
+
import warnings
|
|
74
|
+
import logging
|
|
75
|
+
from up.memory import MemoryManager, _check_chromadb
|
|
76
|
+
|
|
77
|
+
# Suppress noisy warnings and logs
|
|
78
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
79
|
+
warnings.filterwarnings("ignore")
|
|
80
|
+
logging.getLogger("chromadb").setLevel(logging.ERROR)
|
|
81
|
+
|
|
82
|
+
# Redirect stderr temporarily to suppress ChromaDB noise
|
|
83
|
+
old_stderr = sys.stderr
|
|
84
|
+
sys.stderr = open(os.devnull, 'w')
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Use JSON backend if ChromaDB not available (faster, no model download)
|
|
88
|
+
use_vectors = _check_chromadb()
|
|
89
|
+
|
|
90
|
+
manager = MemoryManager(workspace, use_vectors=use_vectors)
|
|
91
|
+
results = manager.sync()
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"commits": results.get("commits_indexed", 0),
|
|
95
|
+
"files": results.get("files_indexed", 0),
|
|
96
|
+
"backend": manager._backend,
|
|
97
|
+
}
|
|
98
|
+
finally:
|
|
99
|
+
sys.stderr.close()
|
|
100
|
+
sys.stderr = old_stderr
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _sync_docs(workspace: Path, full: bool) -> dict:
|
|
104
|
+
"""Sync documentation."""
|
|
105
|
+
from datetime import date
|
|
106
|
+
import re
|
|
107
|
+
|
|
108
|
+
updated = 0
|
|
109
|
+
|
|
110
|
+
# Update CONTEXT.md date
|
|
111
|
+
context_file = workspace / "docs" / "CONTEXT.md"
|
|
112
|
+
if context_file.exists():
|
|
113
|
+
content = context_file.read_text()
|
|
114
|
+
today = date.today().isoformat()
|
|
115
|
+
new_content = re.sub(
|
|
116
|
+
r'\*\*Updated\*\*:\s*[\d-]+',
|
|
117
|
+
f'**Updated**: {today}',
|
|
118
|
+
content
|
|
119
|
+
)
|
|
120
|
+
if new_content != content:
|
|
121
|
+
context_file.write_text(new_content)
|
|
122
|
+
updated += 1
|
|
123
|
+
|
|
124
|
+
# Check INDEX.md exists
|
|
125
|
+
index_file = workspace / "docs" / "INDEX.md"
|
|
126
|
+
if index_file.exists():
|
|
127
|
+
updated += 0 # Just checking
|
|
128
|
+
|
|
129
|
+
return {"updated": updated}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _sync_files(workspace: Path, full: bool) -> dict:
|
|
133
|
+
"""Sync file index."""
|
|
134
|
+
from up.memory import MemoryManager
|
|
135
|
+
|
|
136
|
+
manager = MemoryManager(workspace)
|
|
137
|
+
indexed = manager.index_file_changes()
|
|
138
|
+
|
|
139
|
+
return {"indexed": indexed}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_hooks_installed(workspace: Path) -> dict:
|
|
143
|
+
"""Check if up-cli hooks are installed.
|
|
144
|
+
|
|
145
|
+
Returns dict with status of each hook.
|
|
146
|
+
"""
|
|
147
|
+
git_dir = workspace / ".git"
|
|
148
|
+
if not git_dir.exists():
|
|
149
|
+
return {"git": False, "post_commit": False, "post_checkout": False}
|
|
150
|
+
|
|
151
|
+
hooks_dir = git_dir / "hooks"
|
|
152
|
+
|
|
153
|
+
result = {"git": True, "post_commit": False, "post_checkout": False}
|
|
154
|
+
|
|
155
|
+
post_commit = hooks_dir / "post-commit"
|
|
156
|
+
if post_commit.exists() and "up-cli" in post_commit.read_text():
|
|
157
|
+
result["post_commit"] = True
|
|
158
|
+
|
|
159
|
+
post_checkout = hooks_dir / "post-checkout"
|
|
160
|
+
if post_checkout.exists() and "up-cli" in post_checkout.read_text():
|
|
161
|
+
result["post_checkout"] = True
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@click.command()
|
|
167
|
+
@click.option("--uninstall", is_flag=True, help="Remove hooks instead of installing")
|
|
168
|
+
@click.option("--check", is_flag=True, help="Check if hooks are installed")
|
|
169
|
+
def hooks_cmd(uninstall: bool, check: bool):
|
|
170
|
+
"""Install or uninstall git hooks for automatic syncing.
|
|
171
|
+
|
|
172
|
+
Installs:
|
|
173
|
+
- post-commit: Auto-index commits to memory
|
|
174
|
+
- post-checkout: Update context on branch switch
|
|
175
|
+
|
|
176
|
+
\b
|
|
177
|
+
Examples:
|
|
178
|
+
up hooks # Install hooks
|
|
179
|
+
up hooks --check # Check status
|
|
180
|
+
up hooks --uninstall # Remove hooks
|
|
181
|
+
"""
|
|
182
|
+
cwd = Path.cwd()
|
|
183
|
+
git_dir = cwd / ".git"
|
|
184
|
+
|
|
185
|
+
if not git_dir.exists():
|
|
186
|
+
console.print("[red]Error:[/] Not a git repository")
|
|
187
|
+
raise SystemExit(1)
|
|
188
|
+
|
|
189
|
+
hooks_dir = git_dir / "hooks"
|
|
190
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
191
|
+
|
|
192
|
+
if check:
|
|
193
|
+
status = check_hooks_installed(cwd)
|
|
194
|
+
console.print("\n[bold]Git Hooks Status:[/]")
|
|
195
|
+
console.print(f" post-commit: {'[green]✓ installed[/]' if status['post_commit'] else '[yellow]✗ not installed[/]'}")
|
|
196
|
+
console.print(f" post-checkout: {'[green]✓ installed[/]' if status['post_checkout'] else '[yellow]✗ not installed[/]'}")
|
|
197
|
+
|
|
198
|
+
if not status['post_commit'] or not status['post_checkout']:
|
|
199
|
+
console.print("\n[dim]Run 'up hooks' to install missing hooks[/]")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if uninstall:
|
|
203
|
+
_uninstall_hooks(hooks_dir)
|
|
204
|
+
else:
|
|
205
|
+
_install_hooks(hooks_dir)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _install_hooks(hooks_dir: Path):
|
|
209
|
+
"""Install git hooks."""
|
|
210
|
+
|
|
211
|
+
# Post-commit hook
|
|
212
|
+
post_commit = hooks_dir / "post-commit"
|
|
213
|
+
post_commit_content = '''#!/bin/bash
|
|
214
|
+
# up-cli auto-sync hook
|
|
215
|
+
# Indexes commits to memory automatically
|
|
216
|
+
|
|
217
|
+
# Run in background to not slow down commits
|
|
218
|
+
(
|
|
219
|
+
# Wait a moment for git to finish
|
|
220
|
+
sleep 1
|
|
221
|
+
|
|
222
|
+
# Sync memory with latest commit
|
|
223
|
+
if command -v up &> /dev/null; then
|
|
224
|
+
up memory sync 2>/dev/null
|
|
225
|
+
elif command -v python3 &> /dev/null; then
|
|
226
|
+
python3 -m up.memory sync 2>/dev/null
|
|
227
|
+
fi
|
|
228
|
+
) &
|
|
229
|
+
|
|
230
|
+
exit 0
|
|
231
|
+
'''
|
|
232
|
+
|
|
233
|
+
_write_hook(post_commit, post_commit_content)
|
|
234
|
+
console.print("[green]✓[/] Installed post-commit hook")
|
|
235
|
+
|
|
236
|
+
# Post-checkout hook (for branch switches)
|
|
237
|
+
post_checkout = hooks_dir / "post-checkout"
|
|
238
|
+
post_checkout_content = '''#!/bin/bash
|
|
239
|
+
# up-cli context update hook
|
|
240
|
+
# Updates context when switching branches
|
|
241
|
+
|
|
242
|
+
PREV_HEAD=$1
|
|
243
|
+
NEW_HEAD=$2
|
|
244
|
+
BRANCH_CHECKOUT=$3
|
|
245
|
+
|
|
246
|
+
# Only run on branch checkout, not file checkout
|
|
247
|
+
if [ "$BRANCH_CHECKOUT" = "1" ]; then
|
|
248
|
+
(
|
|
249
|
+
sleep 1
|
|
250
|
+
if command -v up &> /dev/null; then
|
|
251
|
+
up sync --no-memory 2>/dev/null
|
|
252
|
+
fi
|
|
253
|
+
) &
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
exit 0
|
|
257
|
+
'''
|
|
258
|
+
|
|
259
|
+
_write_hook(post_checkout, post_checkout_content)
|
|
260
|
+
console.print("[green]✓[/] Installed post-checkout hook")
|
|
261
|
+
|
|
262
|
+
console.print("\n[bold]Hooks installed![/]")
|
|
263
|
+
console.print("Memory will auto-sync on commits.")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _write_hook(path: Path, content: str):
|
|
267
|
+
"""Write hook file with executable permissions."""
|
|
268
|
+
import stat
|
|
269
|
+
|
|
270
|
+
# Check for existing hook
|
|
271
|
+
if path.exists():
|
|
272
|
+
existing = path.read_text()
|
|
273
|
+
if "up-cli" in existing:
|
|
274
|
+
# Already our hook, overwrite
|
|
275
|
+
pass
|
|
276
|
+
else:
|
|
277
|
+
# User has custom hook, append
|
|
278
|
+
content = existing + "\n\n" + content
|
|
279
|
+
|
|
280
|
+
path.write_text(content)
|
|
281
|
+
path.chmod(path.stat().st_mode | stat.S_IEXEC)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _uninstall_hooks(hooks_dir: Path):
|
|
285
|
+
"""Remove up-cli hooks."""
|
|
286
|
+
|
|
287
|
+
for hook_name in ["post-commit", "post-checkout"]:
|
|
288
|
+
hook_path = hooks_dir / hook_name
|
|
289
|
+
if hook_path.exists():
|
|
290
|
+
content = hook_path.read_text()
|
|
291
|
+
if "up-cli" in content:
|
|
292
|
+
# Remove our section
|
|
293
|
+
lines = content.split("\n")
|
|
294
|
+
new_lines = []
|
|
295
|
+
skip = False
|
|
296
|
+
for line in lines:
|
|
297
|
+
if "up-cli" in line:
|
|
298
|
+
skip = True
|
|
299
|
+
elif skip and line.startswith("exit"):
|
|
300
|
+
skip = False
|
|
301
|
+
continue
|
|
302
|
+
elif not skip:
|
|
303
|
+
new_lines.append(line)
|
|
304
|
+
|
|
305
|
+
new_content = "\n".join(new_lines).strip()
|
|
306
|
+
if new_content:
|
|
307
|
+
hook_path.write_text(new_content)
|
|
308
|
+
else:
|
|
309
|
+
hook_path.unlink()
|
|
310
|
+
|
|
311
|
+
console.print(f"[green]✓[/] Removed {hook_name} hook")
|
|
312
|
+
|
|
313
|
+
console.print("\n[bold]Hooks uninstalled![/]")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
sync_cmd()
|
up/commands/vibe.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""up save/reset/diff - Vibe coding safety commands.
|
|
2
|
+
|
|
3
|
+
These commands provide the core safety rails for AI-assisted development:
|
|
4
|
+
- up save: Create checkpoint before AI work
|
|
5
|
+
- up reset: Restore to checkpoint when AI fails
|
|
6
|
+
- up diff: Review AI changes before accepting
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from up.core.checkpoint import (
|
|
20
|
+
get_checkpoint_manager,
|
|
21
|
+
CheckpointManager,
|
|
22
|
+
CheckpointNotFoundError,
|
|
23
|
+
NotAGitRepoError,
|
|
24
|
+
)
|
|
25
|
+
from up.core.state import get_state_manager
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# =============================================================================
|
|
31
|
+
# up save - Create checkpoint
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
@click.command("save")
|
|
35
|
+
@click.argument("message", required=False)
|
|
36
|
+
@click.option("--task", "-t", help="Associated task ID")
|
|
37
|
+
@click.option("--quiet", "-q", is_flag=True, help="Minimal output")
|
|
38
|
+
def save_cmd(message: str, task: str, quiet: bool):
|
|
39
|
+
"""Create a checkpoint before AI work.
|
|
40
|
+
|
|
41
|
+
Automatically commits any dirty files and creates a tag for easy recovery.
|
|
42
|
+
Use 'up reset' to restore if AI generation goes wrong.
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Examples:
|
|
46
|
+
up save # Auto-named checkpoint
|
|
47
|
+
up save "before auth" # Named checkpoint
|
|
48
|
+
up save -t US-004 # Link to task
|
|
49
|
+
"""
|
|
50
|
+
cwd = Path.cwd()
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
manager = get_checkpoint_manager(cwd)
|
|
54
|
+
|
|
55
|
+
# Check for changes
|
|
56
|
+
has_changes = manager._has_changes()
|
|
57
|
+
|
|
58
|
+
if not has_changes and not quiet:
|
|
59
|
+
console.print("[dim]No changes to checkpoint (working tree clean)[/]")
|
|
60
|
+
|
|
61
|
+
# Create checkpoint
|
|
62
|
+
metadata = manager.save(
|
|
63
|
+
message=message,
|
|
64
|
+
task_id=task,
|
|
65
|
+
auto_commit=True
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if quiet:
|
|
69
|
+
console.print(metadata.id)
|
|
70
|
+
else:
|
|
71
|
+
console.print(f"[green]✓[/] Checkpoint created: [cyan]{metadata.id}[/]")
|
|
72
|
+
if metadata.files_changed > 0:
|
|
73
|
+
console.print(f" Committed {metadata.files_changed} file(s)")
|
|
74
|
+
console.print(f" Commit: {metadata.commit_sha[:8]}")
|
|
75
|
+
console.print(f" Tag: {metadata.tag_name}")
|
|
76
|
+
console.print(f"\nTo restore: [cyan]up reset {metadata.id}[/]")
|
|
77
|
+
|
|
78
|
+
except NotAGitRepoError:
|
|
79
|
+
console.print("[red]Error:[/] Not a git repository")
|
|
80
|
+
console.print("Initialize with: [cyan]git init[/]")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
console.print(f"[red]Error:[/] {e}")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# =============================================================================
|
|
88
|
+
# up reset - Restore checkpoint
|
|
89
|
+
# =============================================================================
|
|
90
|
+
|
|
91
|
+
@click.command("reset")
|
|
92
|
+
@click.argument("checkpoint_id", required=False)
|
|
93
|
+
@click.option("--hard", is_flag=True, default=True, help="Hard reset (discard changes)")
|
|
94
|
+
@click.option("--soft", is_flag=True, help="Soft reset (keep changes staged)")
|
|
95
|
+
@click.option("--list", "list_checkpoints", is_flag=True, help="List available checkpoints")
|
|
96
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
97
|
+
def reset_cmd(checkpoint_id: str, hard: bool, soft: bool, list_checkpoints: bool, yes: bool):
|
|
98
|
+
"""Reset to a checkpoint.
|
|
99
|
+
|
|
100
|
+
Instantly restores your code to a previous checkpoint state.
|
|
101
|
+
Use when AI generation produces bad results.
|
|
102
|
+
|
|
103
|
+
\b
|
|
104
|
+
Examples:
|
|
105
|
+
up reset # Reset to last checkpoint
|
|
106
|
+
up reset cp-20260204-1234 # Reset to specific checkpoint
|
|
107
|
+
up reset --list # Show available checkpoints
|
|
108
|
+
"""
|
|
109
|
+
cwd = Path.cwd()
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
manager = get_checkpoint_manager(cwd)
|
|
113
|
+
|
|
114
|
+
# List mode
|
|
115
|
+
if list_checkpoints:
|
|
116
|
+
checkpoints = manager.list_checkpoints(limit=20)
|
|
117
|
+
|
|
118
|
+
if not checkpoints:
|
|
119
|
+
console.print("[dim]No checkpoints available[/]")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
table = Table(title="Available Checkpoints")
|
|
123
|
+
table.add_column("ID", style="cyan")
|
|
124
|
+
table.add_column("Message")
|
|
125
|
+
table.add_column("Branch")
|
|
126
|
+
table.add_column("Time")
|
|
127
|
+
|
|
128
|
+
for cp in checkpoints:
|
|
129
|
+
table.add_row(
|
|
130
|
+
cp.id,
|
|
131
|
+
cp.message[:40] + "..." if len(cp.message) > 40 else cp.message,
|
|
132
|
+
cp.branch,
|
|
133
|
+
cp.created_at[:19]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
console.print(table)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Get checkpoint info for confirmation
|
|
140
|
+
if checkpoint_id:
|
|
141
|
+
target = checkpoint_id
|
|
142
|
+
else:
|
|
143
|
+
last = manager.get_last_checkpoint()
|
|
144
|
+
if not last:
|
|
145
|
+
console.print("[yellow]No checkpoints available[/]")
|
|
146
|
+
console.print("Create one with: [cyan]up save[/]")
|
|
147
|
+
return
|
|
148
|
+
target = last.id
|
|
149
|
+
|
|
150
|
+
# Show what will be reset
|
|
151
|
+
stats = manager.diff_stats(target)
|
|
152
|
+
|
|
153
|
+
console.print(f"[bold]Reset to checkpoint:[/] {target}")
|
|
154
|
+
if stats["files"] > 0:
|
|
155
|
+
console.print(f" Changes to discard: {stats['files']} files, "
|
|
156
|
+
f"+{stats['insertions']} -{stats['deletions']}")
|
|
157
|
+
|
|
158
|
+
# Confirm
|
|
159
|
+
if not yes:
|
|
160
|
+
if not click.confirm("Proceed with reset?"):
|
|
161
|
+
console.print("[dim]Cancelled[/]")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Perform reset
|
|
165
|
+
use_hard = not soft
|
|
166
|
+
metadata = manager.restore(checkpoint_id=target, hard=use_hard)
|
|
167
|
+
|
|
168
|
+
console.print(f"\n[green]✓[/] Reset to [cyan]{metadata.id}[/]")
|
|
169
|
+
console.print(f" Commit: {metadata.commit_sha[:8]}")
|
|
170
|
+
|
|
171
|
+
# Update state
|
|
172
|
+
state_manager = get_state_manager(cwd)
|
|
173
|
+
state_manager.state.loop.consecutive_failures = 0 # Reset doom loop counter
|
|
174
|
+
state_manager.save()
|
|
175
|
+
|
|
176
|
+
except NotAGitRepoError:
|
|
177
|
+
console.print("[red]Error:[/] Not a git repository")
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
except CheckpointNotFoundError as e:
|
|
180
|
+
console.print(f"[red]Error:[/] {e}")
|
|
181
|
+
console.print("List available: [cyan]up reset --list[/]")
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
console.print(f"[red]Error:[/] {e}")
|
|
185
|
+
sys.exit(1)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# up diff - Review AI changes
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
@click.command("diff")
|
|
193
|
+
@click.argument("checkpoint_id", required=False)
|
|
194
|
+
@click.option("--stat", is_flag=True, help="Show only stats")
|
|
195
|
+
@click.option("--accept", "-a", is_flag=True, help="Accept changes and commit")
|
|
196
|
+
@click.option("--reject", "-r", is_flag=True, help="Reject changes and reset")
|
|
197
|
+
@click.option("--message", "-m", help="Commit message (with --accept)")
|
|
198
|
+
def diff_cmd(checkpoint_id: str, stat: bool, accept: bool, reject: bool, message: str):
|
|
199
|
+
"""Review AI changes before accepting.
|
|
200
|
+
|
|
201
|
+
Shows a syntax-highlighted diff of changes since the last checkpoint.
|
|
202
|
+
Can accept (commit) or reject (reset) the changes.
|
|
203
|
+
|
|
204
|
+
\b
|
|
205
|
+
Examples:
|
|
206
|
+
up diff # Show diff from last checkpoint
|
|
207
|
+
up diff --stat # Show stats only
|
|
208
|
+
up diff --accept # Accept and commit changes
|
|
209
|
+
up diff --reject # Reject and reset to checkpoint
|
|
210
|
+
"""
|
|
211
|
+
cwd = Path.cwd()
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
manager = get_checkpoint_manager(cwd)
|
|
215
|
+
|
|
216
|
+
# Get diff
|
|
217
|
+
diff_output = manager.diff_from_checkpoint(checkpoint_id)
|
|
218
|
+
stats = manager.diff_stats(checkpoint_id)
|
|
219
|
+
|
|
220
|
+
if not diff_output and stats["files"] == 0:
|
|
221
|
+
console.print("[dim]No changes since checkpoint[/]")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# Stats mode
|
|
225
|
+
if stat:
|
|
226
|
+
console.print(Panel.fit(
|
|
227
|
+
f"[bold]Changes since checkpoint[/]\n\n"
|
|
228
|
+
f"Files: {stats['files']}\n"
|
|
229
|
+
f"Insertions: [green]+{stats['insertions']}[/]\n"
|
|
230
|
+
f"Deletions: [red]-{stats['deletions']}[/]",
|
|
231
|
+
border_style="blue"
|
|
232
|
+
))
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# Reject mode
|
|
236
|
+
if reject:
|
|
237
|
+
console.print("[yellow]Rejecting changes...[/]")
|
|
238
|
+
manager.restore(checkpoint_id=checkpoint_id)
|
|
239
|
+
console.print("[green]✓[/] Changes rejected, reset to checkpoint")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Accept mode
|
|
243
|
+
if accept:
|
|
244
|
+
# Commit the changes
|
|
245
|
+
result = subprocess.run(
|
|
246
|
+
["git", "add", "-A"],
|
|
247
|
+
cwd=cwd,
|
|
248
|
+
capture_output=True
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
commit_msg = message or "Accept AI changes"
|
|
252
|
+
result = subprocess.run(
|
|
253
|
+
["git", "commit", "-m", commit_msg],
|
|
254
|
+
cwd=cwd,
|
|
255
|
+
capture_output=True,
|
|
256
|
+
text=True
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if result.returncode == 0:
|
|
260
|
+
console.print(f"[green]✓[/] Changes accepted and committed")
|
|
261
|
+
console.print(f" Message: {commit_msg}")
|
|
262
|
+
else:
|
|
263
|
+
console.print("[yellow]No changes to commit[/]")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# Show diff
|
|
267
|
+
console.print(Panel.fit(
|
|
268
|
+
f"[bold]Changes since checkpoint[/] "
|
|
269
|
+
f"({stats['files']} files, +{stats['insertions']} -{stats['deletions']})",
|
|
270
|
+
border_style="blue"
|
|
271
|
+
))
|
|
272
|
+
console.print()
|
|
273
|
+
|
|
274
|
+
# Syntax-highlighted diff
|
|
275
|
+
syntax = Syntax(diff_output, "diff", theme="monokai", line_numbers=False)
|
|
276
|
+
console.print(syntax)
|
|
277
|
+
|
|
278
|
+
# Interactive prompt
|
|
279
|
+
console.print()
|
|
280
|
+
console.print("[bold]Actions:[/]")
|
|
281
|
+
console.print(" [cyan]up diff --accept[/] Accept changes")
|
|
282
|
+
console.print(" [cyan]up diff --reject[/] Reject and reset")
|
|
283
|
+
|
|
284
|
+
except NotAGitRepoError:
|
|
285
|
+
console.print("[red]Error:[/] Not a git repository")
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
except Exception as e:
|
|
288
|
+
console.print(f"[red]Error:[/] {e}")
|
|
289
|
+
sys.exit(1)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# =============================================================================
|
|
293
|
+
# Command Group (for future expansion)
|
|
294
|
+
# =============================================================================
|
|
295
|
+
|
|
296
|
+
@click.group()
|
|
297
|
+
def vibe():
|
|
298
|
+
"""Vibe coding safety commands."""
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
vibe.add_command(save_cmd, name="save")
|
|
303
|
+
vibe.add_command(reset_cmd, name="reset")
|
|
304
|
+
vibe.add_command(diff_cmd, name="diff")
|