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/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/init.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""up init - Initialize up systems in existing project."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import stat
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import click
|
|
@@ -27,13 +28,32 @@ console = Console()
|
|
|
27
28
|
default=["all"],
|
|
28
29
|
help="Systems to initialize",
|
|
29
30
|
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--hooks/--no-hooks",
|
|
33
|
+
default=True,
|
|
34
|
+
help="Install git hooks for auto-sync (default: yes)",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--memory/--no-memory",
|
|
38
|
+
default=True,
|
|
39
|
+
help="Build initial memory from git history (default: yes)",
|
|
40
|
+
)
|
|
30
41
|
@click.option(
|
|
31
42
|
"--force",
|
|
32
43
|
is_flag=True,
|
|
33
44
|
help="Overwrite existing files",
|
|
34
45
|
)
|
|
35
|
-
def init_cmd(ai: str, systems: tuple, force: bool):
|
|
36
|
-
"""Initialize up systems in the current directory.
|
|
46
|
+
def init_cmd(ai: str, systems: tuple, hooks: bool, memory: bool, force: bool):
|
|
47
|
+
"""Initialize up systems in the current directory.
|
|
48
|
+
|
|
49
|
+
Automatically:
|
|
50
|
+
- Installs git hooks for memory auto-sync
|
|
51
|
+
- Indexes existing git history to memory
|
|
52
|
+
- Scans project structure for context
|
|
53
|
+
|
|
54
|
+
Use --no-hooks to disable hooks.
|
|
55
|
+
Use --no-memory to skip initial memory build.
|
|
56
|
+
"""
|
|
37
57
|
cwd = Path.cwd()
|
|
38
58
|
|
|
39
59
|
console.print(Panel.fit(
|
|
@@ -53,11 +73,177 @@ def init_cmd(ai: str, systems: tuple, force: bool):
|
|
|
53
73
|
force=force,
|
|
54
74
|
)
|
|
55
75
|
|
|
76
|
+
# Install git hooks automatically
|
|
77
|
+
hooks_installed = False
|
|
78
|
+
if hooks:
|
|
79
|
+
hooks_installed = _install_git_hooks(cwd)
|
|
80
|
+
|
|
81
|
+
# Build initial memory from existing project
|
|
82
|
+
memory_stats = None
|
|
83
|
+
if memory:
|
|
84
|
+
memory_stats = _build_initial_memory(cwd)
|
|
85
|
+
|
|
56
86
|
console.print("\n[green]✓[/] Initialization complete!")
|
|
57
|
-
|
|
87
|
+
|
|
88
|
+
if hooks_installed:
|
|
89
|
+
console.print("[green]✓[/] Git hooks installed for auto-sync")
|
|
90
|
+
|
|
91
|
+
if memory_stats:
|
|
92
|
+
console.print(f"[green]✓[/] Memory initialized ({memory_stats['total']} entries)")
|
|
93
|
+
|
|
94
|
+
_print_next_steps(systems, hooks_installed)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_initial_memory(workspace: Path) -> dict:
|
|
98
|
+
"""Build initial memory from existing project.
|
|
99
|
+
|
|
100
|
+
Indexes:
|
|
101
|
+
- Existing git commits (up to 50)
|
|
102
|
+
- Recent file changes
|
|
103
|
+
- Project structure metadata
|
|
104
|
+
|
|
105
|
+
Returns stats dict or None if failed.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
import os
|
|
109
|
+
import warnings
|
|
110
|
+
from up.memory import MemoryManager
|
|
111
|
+
|
|
112
|
+
# Suppress noisy warnings from tokenizers
|
|
113
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
114
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
115
|
+
|
|
116
|
+
console.print("\n[dim]Building initial memory...[/]")
|
|
117
|
+
|
|
118
|
+
manager = MemoryManager(workspace, use_vectors=False) # Use fast JSON backend
|
|
119
|
+
|
|
120
|
+
# Index existing commits
|
|
121
|
+
commits = manager.index_recent_commits(count=50)
|
|
122
|
+
console.print(f" [dim]Indexed {commits} commits[/]")
|
|
123
|
+
|
|
124
|
+
# Index recent file changes
|
|
125
|
+
files = manager.index_file_changes()
|
|
126
|
+
console.print(f" [dim]Indexed {files} files[/]")
|
|
127
|
+
|
|
128
|
+
# Record project initialization
|
|
129
|
+
import subprocess
|
|
130
|
+
|
|
131
|
+
# Get project info
|
|
132
|
+
project_name = workspace.name
|
|
133
|
+
|
|
134
|
+
# Detect languages/frameworks
|
|
135
|
+
languages = []
|
|
136
|
+
if (workspace / "pyproject.toml").exists() or (workspace / "requirements.txt").exists():
|
|
137
|
+
languages.append("Python")
|
|
138
|
+
if (workspace / "package.json").exists():
|
|
139
|
+
languages.append("JavaScript/TypeScript")
|
|
140
|
+
if (workspace / "go.mod").exists():
|
|
141
|
+
languages.append("Go")
|
|
142
|
+
if (workspace / "Cargo.toml").exists():
|
|
143
|
+
languages.append("Rust")
|
|
144
|
+
|
|
145
|
+
# Record initialization as a learning
|
|
146
|
+
if languages:
|
|
147
|
+
manager.record_learning(
|
|
148
|
+
f"Project '{project_name}' initialized with up-cli. "
|
|
149
|
+
f"Languages detected: {', '.join(languages)}. "
|
|
150
|
+
f"Indexed {commits} commits and {files} files into memory."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
stats = manager.get_stats()
|
|
154
|
+
return {
|
|
155
|
+
"total": stats.get("total", 0),
|
|
156
|
+
"commits": commits,
|
|
157
|
+
"files": files,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
console.print(f" [yellow]Warning: Could not build memory: {e}[/]")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _install_git_hooks(workspace: Path) -> bool:
|
|
166
|
+
"""Install git hooks for automatic memory sync.
|
|
167
|
+
|
|
168
|
+
Returns True if hooks were installed successfully.
|
|
169
|
+
"""
|
|
170
|
+
git_dir = workspace / ".git"
|
|
171
|
+
|
|
172
|
+
if not git_dir.exists():
|
|
173
|
+
console.print("[yellow]⚠[/] Not a git repo, skipping hooks")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
hooks_dir = git_dir / "hooks"
|
|
177
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
178
|
+
|
|
179
|
+
# Post-commit hook
|
|
180
|
+
post_commit = hooks_dir / "post-commit"
|
|
181
|
+
post_commit_content = '''#!/bin/bash
|
|
182
|
+
# up-cli auto-sync hook (installed by up init)
|
|
183
|
+
# Indexes commits to memory automatically
|
|
184
|
+
|
|
185
|
+
# Run in background to not slow down commits
|
|
186
|
+
(
|
|
187
|
+
sleep 1
|
|
188
|
+
|
|
189
|
+
# Sync memory with latest commit
|
|
190
|
+
if command -v up &> /dev/null; then
|
|
191
|
+
up memory sync 2>/dev/null
|
|
192
|
+
elif command -v python3 &> /dev/null; then
|
|
193
|
+
python3 -m up.memory sync 2>/dev/null
|
|
194
|
+
fi
|
|
195
|
+
) &
|
|
196
|
+
|
|
197
|
+
exit 0
|
|
198
|
+
'''
|
|
199
|
+
|
|
200
|
+
_write_hook(post_commit, post_commit_content)
|
|
201
|
+
|
|
202
|
+
# Post-checkout hook (for branch switches)
|
|
203
|
+
post_checkout = hooks_dir / "post-checkout"
|
|
204
|
+
post_checkout_content = '''#!/bin/bash
|
|
205
|
+
# up-cli context update hook (installed by up init)
|
|
206
|
+
# Updates context when switching branches
|
|
207
|
+
|
|
208
|
+
PREV_HEAD=$1
|
|
209
|
+
NEW_HEAD=$2
|
|
210
|
+
BRANCH_CHECKOUT=$3
|
|
211
|
+
|
|
212
|
+
# Only run on branch checkout, not file checkout
|
|
213
|
+
if [ "$BRANCH_CHECKOUT" = "1" ]; then
|
|
214
|
+
(
|
|
215
|
+
sleep 1
|
|
216
|
+
if command -v up &> /dev/null; then
|
|
217
|
+
up sync --no-memory 2>/dev/null
|
|
218
|
+
fi
|
|
219
|
+
) &
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
exit 0
|
|
223
|
+
'''
|
|
224
|
+
|
|
225
|
+
_write_hook(post_checkout, post_checkout_content)
|
|
226
|
+
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _write_hook(path: Path, content: str):
|
|
231
|
+
"""Write hook file with executable permissions."""
|
|
232
|
+
# Check for existing hook
|
|
233
|
+
if path.exists():
|
|
234
|
+
existing = path.read_text()
|
|
235
|
+
if "up-cli" in existing:
|
|
236
|
+
# Already our hook, overwrite
|
|
237
|
+
pass
|
|
238
|
+
else:
|
|
239
|
+
# User has custom hook, append
|
|
240
|
+
content = existing + "\n\n" + content
|
|
241
|
+
|
|
242
|
+
path.write_text(content)
|
|
243
|
+
path.chmod(path.stat().st_mode | stat.S_IEXEC)
|
|
58
244
|
|
|
59
245
|
|
|
60
|
-
def _print_next_steps(systems: tuple):
|
|
246
|
+
def _print_next_steps(systems: tuple, hooks_installed: bool):
|
|
61
247
|
"""Print next steps after initialization."""
|
|
62
248
|
console.print("\n[bold]Next steps:[/]")
|
|
63
249
|
|
|
@@ -65,7 +251,10 @@ def _print_next_steps(systems: tuple):
|
|
|
65
251
|
console.print(" • Edit [cyan]docs/roadmap/vision/PRODUCT_VISION.md[/]")
|
|
66
252
|
|
|
67
253
|
if "learn" in systems:
|
|
68
|
-
console.print(" • Run [cyan]
|
|
254
|
+
console.print(" • Run [cyan]up learn auto[/] to analyze your project")
|
|
69
255
|
|
|
70
256
|
if "loop" in systems:
|
|
71
|
-
console.print(" • Run [cyan]
|
|
257
|
+
console.print(" • Run [cyan]up start[/] to start development")
|
|
258
|
+
|
|
259
|
+
if hooks_installed:
|
|
260
|
+
console.print("\n[dim]Auto-sync enabled: commits will be indexed automatically[/]")
|