aline-ai 0.2.5__py3-none-any.whl → 0.3.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.
- {aline_ai-0.2.5.dist-info → aline_ai-0.3.0.dist-info}/METADATA +3 -1
- aline_ai-0.3.0.dist-info/RECORD +41 -0
- aline_ai-0.3.0.dist-info/entry_points.txt +3 -0
- realign/__init__.py +32 -1
- realign/cli.py +203 -19
- realign/commands/__init__.py +2 -2
- realign/commands/clean.py +149 -0
- realign/commands/config.py +1 -1
- realign/commands/export_shares.py +1785 -0
- realign/commands/hide.py +112 -24
- realign/commands/import_history.py +873 -0
- realign/commands/init.py +104 -217
- realign/commands/mirror.py +131 -0
- realign/commands/pull.py +101 -0
- realign/commands/push.py +155 -245
- realign/commands/review.py +216 -54
- realign/commands/session_utils.py +139 -4
- realign/commands/share.py +965 -0
- realign/commands/status.py +559 -0
- realign/commands/sync.py +91 -0
- realign/commands/undo.py +423 -0
- realign/commands/watcher.py +805 -0
- realign/config.py +21 -10
- realign/file_lock.py +3 -1
- realign/hash_registry.py +310 -0
- realign/hooks.py +368 -384
- realign/logging_config.py +2 -2
- realign/mcp_server.py +263 -549
- realign/mcp_watcher.py +999 -142
- realign/mirror_utils.py +322 -0
- realign/prompts/__init__.py +21 -0
- realign/prompts/presets.py +238 -0
- realign/redactor.py +168 -16
- realign/tracker/__init__.py +9 -0
- realign/tracker/git_tracker.py +1123 -0
- realign/watcher_daemon.py +115 -0
- aline_ai-0.2.5.dist-info/RECORD +0 -28
- aline_ai-0.2.5.dist-info/entry_points.txt +0 -5
- realign/commands/auto_commit.py +0 -231
- realign/commands/commit.py +0 -379
- realign/commands/search.py +0 -449
- realign/commands/show.py +0 -416
- {aline_ai-0.2.5.dist-info → aline_ai-0.3.0.dist-info}/WHEEL +0 -0
- {aline_ai-0.2.5.dist-info → aline_ai-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.2.5.dist-info → aline_ai-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ReAlign status command - Display system status and activity."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from ..config import ReAlignConfig
|
|
14
|
+
from ..hooks import find_all_active_sessions
|
|
15
|
+
from ..logging_config import setup_logger
|
|
16
|
+
from .review import get_unpushed_commits
|
|
17
|
+
|
|
18
|
+
# Initialize logger
|
|
19
|
+
logger = setup_logger('realign.status', 'status.log')
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ============================================================================
|
|
24
|
+
# Data Collection Functions
|
|
25
|
+
# ============================================================================
|
|
26
|
+
|
|
27
|
+
def check_initialization_status() -> Dict:
|
|
28
|
+
"""
|
|
29
|
+
Check if ReAlign is initialized in current directory.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
dict: Initialization status with paths and commit count
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
realign_path = Path.cwd() / '.realign'
|
|
36
|
+
if not realign_path.exists():
|
|
37
|
+
return {
|
|
38
|
+
"initialized": False,
|
|
39
|
+
"realign_path": None,
|
|
40
|
+
"project_path": None,
|
|
41
|
+
"total_commits": 0,
|
|
42
|
+
"has_git_mirror": False
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Check for git mirror
|
|
46
|
+
git_path = realign_path / '.git'
|
|
47
|
+
has_git_mirror = git_path.exists()
|
|
48
|
+
|
|
49
|
+
# Try to count commits
|
|
50
|
+
total_commits = 0
|
|
51
|
+
if has_git_mirror:
|
|
52
|
+
try:
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
['git', '-C', str(git_path), 'rev-list', '--count', 'HEAD'],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=2
|
|
58
|
+
)
|
|
59
|
+
if result.returncode == 0:
|
|
60
|
+
total_commits = int(result.stdout.strip())
|
|
61
|
+
except (subprocess.TimeoutExpired, ValueError, Exception) as e:
|
|
62
|
+
logger.warning(f"Failed to count commits: {e}")
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"initialized": True,
|
|
66
|
+
"realign_path": realign_path,
|
|
67
|
+
"project_path": Path.cwd(),
|
|
68
|
+
"total_commits": total_commits,
|
|
69
|
+
"has_git_mirror": has_git_mirror
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Error checking initialization: {e}", exc_info=True)
|
|
74
|
+
return {
|
|
75
|
+
"initialized": False,
|
|
76
|
+
"realign_path": None,
|
|
77
|
+
"project_path": None,
|
|
78
|
+
"total_commits": 0,
|
|
79
|
+
"has_git_mirror": False
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def detect_watcher_status() -> Dict:
|
|
84
|
+
"""
|
|
85
|
+
Detect if MCP watcher is running.
|
|
86
|
+
|
|
87
|
+
Uses multi-method detection:
|
|
88
|
+
1. Check for aline-mcp process (ps aux)
|
|
89
|
+
2. Check log freshness (< 5 mins = active)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
dict: Watcher status and configuration
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
# Load configuration
|
|
96
|
+
config = ReAlignConfig.load()
|
|
97
|
+
|
|
98
|
+
# Step 1: Check for process
|
|
99
|
+
is_process_running = False
|
|
100
|
+
pid = None
|
|
101
|
+
try:
|
|
102
|
+
ps_output = subprocess.run(
|
|
103
|
+
['ps', 'aux'],
|
|
104
|
+
capture_output=True,
|
|
105
|
+
text=True,
|
|
106
|
+
timeout=2
|
|
107
|
+
)
|
|
108
|
+
if ps_output.returncode == 0:
|
|
109
|
+
for line in ps_output.stdout.split('\n'):
|
|
110
|
+
if 'aline-mcp' in line:
|
|
111
|
+
is_process_running = True
|
|
112
|
+
# Try to extract PID (second column)
|
|
113
|
+
parts = line.split()
|
|
114
|
+
if len(parts) > 1:
|
|
115
|
+
try:
|
|
116
|
+
pid = int(parts[1])
|
|
117
|
+
except ValueError:
|
|
118
|
+
pass
|
|
119
|
+
break
|
|
120
|
+
except subprocess.TimeoutExpired:
|
|
121
|
+
logger.warning("Process check timed out")
|
|
122
|
+
|
|
123
|
+
# Step 2: Check log freshness
|
|
124
|
+
log_path = Path.home() / '.aline/.logs/mcp_watcher.log'
|
|
125
|
+
is_log_active = False
|
|
126
|
+
last_activity = None
|
|
127
|
+
|
|
128
|
+
if log_path.exists():
|
|
129
|
+
try:
|
|
130
|
+
last_modified = datetime.fromtimestamp(log_path.stat().st_mtime)
|
|
131
|
+
seconds_since_modified = (datetime.now() - last_modified).total_seconds()
|
|
132
|
+
is_log_active = seconds_since_modified < 300 # 5 mins
|
|
133
|
+
last_activity = last_modified
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.warning(f"Failed to check log timestamp: {e}")
|
|
136
|
+
|
|
137
|
+
# Step 3: Determine status
|
|
138
|
+
if is_process_running and is_log_active:
|
|
139
|
+
status = "Running"
|
|
140
|
+
elif not is_process_running:
|
|
141
|
+
status = "Stopped"
|
|
142
|
+
else:
|
|
143
|
+
status = "Inactive" # Process exists but log is stale
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"status": status,
|
|
147
|
+
"pid": pid,
|
|
148
|
+
"auto_commit_enabled": config.mcp_auto_commit,
|
|
149
|
+
"debounce_delay": 2.0, # Hardcoded from mcp_watcher.py
|
|
150
|
+
"cooldown": 5.0, # Hardcoded from mcp_watcher.py
|
|
151
|
+
"last_activity": last_activity
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Error detecting watcher status: {e}", exc_info=True)
|
|
156
|
+
return {
|
|
157
|
+
"status": "Unknown",
|
|
158
|
+
"pid": None,
|
|
159
|
+
"auto_commit_enabled": False,
|
|
160
|
+
"debounce_delay": 2.0,
|
|
161
|
+
"cooldown": 5.0,
|
|
162
|
+
"last_activity": None
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_session_information() -> Dict:
|
|
167
|
+
"""
|
|
168
|
+
Detect active sessions from Claude Code and Codex.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
dict: Session paths and latest session info
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
config = ReAlignConfig.load()
|
|
175
|
+
project_path = Path.cwd()
|
|
176
|
+
|
|
177
|
+
# Find all active sessions
|
|
178
|
+
all_sessions = find_all_active_sessions(config, project_path)
|
|
179
|
+
|
|
180
|
+
# Separate by type (Claude vs Codex)
|
|
181
|
+
claude_sessions = []
|
|
182
|
+
codex_sessions = []
|
|
183
|
+
|
|
184
|
+
for session in all_sessions:
|
|
185
|
+
session_str = str(session)
|
|
186
|
+
if '.claude/projects/' in session_str:
|
|
187
|
+
claude_sessions.append(session)
|
|
188
|
+
elif '.codex/sessions/' in session_str:
|
|
189
|
+
codex_sessions.append(session)
|
|
190
|
+
else:
|
|
191
|
+
# Default to Claude if unclear
|
|
192
|
+
claude_sessions.append(session)
|
|
193
|
+
|
|
194
|
+
# Get latest session
|
|
195
|
+
latest_session = None
|
|
196
|
+
latest_session_time = None
|
|
197
|
+
|
|
198
|
+
if all_sessions:
|
|
199
|
+
try:
|
|
200
|
+
latest_session = max(all_sessions, key=lambda f: f.stat().st_mtime)
|
|
201
|
+
latest_session_time = datetime.fromtimestamp(latest_session.stat().st_mtime)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.warning(f"Failed to determine latest session: {e}")
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"claude_sessions": claude_sessions,
|
|
207
|
+
"codex_sessions": codex_sessions,
|
|
208
|
+
"latest_session": latest_session,
|
|
209
|
+
"latest_session_time": latest_session_time
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"Error getting session information: {e}", exc_info=True)
|
|
214
|
+
return {
|
|
215
|
+
"claude_sessions": [],
|
|
216
|
+
"codex_sessions": [],
|
|
217
|
+
"latest_session": None,
|
|
218
|
+
"latest_session_time": None
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_configuration_summary() -> Dict:
|
|
223
|
+
"""
|
|
224
|
+
Extract key configuration values.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
dict: Configuration summary
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
config = ReAlignConfig.load()
|
|
231
|
+
return {
|
|
232
|
+
"llm_provider": config.llm_provider,
|
|
233
|
+
"use_llm": config.use_LLM,
|
|
234
|
+
"redact_on_match": config.redact_on_match,
|
|
235
|
+
"hooks_installation": config.hooks_installation,
|
|
236
|
+
"auto_detect_claude": config.auto_detect_claude,
|
|
237
|
+
"auto_detect_codex": config.auto_detect_codex
|
|
238
|
+
}
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Error getting configuration: {e}", exc_info=True)
|
|
241
|
+
return {
|
|
242
|
+
"llm_provider": "unknown",
|
|
243
|
+
"use_llm": False,
|
|
244
|
+
"redact_on_match": False,
|
|
245
|
+
"hooks_installation": "unknown",
|
|
246
|
+
"auto_detect_claude": False,
|
|
247
|
+
"auto_detect_codex": False
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_recent_activity() -> Dict:
|
|
252
|
+
"""
|
|
253
|
+
Get recent commit and session statistics.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
dict: Recent activity information
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
realign_path = Path.cwd() / '.realign'
|
|
260
|
+
|
|
261
|
+
# Latest commit from .realign/.git
|
|
262
|
+
latest_commit_hash = None
|
|
263
|
+
latest_commit_message = None
|
|
264
|
+
latest_commit_time = None
|
|
265
|
+
|
|
266
|
+
git_path = realign_path / '.git'
|
|
267
|
+
if git_path.exists():
|
|
268
|
+
try:
|
|
269
|
+
result = subprocess.run(
|
|
270
|
+
['git', '-C', str(git_path), 'log', '-1', '--format=%h|%s|%ct'],
|
|
271
|
+
capture_output=True,
|
|
272
|
+
text=True,
|
|
273
|
+
timeout=2
|
|
274
|
+
)
|
|
275
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
276
|
+
parts = result.stdout.strip().split('|')
|
|
277
|
+
if len(parts) >= 3:
|
|
278
|
+
latest_commit_hash = parts[0]
|
|
279
|
+
latest_commit_message = parts[1]
|
|
280
|
+
latest_commit_time = datetime.fromtimestamp(int(parts[2]))
|
|
281
|
+
except (subprocess.TimeoutExpired, ValueError, Exception) as e:
|
|
282
|
+
logger.warning(f"Failed to get latest commit: {e}")
|
|
283
|
+
|
|
284
|
+
# Unpushed commits count
|
|
285
|
+
unpushed_count = 0
|
|
286
|
+
try:
|
|
287
|
+
unpushed_commits = get_unpushed_commits()
|
|
288
|
+
unpushed_count = len(unpushed_commits)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.warning(f"Failed to get unpushed commits: {e}")
|
|
291
|
+
|
|
292
|
+
# Total sessions tracked
|
|
293
|
+
total_sessions = 0
|
|
294
|
+
sessions_path = realign_path / 'sessions'
|
|
295
|
+
if sessions_path.exists():
|
|
296
|
+
try:
|
|
297
|
+
total_sessions = len(list(sessions_path.glob('*.jsonl')))
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.warning(f"Failed to count sessions: {e}")
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
"latest_commit_hash": latest_commit_hash,
|
|
303
|
+
"latest_commit_message": latest_commit_message,
|
|
304
|
+
"latest_commit_time": latest_commit_time,
|
|
305
|
+
"unpushed_count": unpushed_count,
|
|
306
|
+
"total_sessions": total_sessions
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Error getting recent activity: {e}", exc_info=True)
|
|
311
|
+
return {
|
|
312
|
+
"latest_commit_hash": None,
|
|
313
|
+
"latest_commit_message": None,
|
|
314
|
+
"latest_commit_time": None,
|
|
315
|
+
"unpushed_count": 0,
|
|
316
|
+
"total_sessions": 0
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def collect_all_status_data() -> Dict:
|
|
321
|
+
"""
|
|
322
|
+
Collect all status information from all data functions.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
dict: Complete status data
|
|
326
|
+
"""
|
|
327
|
+
return {
|
|
328
|
+
"init": check_initialization_status(),
|
|
329
|
+
"watcher": detect_watcher_status(),
|
|
330
|
+
"sessions": get_session_information(),
|
|
331
|
+
"config": get_configuration_summary(),
|
|
332
|
+
"activity": get_recent_activity()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ============================================================================
|
|
337
|
+
# Helper Functions
|
|
338
|
+
# ============================================================================
|
|
339
|
+
|
|
340
|
+
def format_time_with_relative(dt: datetime) -> str:
|
|
341
|
+
"""
|
|
342
|
+
Format datetime with both absolute and relative time.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
dt: Datetime to format
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
str: Formatted time string (e.g., "2025-11-29 14:23:45 (2 mins ago)")
|
|
349
|
+
"""
|
|
350
|
+
absolute = dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
351
|
+
|
|
352
|
+
# Calculate relative time
|
|
353
|
+
now = datetime.now()
|
|
354
|
+
diff = now - dt
|
|
355
|
+
total_seconds = diff.total_seconds()
|
|
356
|
+
|
|
357
|
+
if total_seconds < 60:
|
|
358
|
+
relative = "just now"
|
|
359
|
+
elif total_seconds < 3600:
|
|
360
|
+
mins = int(total_seconds // 60)
|
|
361
|
+
relative = f"{mins} min{'s' if mins != 1 else ''} ago"
|
|
362
|
+
elif diff.days == 0:
|
|
363
|
+
hours = int(total_seconds // 3600)
|
|
364
|
+
relative = f"{hours} hour{'s' if hours != 1 else ''} ago"
|
|
365
|
+
elif diff.days < 7:
|
|
366
|
+
relative = f"{diff.days} day{'s' if diff.days != 1 else ''} ago"
|
|
367
|
+
else:
|
|
368
|
+
weeks = diff.days // 7
|
|
369
|
+
relative = f"{weeks} week{'s' if weeks != 1 else ''} ago"
|
|
370
|
+
|
|
371
|
+
return f"{absolute} ({relative})"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def abbreviate_path(path: Path) -> str:
|
|
375
|
+
"""
|
|
376
|
+
Abbreviate home directory with ~.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
path: Path to abbreviate
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
str: Abbreviated path
|
|
383
|
+
"""
|
|
384
|
+
home = Path.home()
|
|
385
|
+
try:
|
|
386
|
+
relative = path.relative_to(home)
|
|
387
|
+
return f"~/{relative}"
|
|
388
|
+
except ValueError:
|
|
389
|
+
# Path is not under home directory
|
|
390
|
+
return str(path)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ============================================================================
|
|
394
|
+
# Display Function
|
|
395
|
+
# ============================================================================
|
|
396
|
+
|
|
397
|
+
def display_status(data: Dict, is_watch: bool = False) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Display status using Rich library.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
data: Status data from collect_all_status_data()
|
|
403
|
+
is_watch: Whether in watch mode
|
|
404
|
+
"""
|
|
405
|
+
# Title panel
|
|
406
|
+
console.print(Panel("ReAlign System Status", style="bold cyan"))
|
|
407
|
+
|
|
408
|
+
# Section 1: Initialization
|
|
409
|
+
console.print("\n[bold cyan][1] Initialization[/bold cyan]")
|
|
410
|
+
if data['init']['initialized']:
|
|
411
|
+
console.print(f" Status: Initialized [green]✓[/green]")
|
|
412
|
+
console.print(f" Path: {data['init']['project_path']}")
|
|
413
|
+
console.print(f" Shadow Repository: .realign/.git ({data['init']['total_commits']} commits)")
|
|
414
|
+
else:
|
|
415
|
+
console.print(f" Status: Not initialized [red]✗[/red]")
|
|
416
|
+
console.print(f"\n [dim]ReAlign is not initialized in this directory.[/dim]")
|
|
417
|
+
console.print(f" [dim]Run 'aline init' to set up session tracking.[/dim]")
|
|
418
|
+
return # Stop here, don't show other sections
|
|
419
|
+
|
|
420
|
+
# Section 2: Watcher
|
|
421
|
+
console.print("\n[bold cyan][2] Watcher[/bold cyan]")
|
|
422
|
+
status = data['watcher']['status']
|
|
423
|
+
if status == "Running":
|
|
424
|
+
console.print(f" Status: Running [green]✓[/green]")
|
|
425
|
+
elif status == "Stopped":
|
|
426
|
+
console.print(f" Status: Stopped [red]✗[/red]")
|
|
427
|
+
else:
|
|
428
|
+
console.print(f" Status: Inactive [yellow]✗[/yellow]")
|
|
429
|
+
console.print(f" [dim]Suggestion: Restart MCP server[/dim]")
|
|
430
|
+
|
|
431
|
+
# Auto-commit status
|
|
432
|
+
if data['watcher']['auto_commit_enabled']:
|
|
433
|
+
console.print(f" Auto-commit: [green]Enabled[/green]")
|
|
434
|
+
else:
|
|
435
|
+
console.print(f" Auto-commit: [red]Disabled[/red]")
|
|
436
|
+
|
|
437
|
+
# Timing info
|
|
438
|
+
console.print(f" Timing: Debounce {data['watcher']['debounce_delay']}s | Cooldown {data['watcher']['cooldown']}s")
|
|
439
|
+
|
|
440
|
+
# Last activity (if available)
|
|
441
|
+
if data['watcher']['last_activity']:
|
|
442
|
+
time_str = format_time_with_relative(data['watcher']['last_activity'])
|
|
443
|
+
console.print(f" Last Activity: {time_str}")
|
|
444
|
+
|
|
445
|
+
# Section 3: Sessions
|
|
446
|
+
console.print("\n[bold cyan][3] Active Sessions[/bold cyan]")
|
|
447
|
+
claude_count = len(data['sessions']['claude_sessions'])
|
|
448
|
+
codex_count = len(data['sessions']['codex_sessions'])
|
|
449
|
+
|
|
450
|
+
if claude_count > 0:
|
|
451
|
+
console.print(f" Claude Code: {claude_count} session(s)")
|
|
452
|
+
for session in data['sessions']['claude_sessions'][:3]: # Show max 3
|
|
453
|
+
abbreviated_path = abbreviate_path(session)
|
|
454
|
+
console.print(f" {abbreviated_path}")
|
|
455
|
+
if claude_count > 3:
|
|
456
|
+
console.print(f" [dim]... and {claude_count - 3} more[/dim]")
|
|
457
|
+
else:
|
|
458
|
+
console.print(f" Claude Code: [dim]No sessions found[/dim]")
|
|
459
|
+
|
|
460
|
+
if codex_count > 0:
|
|
461
|
+
console.print(f" Codex: {codex_count} session(s)")
|
|
462
|
+
for session in data['sessions']['codex_sessions'][:3]:
|
|
463
|
+
abbreviated_path = abbreviate_path(session)
|
|
464
|
+
console.print(f" {abbreviated_path}")
|
|
465
|
+
if codex_count > 3:
|
|
466
|
+
console.print(f" [dim]... and {codex_count - 3} more[/dim]")
|
|
467
|
+
else:
|
|
468
|
+
console.print(f" Codex: [dim]No sessions found[/dim]")
|
|
469
|
+
|
|
470
|
+
# Section 4: Configuration
|
|
471
|
+
console.print("\n[bold cyan][4] Configuration[/bold cyan]")
|
|
472
|
+
|
|
473
|
+
# LLM Provider
|
|
474
|
+
llm_status = "[green]Enabled[/green]" if data['config']['use_llm'] else "[red]Disabled[/red]"
|
|
475
|
+
console.print(f" LLM Provider: {data['config']['llm_provider']} ({llm_status})")
|
|
476
|
+
|
|
477
|
+
# Redaction
|
|
478
|
+
redaction_status = "[green]Enabled[/green]" if data['config']['redact_on_match'] else "[red]Disabled[/red]"
|
|
479
|
+
console.print(f" Redaction: {redaction_status}")
|
|
480
|
+
|
|
481
|
+
# Hook Mode
|
|
482
|
+
console.print(f" Hook Mode: {data['config']['hooks_installation']}")
|
|
483
|
+
|
|
484
|
+
# Section 5: Recent Activity
|
|
485
|
+
console.print("\n[bold cyan][5] Recent Activity[/bold cyan]")
|
|
486
|
+
|
|
487
|
+
# Latest commit
|
|
488
|
+
if data['activity']['latest_commit_hash']:
|
|
489
|
+
commit_msg = data['activity']['latest_commit_message']
|
|
490
|
+
# Extract turn number if present
|
|
491
|
+
if 'Turn' in commit_msg:
|
|
492
|
+
# Format: "Session xxx, Turn N: message"
|
|
493
|
+
parts = commit_msg.split(': ', 1)
|
|
494
|
+
if len(parts) == 2:
|
|
495
|
+
commit_msg = parts[1][:50] # Truncate message
|
|
496
|
+
else:
|
|
497
|
+
commit_msg = commit_msg[:50]
|
|
498
|
+
|
|
499
|
+
time_str = format_time_with_relative(data['activity']['latest_commit_time'])
|
|
500
|
+
console.print(f" Latest Commit: {data['activity']['latest_commit_hash']} - {commit_msg}")
|
|
501
|
+
console.print(f" {time_str}")
|
|
502
|
+
else:
|
|
503
|
+
console.print(f" Latest Commit: [dim]No commits yet[/dim]")
|
|
504
|
+
|
|
505
|
+
# Unpushed commits
|
|
506
|
+
unpushed = data['activity']['unpushed_count']
|
|
507
|
+
if unpushed > 0:
|
|
508
|
+
console.print(f" Unpushed: {unpushed} commit{'s' if unpushed != 1 else ''}")
|
|
509
|
+
else:
|
|
510
|
+
console.print(f" Unpushed: [dim]None[/dim]")
|
|
511
|
+
|
|
512
|
+
# Sessions tracked
|
|
513
|
+
total_sessions = data['activity']['total_sessions']
|
|
514
|
+
console.print(f" Sessions Tracked: {total_sessions}")
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
# ============================================================================
|
|
518
|
+
# Main Command Function
|
|
519
|
+
# ============================================================================
|
|
520
|
+
|
|
521
|
+
def status_command(watch: bool = False) -> int:
|
|
522
|
+
"""
|
|
523
|
+
Main entry point for status command.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
watch: Enable watch mode (continuous refresh)
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
int: Exit code (0 = success, 1 = error)
|
|
530
|
+
"""
|
|
531
|
+
try:
|
|
532
|
+
if watch:
|
|
533
|
+
# Watch mode: continuous refresh
|
|
534
|
+
while True:
|
|
535
|
+
console.clear()
|
|
536
|
+
console.print(f"[dim]Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}[/dim]\n")
|
|
537
|
+
|
|
538
|
+
# Collect all data
|
|
539
|
+
data = collect_all_status_data()
|
|
540
|
+
|
|
541
|
+
# Display
|
|
542
|
+
display_status(data, is_watch=True)
|
|
543
|
+
|
|
544
|
+
# Wait 2 seconds
|
|
545
|
+
time.sleep(2)
|
|
546
|
+
else:
|
|
547
|
+
# Single display
|
|
548
|
+
data = collect_all_status_data()
|
|
549
|
+
display_status(data, is_watch=False)
|
|
550
|
+
|
|
551
|
+
return 0
|
|
552
|
+
|
|
553
|
+
except KeyboardInterrupt:
|
|
554
|
+
console.print("\n[yellow]Status monitoring stopped[/yellow]")
|
|
555
|
+
return 0
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.error(f"Error in status command: {e}", exc_info=True)
|
|
558
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
559
|
+
return 1
|
realign/commands/sync.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Sync command - Bidirectional synchronization (pull + push)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..tracker.git_tracker import ReAlignGitTracker
|
|
8
|
+
from ..logging_config import setup_logger
|
|
9
|
+
|
|
10
|
+
logger = setup_logger('realign.commands.sync', 'sync.log')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sync_command(repo_root: Optional[Path] = None) -> int:
|
|
14
|
+
"""
|
|
15
|
+
Synchronize with remote repository (pull then push).
|
|
16
|
+
|
|
17
|
+
This command performs a bidirectional sync:
|
|
18
|
+
1. Pulls updates from remote
|
|
19
|
+
2. Pushes local commits to remote
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
repo_root: Path to repository root (uses cwd if not provided)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Exit code (0 for success, 1 for error)
|
|
26
|
+
"""
|
|
27
|
+
# Get project root
|
|
28
|
+
if repo_root is None:
|
|
29
|
+
repo_root = Path(os.getcwd()).resolve()
|
|
30
|
+
|
|
31
|
+
# Initialize tracker
|
|
32
|
+
tracker = ReAlignGitTracker(repo_root)
|
|
33
|
+
|
|
34
|
+
# Check if repository is initialized
|
|
35
|
+
if not tracker.is_initialized():
|
|
36
|
+
print("❌ Repository not initialized")
|
|
37
|
+
print("Run 'aline init' first")
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
# Check if remote is configured
|
|
41
|
+
if not tracker.has_remote():
|
|
42
|
+
print("❌ No remote configured")
|
|
43
|
+
print("\nTo join a shared repository:")
|
|
44
|
+
print(" aline init --join <repo>")
|
|
45
|
+
print("\nOr to set up sharing:")
|
|
46
|
+
print(" aline init --share")
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
remote_url = tracker.get_remote_url()
|
|
50
|
+
print(f"Synchronizing with: {remote_url}")
|
|
51
|
+
print()
|
|
52
|
+
|
|
53
|
+
# Step 1: Pull from remote
|
|
54
|
+
print("[1/2] Pulling from remote...")
|
|
55
|
+
|
|
56
|
+
pull_success = tracker.safe_pull()
|
|
57
|
+
|
|
58
|
+
if not pull_success:
|
|
59
|
+
print("❌ Pull failed")
|
|
60
|
+
print("\nSync aborted. Fix pull issues before syncing.")
|
|
61
|
+
print("Check logs: .realign/logs/sync.log")
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
print("✓ Pull completed")
|
|
65
|
+
print()
|
|
66
|
+
|
|
67
|
+
# Step 2: Push to remote
|
|
68
|
+
print("[2/2] Pushing to remote...")
|
|
69
|
+
|
|
70
|
+
# Get unpushed commits
|
|
71
|
+
unpushed = tracker.get_unpushed_commits()
|
|
72
|
+
|
|
73
|
+
if not unpushed:
|
|
74
|
+
print("✓ No commits to push")
|
|
75
|
+
print("\n✓ Synchronization complete")
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
print(f"Found {len(unpushed)} unpushed commit(s)")
|
|
79
|
+
|
|
80
|
+
push_success = tracker.safe_push()
|
|
81
|
+
|
|
82
|
+
if push_success:
|
|
83
|
+
print(f"✓ Pushed {len(unpushed)} commit(s)")
|
|
84
|
+
print("\n✓ Synchronization complete")
|
|
85
|
+
return 0
|
|
86
|
+
else:
|
|
87
|
+
print("❌ Push failed")
|
|
88
|
+
print("\nPull succeeded, but push failed.")
|
|
89
|
+
print("You can try 'aline push' separately")
|
|
90
|
+
print("Check logs: .realign/logs/sync.log")
|
|
91
|
+
return 1
|