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.
Files changed (55) hide show
  1. up/__init__.py +1 -1
  2. up/ai_cli.py +229 -0
  3. up/cli.py +75 -4
  4. up/commands/agent.py +521 -0
  5. up/commands/bisect.py +343 -0
  6. up/commands/branch.py +350 -0
  7. up/commands/dashboard.py +248 -0
  8. up/commands/init.py +195 -6
  9. up/commands/learn.py +1741 -0
  10. up/commands/memory.py +545 -0
  11. up/commands/new.py +108 -10
  12. up/commands/provenance.py +267 -0
  13. up/commands/review.py +239 -0
  14. up/commands/start.py +1124 -0
  15. up/commands/status.py +360 -0
  16. up/commands/summarize.py +122 -0
  17. up/commands/sync.py +317 -0
  18. up/commands/vibe.py +304 -0
  19. up/context.py +421 -0
  20. up/core/__init__.py +69 -0
  21. up/core/checkpoint.py +479 -0
  22. up/core/provenance.py +364 -0
  23. up/core/state.py +678 -0
  24. up/events.py +512 -0
  25. up/git/__init__.py +37 -0
  26. up/git/utils.py +270 -0
  27. up/git/worktree.py +331 -0
  28. up/learn/__init__.py +155 -0
  29. up/learn/analyzer.py +227 -0
  30. up/learn/plan.py +374 -0
  31. up/learn/research.py +511 -0
  32. up/learn/utils.py +117 -0
  33. up/memory.py +1096 -0
  34. up/parallel.py +551 -0
  35. up/summarizer.py +407 -0
  36. up/templates/__init__.py +70 -2
  37. up/templates/config/__init__.py +502 -20
  38. up/templates/docs/SKILL.md +28 -0
  39. up/templates/docs/__init__.py +341 -0
  40. up/templates/docs/standards/HEADERS.md +24 -0
  41. up/templates/docs/standards/STRUCTURE.md +18 -0
  42. up/templates/docs/standards/TEMPLATES.md +19 -0
  43. up/templates/learn/__init__.py +567 -14
  44. up/templates/loop/__init__.py +546 -27
  45. up/templates/mcp/__init__.py +474 -0
  46. up/templates/projects/__init__.py +786 -0
  47. up/ui/__init__.py +14 -0
  48. up/ui/loop_display.py +650 -0
  49. up/ui/theme.py +137 -0
  50. up_cli-0.5.0.dist-info/METADATA +519 -0
  51. up_cli-0.5.0.dist-info/RECORD +55 -0
  52. up_cli-0.1.1.dist-info/METADATA +0 -186
  53. up_cli-0.1.1.dist-info/RECORD +0 -14
  54. {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
  55. {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")