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
realign/commands/init.py
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Aline init command - Initialize Aline tracking system."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import subprocess
|
|
5
|
-
import yaml
|
|
6
5
|
from pathlib import Path
|
|
7
|
-
from typing import
|
|
6
|
+
from typing import Dict, Any
|
|
8
7
|
import typer
|
|
9
8
|
from rich.console import Console
|
|
10
|
-
from rich.prompt import Confirm
|
|
11
9
|
|
|
12
10
|
from ..config import ReAlignConfig, get_default_config_content
|
|
11
|
+
from realign import get_realign_dir
|
|
13
12
|
|
|
14
13
|
console = Console()
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
def init_repository(
|
|
18
17
|
repo_path: str = ".",
|
|
19
|
-
auto_init_git: bool = True,
|
|
20
|
-
skip_commit: bool = False,
|
|
21
18
|
force: bool = False,
|
|
22
19
|
) -> Dict[str, Any]:
|
|
23
20
|
"""
|
|
@@ -25,9 +22,7 @@ def init_repository(
|
|
|
25
22
|
|
|
26
23
|
Args:
|
|
27
24
|
repo_path: Path to the repository to initialize
|
|
28
|
-
|
|
29
|
-
skip_commit: Skip auto-commit of hooks
|
|
30
|
-
force: Force re-initialization and update all hooks even if they exist
|
|
25
|
+
force: Force re-initialization even if .aline already exists
|
|
31
26
|
|
|
32
27
|
Returns:
|
|
33
28
|
Dictionary with initialization results and metadata
|
|
@@ -37,10 +32,9 @@ def init_repository(
|
|
|
37
32
|
"repo_path": None,
|
|
38
33
|
"repo_root": None,
|
|
39
34
|
"realign_dir": None,
|
|
40
|
-
"hooks_created": [],
|
|
41
35
|
"config_path": None,
|
|
42
36
|
"history_dir": None,
|
|
43
|
-
"
|
|
37
|
+
"realign_git_initialized": False,
|
|
44
38
|
"message": "",
|
|
45
39
|
"errors": [],
|
|
46
40
|
}
|
|
@@ -54,77 +48,38 @@ def init_repository(
|
|
|
54
48
|
result["errors"].append(f"Failed to change to directory {repo_path}: {e}")
|
|
55
49
|
result["message"] = "Failed to access target directory"
|
|
56
50
|
return result
|
|
51
|
+
|
|
57
52
|
try:
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
subprocess.run(
|
|
62
|
-
["git", "rev-parse", "--git-dir"],
|
|
63
|
-
check=True,
|
|
64
|
-
capture_output=True,
|
|
65
|
-
text=True,
|
|
66
|
-
)
|
|
67
|
-
except subprocess.CalledProcessError:
|
|
68
|
-
is_git_repo = False
|
|
69
|
-
|
|
70
|
-
# If not a git repo, auto-initialize if allowed
|
|
71
|
-
if not is_git_repo:
|
|
72
|
-
if auto_init_git:
|
|
73
|
-
try:
|
|
74
|
-
subprocess.run(["git", "init"], check=True, capture_output=True)
|
|
75
|
-
result["git_initialized"] = True
|
|
76
|
-
except subprocess.CalledProcessError as e:
|
|
77
|
-
result["errors"].append(f"Failed to initialize git repository: {e}")
|
|
78
|
-
result["message"] = "Git initialization failed"
|
|
79
|
-
return result
|
|
80
|
-
else:
|
|
81
|
-
result["errors"].append("Not in a git repository and auto_init_git=False")
|
|
82
|
-
result["message"] = "Not a git repository"
|
|
83
|
-
return result
|
|
84
|
-
|
|
85
|
-
repo_root = Path(
|
|
86
|
-
subprocess.run(
|
|
87
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
88
|
-
check=True,
|
|
89
|
-
capture_output=True,
|
|
90
|
-
text=True,
|
|
91
|
-
).stdout.strip()
|
|
92
|
-
)
|
|
53
|
+
# Use current directory as repo_root (no git dependency)
|
|
54
|
+
repo_root = Path(os.getcwd()).resolve()
|
|
93
55
|
result["repo_root"] = str(repo_root)
|
|
94
56
|
|
|
95
|
-
# Create directory structure
|
|
96
|
-
realign_dir = repo_root
|
|
97
|
-
hooks_dir = realign_dir / "hooks"
|
|
57
|
+
# Create directory structure in ~/.aline/{project_name}/
|
|
58
|
+
realign_dir = get_realign_dir(repo_root)
|
|
98
59
|
sessions_dir = realign_dir / "sessions"
|
|
99
60
|
result["realign_dir"] = str(realign_dir)
|
|
100
61
|
|
|
101
|
-
|
|
62
|
+
# Check if already initialized (unless --force)
|
|
63
|
+
if realign_dir.exists() and not force:
|
|
64
|
+
result["errors"].append("Aline already initialized in this project")
|
|
65
|
+
result["message"] = "Already initialized. Use --force to reinitialize."
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
# If force and exists, remove existing directory
|
|
69
|
+
if force and realign_dir.exists():
|
|
70
|
+
import shutil
|
|
71
|
+
console.print(f"[yellow]Removing existing Aline directory: {realign_dir}[/yellow]")
|
|
72
|
+
shutil.rmtree(realign_dir)
|
|
73
|
+
result["message"] = "Re-initialized existing Aline directory"
|
|
74
|
+
|
|
75
|
+
# Create directories (no hooks needed)
|
|
76
|
+
for directory in [realign_dir, sessions_dir]:
|
|
102
77
|
directory.mkdir(parents=True, exist_ok=True)
|
|
103
78
|
|
|
104
|
-
# Install pre-commit hook
|
|
105
|
-
pre_commit_path = hooks_dir / "pre-commit"
|
|
106
|
-
if force or not pre_commit_path.exists():
|
|
107
|
-
action = "updated" if pre_commit_path.exists() else "created"
|
|
108
|
-
pre_commit_content = get_pre_commit_hook()
|
|
109
|
-
pre_commit_path.write_text(pre_commit_content, encoding="utf-8")
|
|
110
|
-
pre_commit_path.chmod(0o755)
|
|
111
|
-
result["hooks_created"].append(f"pre-commit ({action})")
|
|
112
|
-
|
|
113
|
-
# Install prepare-commit-msg hook
|
|
114
|
-
hook_path = hooks_dir / "prepare-commit-msg"
|
|
115
|
-
if force or not hook_path.exists():
|
|
116
|
-
action = "updated" if hook_path.exists() else "created"
|
|
117
|
-
hook_content = get_prepare_commit_msg_hook()
|
|
118
|
-
hook_path.write_text(hook_content, encoding="utf-8")
|
|
119
|
-
hook_path.chmod(0o755)
|
|
120
|
-
result["hooks_created"].append(f"prepare-commit-msg ({action})")
|
|
121
|
-
|
|
122
79
|
# Create .gitignore for sessions and metadata
|
|
123
80
|
gitignore_path = realign_dir / ".gitignore"
|
|
124
81
|
if not gitignore_path.exists():
|
|
125
82
|
gitignore_content = (
|
|
126
|
-
"# Uncomment to ignore session files\n"
|
|
127
|
-
"# sessions/\n\n"
|
|
128
83
|
"# Ignore metadata files (used internally to prevent duplicate processing)\n"
|
|
129
84
|
".metadata/\n\n"
|
|
130
85
|
"# Ignore original sessions (contains potential secrets before redaction)\n"
|
|
@@ -132,37 +87,24 @@ def init_repository(
|
|
|
132
87
|
)
|
|
133
88
|
gitignore_path.write_text(gitignore_content, encoding="utf-8")
|
|
134
89
|
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
capture_output=True,
|
|
151
|
-
text=True,
|
|
152
|
-
check=False,
|
|
153
|
-
).stdout.strip(),
|
|
154
|
-
}
|
|
155
|
-
with open(backup_file, "w", encoding="utf-8") as f:
|
|
156
|
-
yaml.dump(backup_data, f)
|
|
157
|
-
|
|
158
|
-
# Set new hooks path
|
|
159
|
-
subprocess.run(
|
|
160
|
-
["git", "config", "--local", "core.hooksPath", ".realign/hooks"],
|
|
161
|
-
check=True,
|
|
162
|
-
)
|
|
90
|
+
# Initialize .aline/.git if it doesn't exist
|
|
91
|
+
# NOTE: We only create the git repo here, the initial commit happens after mirroring
|
|
92
|
+
realign_git = realign_dir / ".git"
|
|
93
|
+
if not realign_git.exists():
|
|
94
|
+
try:
|
|
95
|
+
subprocess.run(
|
|
96
|
+
["git", "init"],
|
|
97
|
+
cwd=realign_dir,
|
|
98
|
+
check=True,
|
|
99
|
+
capture_output=True
|
|
100
|
+
)
|
|
101
|
+
result["realign_git_initialized"] = True
|
|
102
|
+
except subprocess.CalledProcessError as e:
|
|
103
|
+
result["errors"].append(f"Failed to initialize .aline/.git: {e}")
|
|
104
|
+
# Continue anyway, this is not critical for basic functionality
|
|
163
105
|
|
|
164
106
|
# Initialize global config if not exists
|
|
165
|
-
global_config_path = Path.home() / ".config" / "
|
|
107
|
+
global_config_path = Path.home() / ".config" / "aline" / "config.yaml"
|
|
166
108
|
if not global_config_path.exists():
|
|
167
109
|
global_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
110
|
global_config_path.write_text(get_default_config_content(), encoding="utf-8")
|
|
@@ -174,23 +116,26 @@ def init_repository(
|
|
|
174
116
|
history_dir.mkdir(parents=True, exist_ok=True)
|
|
175
117
|
result["history_dir"] = str(history_dir)
|
|
176
118
|
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
119
|
+
# Create a .aline-config file in project root to store realign_dir location
|
|
120
|
+
config_marker = repo_root / ".aline-config"
|
|
121
|
+
config_marker.write_text(str(realign_dir), encoding="utf-8")
|
|
122
|
+
|
|
123
|
+
# Update project .gitignore to ignore .aline-config
|
|
124
|
+
project_gitignore = repo_root / ".gitignore"
|
|
125
|
+
if project_gitignore.exists():
|
|
126
|
+
gitignore_content = project_gitignore.read_text(encoding="utf-8")
|
|
127
|
+
if ".aline-config" not in gitignore_content:
|
|
128
|
+
# Add .aline-config to .gitignore
|
|
129
|
+
if not gitignore_content.endswith('\n'):
|
|
130
|
+
gitignore_content += '\n'
|
|
131
|
+
gitignore_content += '\n# Aline config file\n.aline-config\n'
|
|
132
|
+
project_gitignore.write_text(gitignore_content, encoding="utf-8")
|
|
133
|
+
else:
|
|
134
|
+
# Create new .gitignore with .aline-config
|
|
135
|
+
project_gitignore.write_text('# Aline config file\n.aline-config\n', encoding="utf-8")
|
|
191
136
|
|
|
192
137
|
result["success"] = True
|
|
193
|
-
result["message"] = "
|
|
138
|
+
result["message"] = "Aline initialized successfully"
|
|
194
139
|
|
|
195
140
|
except Exception as e:
|
|
196
141
|
result["errors"].append(f"Initialization failed: {e}")
|
|
@@ -203,24 +148,22 @@ def init_repository(
|
|
|
203
148
|
|
|
204
149
|
|
|
205
150
|
def init_command(
|
|
206
|
-
|
|
207
|
-
skip_commit: bool = typer.Option(False, "--skip-commit", help="Skip auto-commit of hooks"),
|
|
208
|
-
force: bool = typer.Option(False, "--force", "-f", help="Force re-initialization and update hooks even if they exist"),
|
|
151
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force re-initialization even if .aline exists"),
|
|
209
152
|
):
|
|
210
|
-
"""Initialize
|
|
153
|
+
"""Initialize Aline tracking system in the current directory.
|
|
211
154
|
|
|
212
|
-
|
|
155
|
+
Creates .aline directory structure and initializes the shadow git repository.
|
|
156
|
+
Works with or without an existing git repository in the project.
|
|
213
157
|
"""
|
|
158
|
+
# Standard initialization
|
|
214
159
|
# Call the core function
|
|
215
160
|
result = init_repository(
|
|
216
161
|
repo_path=".",
|
|
217
|
-
auto_init_git=True,
|
|
218
|
-
skip_commit=skip_commit,
|
|
219
162
|
force=force,
|
|
220
163
|
)
|
|
221
164
|
|
|
222
165
|
# Print detailed results
|
|
223
|
-
console.print("\n[bold blue]═══
|
|
166
|
+
console.print("\n[bold blue]═══ Aline Initialization ═══[/bold blue]\n")
|
|
224
167
|
|
|
225
168
|
if result["success"]:
|
|
226
169
|
console.print("[bold green]✓ Status: SUCCESS[/bold green]\n")
|
|
@@ -228,17 +171,12 @@ def init_command(
|
|
|
228
171
|
console.print("[bold red]✗ Status: FAILED[/bold red]\n")
|
|
229
172
|
|
|
230
173
|
# Print all parameters and results
|
|
231
|
-
console.print("[bold]
|
|
232
|
-
console.print(f"
|
|
233
|
-
console.print(f"
|
|
234
|
-
console.print(f"
|
|
235
|
-
console.print(f" Config Path: [cyan]{result.get('config_path', 'N/A')}[/cyan]")
|
|
174
|
+
console.print("[bold]Configuration:[/bold]")
|
|
175
|
+
console.print(f" Project Root: [cyan]{result.get('repo_root', 'N/A')}[/cyan]")
|
|
176
|
+
console.print(f" Aline Directory: [cyan]{result.get('realign_dir', 'N/A')}[/cyan]")
|
|
177
|
+
console.print(f" Global Config: [cyan]{result.get('config_path', 'N/A')}[/cyan]")
|
|
236
178
|
console.print(f" History Directory: [cyan]{result.get('history_dir', 'N/A')}[/cyan]")
|
|
237
|
-
console.print(f" Git Initialized: [cyan]{result.get('
|
|
238
|
-
console.print(f" Force Reinit: [cyan]{force}[/cyan]")
|
|
239
|
-
console.print(f" Skip Commit: [cyan]{skip_commit}[/cyan]")
|
|
240
|
-
console.print(f" Hooks: [cyan]{', '.join(result.get('hooks_created', [])) or 'None'}[/cyan]")
|
|
241
|
-
console.print(f" Auto-committed: [cyan]{result.get('committed', False)}[/cyan]")
|
|
179
|
+
console.print(f" Shadow Git Initialized: [cyan]{result.get('realign_git_initialized', False)}[/cyan]")
|
|
242
180
|
|
|
243
181
|
if result.get("errors"):
|
|
244
182
|
console.print("\n[bold red]Errors:[/bold red]")
|
|
@@ -248,95 +186,44 @@ def init_command(
|
|
|
248
186
|
console.print(f"\n[bold]Result:[/bold] {result['message']}\n")
|
|
249
187
|
|
|
250
188
|
if result["success"]:
|
|
189
|
+
# Mirror project files after successful initialization
|
|
190
|
+
console.print("[bold]Mirroring project files...[/bold]")
|
|
191
|
+
from .mirror import mirror_project
|
|
192
|
+
mirror_success = mirror_project(project_path=Path(result["repo_root"]), verbose=False)
|
|
193
|
+
|
|
194
|
+
if mirror_success:
|
|
195
|
+
console.print("[green]✓ Project files mirrored successfully[/green]\n")
|
|
196
|
+
|
|
197
|
+
# Create initial commit with mirrored files
|
|
198
|
+
realign_dir = Path(result["realign_dir"])
|
|
199
|
+
try:
|
|
200
|
+
subprocess.run(
|
|
201
|
+
["git", "add", "-A"],
|
|
202
|
+
cwd=realign_dir,
|
|
203
|
+
check=True,
|
|
204
|
+
capture_output=True
|
|
205
|
+
)
|
|
206
|
+
subprocess.run(
|
|
207
|
+
["git", "commit", "-m", "Initial commit: Mirror project files"],
|
|
208
|
+
cwd=realign_dir,
|
|
209
|
+
check=True,
|
|
210
|
+
capture_output=True
|
|
211
|
+
)
|
|
212
|
+
console.print("[green]✓ Created initial commit in shadow git[/green]\n")
|
|
213
|
+
except subprocess.CalledProcessError as e:
|
|
214
|
+
console.print(f"[yellow]⚠ Warning: Failed to create initial commit: {e}[/yellow]\n")
|
|
215
|
+
else:
|
|
216
|
+
console.print("[yellow]⚠ Warning: Failed to mirror project files[/yellow]")
|
|
217
|
+
console.print("[dim]You can manually run 'aline mirror' later[/dim]\n")
|
|
218
|
+
|
|
251
219
|
console.print("[bold]Next steps:[/bold]")
|
|
252
|
-
console.print(" 1.
|
|
253
|
-
console.print(
|
|
254
|
-
console.print("
|
|
255
|
-
console.print("
|
|
256
|
-
console.print(" 4. View sessions with: [cyan]realign show <commit>[/cyan]", style="dim")
|
|
220
|
+
console.print(" 1. Start Claude Code or Codex - the MCP server will auto-start", style="dim")
|
|
221
|
+
console.print(" 2. Sessions are automatically tracked to .aline/.git", style="dim")
|
|
222
|
+
console.print(" 3. Review commits with: [cyan]aline review[/cyan]", style="dim")
|
|
223
|
+
console.print(" 4. Hide sensitive commits with: [cyan]aline hide <indices>[/cyan]", style="dim")
|
|
257
224
|
else:
|
|
258
225
|
raise typer.Exit(1)
|
|
259
226
|
|
|
260
227
|
|
|
261
|
-
def get_pre_commit_hook() -> str:
|
|
262
|
-
"""Get the pre-commit hook script content."""
|
|
263
|
-
return '''#!/bin/bash
|
|
264
|
-
# ReAlign pre-commit hook
|
|
265
|
-
# Finds and stages agent session files before commit
|
|
266
|
-
|
|
267
|
-
# 1. Try direct Python execution first (development mode - highest priority for dev machines)
|
|
268
|
-
# Check if realign module is importable (e.g., installed in editable mode or in PYTHONPATH)
|
|
269
|
-
if python -c "import realign.hooks" 2>/dev/null; then
|
|
270
|
-
echo "Aline pre-commit hook (dev-mode)" >&2
|
|
271
|
-
exec python -m realign.hooks --pre-commit "$@"
|
|
272
|
-
fi
|
|
273
|
-
|
|
274
|
-
# 2. Try to find aline-hook-pre-commit in PATH (pipx/pip installations)
|
|
275
|
-
if command -v aline-hook-pre-commit >/dev/null 2>&1; then
|
|
276
|
-
VERSION=$(aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
277
|
-
echo "Aline pre-commit hook (cli-$VERSION)" >&2
|
|
278
|
-
exec aline-hook-pre-commit "$@"
|
|
279
|
-
fi
|
|
280
|
-
|
|
281
|
-
# 3. Try using uvx (for MCP installations where command is in uvx cache)
|
|
282
|
-
if command -v uvx >/dev/null 2>&1; then
|
|
283
|
-
VERSION=$(uvx --from aline-ai aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
284
|
-
echo "Aline pre-commit hook (mcp-$VERSION)" >&2
|
|
285
|
-
exec uvx --from aline-ai aline-hook-pre-commit "$@"
|
|
286
|
-
fi
|
|
287
|
-
|
|
288
|
-
# If all else fails, print an error
|
|
289
|
-
echo "Error: Cannot find aline. Please ensure it's installed:" >&2
|
|
290
|
-
echo " - For dev: Ensure 'python -m realign.hooks' works (editable install or PYTHONPATH)" >&2
|
|
291
|
-
echo " - For CLI: pipx install aline-ai" >&2
|
|
292
|
-
echo " - For MCP: Ensure uvx is available" >&2
|
|
293
|
-
exit 1
|
|
294
|
-
'''
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def get_prepare_commit_msg_hook() -> str:
|
|
298
|
-
"""Get the prepare-commit-msg hook script content."""
|
|
299
|
-
return '''#!/bin/bash
|
|
300
|
-
# ReAlign prepare-commit-msg hook
|
|
301
|
-
# Adds agent session metadata to commit messages
|
|
302
|
-
|
|
303
|
-
COMMIT_MSG_FILE="$1"
|
|
304
|
-
COMMIT_SOURCE="$2"
|
|
305
|
-
|
|
306
|
-
# Skip for merge, squash, and commit --amend (but allow message and template)
|
|
307
|
-
if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ] || [ "$COMMIT_SOURCE" = "commit" ]; then
|
|
308
|
-
exit 0
|
|
309
|
-
fi
|
|
310
|
-
|
|
311
|
-
# 1. Try direct Python execution first (development mode - highest priority for dev machines)
|
|
312
|
-
# Check if realign module is importable (e.g., installed in editable mode or in PYTHONPATH)
|
|
313
|
-
if python -c "import realign.hooks" 2>/dev/null; then
|
|
314
|
-
echo "Aline prepare-commit-msg hook (dev-mode)" >&2
|
|
315
|
-
exec python -m realign.hooks --prepare-commit-msg "$@"
|
|
316
|
-
fi
|
|
317
|
-
|
|
318
|
-
# 2. Try to find aline-hook-prepare-commit-msg in PATH (pipx/pip installations)
|
|
319
|
-
if command -v aline-hook-prepare-commit-msg >/dev/null 2>&1; then
|
|
320
|
-
VERSION=$(aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
321
|
-
echo "Aline prepare-commit-msg hook (cli-$VERSION)" >&2
|
|
322
|
-
exec aline-hook-prepare-commit-msg "$@"
|
|
323
|
-
fi
|
|
324
|
-
|
|
325
|
-
# 3. Try using uvx (for MCP installations where command is in uvx cache)
|
|
326
|
-
if command -v uvx >/dev/null 2>&1; then
|
|
327
|
-
VERSION=$(uvx --from aline-ai aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
328
|
-
echo "Aline prepare-commit-msg hook (mcp-$VERSION)" >&2
|
|
329
|
-
exec uvx --from aline-ai aline-hook-prepare-commit-msg "$@"
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
# If all else fails, print an error
|
|
333
|
-
echo "Error: Cannot find aline. Please ensure it's installed:" >&2
|
|
334
|
-
echo " - For dev: Ensure 'python -m realign.hooks' works (editable install or PYTHONPATH)" >&2
|
|
335
|
-
echo " - For CLI: pipx install aline-ai" >&2
|
|
336
|
-
echo " - For MCP: Ensure uvx is available" >&2
|
|
337
|
-
exit 1
|
|
338
|
-
'''
|
|
339
|
-
|
|
340
|
-
|
|
341
228
|
if __name__ == "__main__":
|
|
342
229
|
typer.run(init_command)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""ReAlign mirror command - Mirror project files to shadow git repository."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
|
+
|
|
9
|
+
from ..tracker.git_tracker import ReAlignGitTracker
|
|
10
|
+
from ..mirror_utils import collect_project_files
|
|
11
|
+
from realign import get_realign_dir
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def mirror_project(
|
|
17
|
+
project_path: Optional[Path] = None,
|
|
18
|
+
verbose: bool = False
|
|
19
|
+
) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Mirror all project files to the shadow git repository.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
project_path: Path to project directory (defaults to current directory)
|
|
25
|
+
verbose: Print detailed progress information
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if successful, False otherwise
|
|
29
|
+
"""
|
|
30
|
+
# Use current directory if not specified
|
|
31
|
+
if project_path is None:
|
|
32
|
+
project_path = Path.cwd()
|
|
33
|
+
else:
|
|
34
|
+
project_path = Path(project_path).resolve()
|
|
35
|
+
|
|
36
|
+
# Check if project exists
|
|
37
|
+
if not project_path.exists():
|
|
38
|
+
console.print(f"[red]Error: Project directory does not exist: {project_path}[/red]")
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
# Check if ReAlign is initialized
|
|
42
|
+
realign_dir = get_realign_dir(project_path)
|
|
43
|
+
if not realign_dir.exists():
|
|
44
|
+
console.print(f"[red]Error: ReAlign not initialized in {project_path}[/red]")
|
|
45
|
+
console.print(f"[dim]Run 'realign init' first[/dim]")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# Initialize git tracker
|
|
50
|
+
tracker = ReAlignGitTracker(project_path)
|
|
51
|
+
if not tracker.is_initialized():
|
|
52
|
+
console.print("[yellow]Shadow git not initialized, initializing now...[/yellow]")
|
|
53
|
+
if not tracker.init_repo():
|
|
54
|
+
console.print("[red]Failed to initialize shadow git[/red]")
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
# Collect all project files
|
|
58
|
+
if verbose:
|
|
59
|
+
console.print(f"[dim]Scanning project files in {project_path}...[/dim]")
|
|
60
|
+
|
|
61
|
+
all_files = collect_project_files(project_path)
|
|
62
|
+
|
|
63
|
+
if not all_files:
|
|
64
|
+
console.print("[yellow]No files to mirror[/yellow]")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
# Mirror files with progress
|
|
68
|
+
with Progress(
|
|
69
|
+
SpinnerColumn(),
|
|
70
|
+
TextColumn("[progress.description]{task.description}"),
|
|
71
|
+
console=console
|
|
72
|
+
) as progress:
|
|
73
|
+
task = progress.add_task(
|
|
74
|
+
f"Mirroring {len(all_files)} file(s)...",
|
|
75
|
+
total=None
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
mirrored_files = tracker.mirror_files(all_files)
|
|
79
|
+
progress.update(task, completed=True)
|
|
80
|
+
|
|
81
|
+
# Report results
|
|
82
|
+
if mirrored_files:
|
|
83
|
+
console.print(f"[green]✓ Mirrored {len(mirrored_files)} file(s) to {realign_dir / 'mirror'}[/green]")
|
|
84
|
+
if verbose:
|
|
85
|
+
console.print("\n[bold]Mirrored files:[/bold]")
|
|
86
|
+
for file_path in mirrored_files[:10]: # Show first 10
|
|
87
|
+
rel_path = file_path.relative_to(realign_dir / "mirror")
|
|
88
|
+
console.print(f" • {rel_path}")
|
|
89
|
+
if len(mirrored_files) > 10:
|
|
90
|
+
console.print(f" ... and {len(mirrored_files) - 10} more")
|
|
91
|
+
else:
|
|
92
|
+
console.print("[dim]No files needed to be copied (all up to date)[/dim]")
|
|
93
|
+
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
console.print(f"[red]Error mirroring project: {e}[/red]")
|
|
98
|
+
if verbose:
|
|
99
|
+
import traceback
|
|
100
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def mirror_command(
|
|
105
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed progress"),
|
|
106
|
+
path: Optional[str] = typer.Argument(None, help="Project path (defaults to current directory)"),
|
|
107
|
+
):
|
|
108
|
+
"""Mirror all project files to the shadow git repository.
|
|
109
|
+
|
|
110
|
+
This command copies all project files (respecting .gitignore) to the
|
|
111
|
+
~/.aline/{project_name}/mirror/ directory in the shadow git repository.
|
|
112
|
+
|
|
113
|
+
The mirror is automatically updated when watcher detects session changes,
|
|
114
|
+
but this command can be used to manually sync files at any time.
|
|
115
|
+
"""
|
|
116
|
+
project_path = Path(path) if path else None
|
|
117
|
+
|
|
118
|
+
if verbose:
|
|
119
|
+
if project_path:
|
|
120
|
+
console.print(f"[bold blue]Mirroring project: {project_path}[/bold blue]\n")
|
|
121
|
+
else:
|
|
122
|
+
console.print(f"[bold blue]Mirroring project: {Path.cwd()}[/bold blue]\n")
|
|
123
|
+
|
|
124
|
+
success = mirror_project(project_path=project_path, verbose=verbose)
|
|
125
|
+
|
|
126
|
+
if not success:
|
|
127
|
+
raise typer.Exit(1)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
typer.run(mirror_command)
|
realign/commands/pull.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Pull command - Pull session updates from remote repository."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..tracker.git_tracker import ReAlignGitTracker
|
|
9
|
+
from ..logging_config import setup_logger
|
|
10
|
+
|
|
11
|
+
logger = setup_logger('realign.commands.pull', 'pull.log')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pull_command(repo_root: Optional[Path] = None) -> int:
|
|
15
|
+
"""
|
|
16
|
+
Pull session updates from remote repository.
|
|
17
|
+
|
|
18
|
+
This command fetches and merges session commits from the
|
|
19
|
+
remote repository, bringing in updates from your teammates.
|
|
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
|
+
# Check for unpushed commits
|
|
50
|
+
unpushed = tracker.get_unpushed_commits()
|
|
51
|
+
|
|
52
|
+
if unpushed:
|
|
53
|
+
print(f"⚠️ Warning: You have {len(unpushed)} unpushed commit(s)")
|
|
54
|
+
print("These will be merged with remote changes")
|
|
55
|
+
print()
|
|
56
|
+
|
|
57
|
+
confirm = input("Continue with pull? [Y/n]: ").strip().lower()
|
|
58
|
+
if confirm == 'n':
|
|
59
|
+
print("Cancelled")
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
# Perform pull
|
|
63
|
+
print("Pulling from remote...")
|
|
64
|
+
|
|
65
|
+
remote_url = tracker.get_remote_url()
|
|
66
|
+
print(f"Remote: {remote_url}")
|
|
67
|
+
print()
|
|
68
|
+
|
|
69
|
+
success = tracker.safe_pull()
|
|
70
|
+
|
|
71
|
+
if success:
|
|
72
|
+
print("✓ Successfully pulled updates from remote")
|
|
73
|
+
|
|
74
|
+
# Try to get some stats about what was pulled
|
|
75
|
+
try:
|
|
76
|
+
# Get log of recent commits
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
["git", "log", "--oneline", "-5"],
|
|
79
|
+
cwd=tracker.realign_dir,
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
check=False
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
86
|
+
print("\nRecent commits:")
|
|
87
|
+
for line in result.stdout.strip().split('\n'):
|
|
88
|
+
print(f" {line}")
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.debug(f"Failed to get commit stats: {e}")
|
|
92
|
+
|
|
93
|
+
return 0
|
|
94
|
+
else:
|
|
95
|
+
print("❌ Pull failed")
|
|
96
|
+
print("\nPossible issues:")
|
|
97
|
+
print(" - Conflicts requiring manual resolution")
|
|
98
|
+
print(" - Network connection problems")
|
|
99
|
+
print(" - Repository access issues")
|
|
100
|
+
print("\nCheck logs: .realign/logs/pull.log")
|
|
101
|
+
return 1
|