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.
Files changed (45) hide show
  1. {aline_ai-0.2.5.dist-info → aline_ai-0.3.0.dist-info}/METADATA +3 -1
  2. aline_ai-0.3.0.dist-info/RECORD +41 -0
  3. aline_ai-0.3.0.dist-info/entry_points.txt +3 -0
  4. realign/__init__.py +32 -1
  5. realign/cli.py +203 -19
  6. realign/commands/__init__.py +2 -2
  7. realign/commands/clean.py +149 -0
  8. realign/commands/config.py +1 -1
  9. realign/commands/export_shares.py +1785 -0
  10. realign/commands/hide.py +112 -24
  11. realign/commands/import_history.py +873 -0
  12. realign/commands/init.py +104 -217
  13. realign/commands/mirror.py +131 -0
  14. realign/commands/pull.py +101 -0
  15. realign/commands/push.py +155 -245
  16. realign/commands/review.py +216 -54
  17. realign/commands/session_utils.py +139 -4
  18. realign/commands/share.py +965 -0
  19. realign/commands/status.py +559 -0
  20. realign/commands/sync.py +91 -0
  21. realign/commands/undo.py +423 -0
  22. realign/commands/watcher.py +805 -0
  23. realign/config.py +21 -10
  24. realign/file_lock.py +3 -1
  25. realign/hash_registry.py +310 -0
  26. realign/hooks.py +368 -384
  27. realign/logging_config.py +2 -2
  28. realign/mcp_server.py +263 -549
  29. realign/mcp_watcher.py +999 -142
  30. realign/mirror_utils.py +322 -0
  31. realign/prompts/__init__.py +21 -0
  32. realign/prompts/presets.py +238 -0
  33. realign/redactor.py +168 -16
  34. realign/tracker/__init__.py +9 -0
  35. realign/tracker/git_tracker.py +1123 -0
  36. realign/watcher_daemon.py +115 -0
  37. aline_ai-0.2.5.dist-info/RECORD +0 -28
  38. aline_ai-0.2.5.dist-info/entry_points.txt +0 -5
  39. realign/commands/auto_commit.py +0 -231
  40. realign/commands/commit.py +0 -379
  41. realign/commands/search.py +0 -449
  42. realign/commands/show.py +0 -416
  43. {aline_ai-0.2.5.dist-info → aline_ai-0.3.0.dist-info}/WHEEL +0 -0
  44. {aline_ai-0.2.5.dist-info → aline_ai-0.3.0.dist-info}/licenses/LICENSE +0 -0
  45. {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
- """ReAlign init command - Initialize ReAlign in a git repository."""
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 Optional, Dict, Any
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
- auto_init_git: Automatically initialize git repo if not exists
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
- "git_initialized": False,
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
- # Check if we're in a git repository
59
- is_git_repo = True
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 / ".realign"
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
- for directory in [realign_dir, hooks_dir, sessions_dir]:
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
- # Backup and set core.hooksPath
136
- backup_file = realign_dir / "backup_hook_config.yaml"
137
- current_hooks_path = subprocess.run(
138
- ["git", "config", "--local", "core.hooksPath"],
139
- capture_output=True,
140
- text=True,
141
- check=False,
142
- ).stdout.strip()
143
-
144
- if current_hooks_path and current_hooks_path != ".realign/hooks":
145
- # Backup old configuration
146
- backup_data = {
147
- "original_hooks_path": current_hooks_path,
148
- "backup_timestamp": subprocess.run(
149
- ["date", "+%Y-%m-%d %H:%M:%S"],
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" / "realign" / "config.yaml"
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
- # Add hooks to git (optional commit)
178
- if not skip_commit:
179
- try:
180
- subprocess.run(["git", "add", ".realign/hooks/prepare-commit-msg"], check=True)
181
- # Try to commit (may fail if no changes, which is fine)
182
- commit_result = subprocess.run(
183
- ["git", "commit", "-m", "chore(realign): add prepare-commit-msg hook", "--no-verify"],
184
- capture_output=True,
185
- text=True,
186
- check=False,
187
- )
188
- result["committed"] = commit_result.returncode == 0
189
- except subprocess.CalledProcessError:
190
- result["committed"] = False
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"] = "ReAlign initialized successfully"
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
- yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts (deprecated, now default)"),
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 ReAlign in the current git repository.
153
+ """Initialize Aline tracking system in the current directory.
211
154
 
212
- Use --force to re-initialize and update hooks to the latest version.
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]═══ ReAlign Initialization ═══[/bold blue]\n")
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]Parameters:[/bold]")
232
- console.print(f" Repository Path: [cyan]{result.get('repo_path', 'N/A')}[/cyan]")
233
- console.print(f" Repository Root: [cyan]{result.get('repo_root', 'N/A')}[/cyan]")
234
- console.print(f" ReAlign Directory: [cyan]{result.get('realign_dir', 'N/A')}[/cyan]")
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('git_initialized', False)}[/cyan]")
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. Ensure your agent saves chat histories to:", style="dim")
253
- console.print(f" {result['history_dir']}", style="cyan")
254
- console.print(" 2. Make commits as usual - ReAlign will automatically track sessions", style="dim")
255
- console.print(" 3. Search sessions with: [cyan]realign search <keyword>[/cyan]", style="dim")
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)
@@ -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