Skip to content

Commit 9ddc7b8

Browse files
author
Aegis Stack
committed
Aegis Update
1 parent 78f8842 commit 9ddc7b8

11 files changed

Lines changed: 1255 additions & 0 deletions

aegis/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .commands.init import init_command
1717
from .commands.remove import remove_command
1818
from .commands.services import services_command
19+
from .commands.update import update_command
1920
from .commands.version import version_command
2021

2122
# Create the main Typer application
@@ -42,6 +43,7 @@
4243
app.command(name="add")(add_command)
4344
app.command(name="add-service")(add_service_command)
4445
app.command(name="remove")(remove_command)
46+
app.command(name="update")(update_command)
4547

4648

4749
# This is what runs when you do: aegis

aegis/commands/add.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from ..core.dependency_resolver import DependencyResolver
1919
from ..core.manual_updater import ManualUpdater
20+
from ..core.version_compatibility import validate_version_compatibility
2021

2122

2223
def add_command(
@@ -43,6 +44,12 @@ def add_command(
4344
help="Path to the Aegis Stack project (default: current directory)",
4445
),
4546
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
47+
force: bool = typer.Option(
48+
False,
49+
"--force",
50+
"-f",
51+
help="Force through version mismatch warnings",
52+
),
4653
) -> None:
4754
"""
4855
Add components to an existing Aegis Stack project.
@@ -85,6 +92,9 @@ def add_command(
8592

8693
typer.echo(f"📁 Project: {target_path}")
8794

95+
# Check version compatibility between CLI and project template
96+
validate_version_compatibility(target_path, command_name="add", force=force)
97+
8898
# Validate components argument or interactive mode
8999
if not interactive and not components:
90100
typer.echo(

aegis/commands/remove.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from ..core.dependency_resolver import DependencyResolver
1717
from ..core.manual_updater import ManualUpdater
18+
from ..core.version_compatibility import validate_version_compatibility
1819

1920

2021
def remove_command(
@@ -35,6 +36,12 @@ def remove_command(
3536
help="Path to the Aegis Stack project (default: current directory)",
3637
),
3738
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
39+
force: bool = typer.Option(
40+
False,
41+
"--force",
42+
"-f",
43+
help="Force through version mismatch warnings",
44+
),
3845
) -> None:
3946
"""
4047
Remove components from an existing Aegis Stack project.
@@ -69,6 +76,9 @@ def remove_command(
6976

7077
typer.echo(f"📁 Project: {target_path}")
7178

79+
# Check version compatibility between CLI and project template
80+
validate_version_compatibility(target_path, command_name="remove", force=force)
81+
7282
# Validate components argument or interactive mode
7383
if not interactive and not components:
7484
typer.echo(

aegis/commands/update.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"""
2+
Update command implementation.
3+
4+
Updates an existing Aegis Stack project to a newer template version using
5+
Copier's git-aware update mechanism.
6+
"""
7+
8+
from pathlib import Path
9+
10+
import typer
11+
12+
from ..core.copier_manager import is_copier_project, load_copier_answers
13+
from ..core.copier_updater import (
14+
get_changelog,
15+
get_current_template_commit,
16+
get_latest_version,
17+
get_template_root,
18+
resolve_version_to_ref,
19+
validate_clean_git_tree,
20+
)
21+
from ..core.post_gen_tasks import run_post_generation_tasks
22+
from ..core.version_compatibility import get_cli_version, get_project_template_version
23+
24+
25+
def update_command(
26+
to_version: str | None = typer.Option(
27+
None,
28+
"--to-version",
29+
help="Update to specific version (default: latest)",
30+
),
31+
dry_run: bool = typer.Option(
32+
False,
33+
"--dry-run",
34+
help="Preview changes without applying",
35+
),
36+
project_path: str = typer.Option(
37+
".",
38+
"--project-path",
39+
"-p",
40+
help="Path to the Aegis Stack project (default: current directory)",
41+
),
42+
yes: bool = typer.Option(
43+
False,
44+
"--yes",
45+
"-y",
46+
help="Skip confirmation prompt",
47+
),
48+
) -> None:
49+
"""
50+
Update project to a newer template version.
51+
52+
This command uses Copier's git-aware update mechanism to merge template
53+
changes into your project while preserving your customizations.
54+
55+
Examples:
56+
57+
- aegis update
58+
59+
- aegis update --to-version 0.2.0
60+
61+
- aegis update --dry-run
62+
63+
- aegis update --project-path ../my-project
64+
65+
Note: This command requires a clean git working tree.
66+
"""
67+
68+
typer.echo("🛡️ Aegis Stack - Update Template")
69+
typer.echo("=" * 50)
70+
71+
# Resolve project path
72+
target_path = Path(project_path).resolve()
73+
74+
# Validate it's a Copier project
75+
if not is_copier_project(target_path):
76+
typer.echo(
77+
f"❌ Project at {target_path} was not generated with Copier.", err=True
78+
)
79+
typer.echo(
80+
" The 'aegis update' command only works with Copier-generated projects.",
81+
err=True,
82+
)
83+
typer.echo(
84+
" Projects generated before v0.2.0 need to be regenerated.",
85+
err=True,
86+
)
87+
raise typer.Exit(1)
88+
89+
typer.echo(f"📁 Project: {target_path}")
90+
91+
# Check git status
92+
is_clean, git_message = validate_clean_git_tree(target_path)
93+
if not is_clean:
94+
typer.echo(f"❌ {git_message}", err=True)
95+
typer.echo(
96+
" Commit or stash your changes before running 'aegis update'.",
97+
err=True,
98+
)
99+
typer.echo(
100+
" Copier requires a clean git tree to safely merge changes.", err=True
101+
)
102+
raise typer.Exit(1)
103+
104+
typer.echo("✅ Git tree is clean")
105+
106+
# Get current template version
107+
current_commit = get_current_template_commit(target_path)
108+
if not current_commit:
109+
typer.secho(
110+
"⚠️ Warning: Cannot determine current template version", fg="yellow"
111+
)
112+
typer.echo(" Project may have been generated from an untagged commit")
113+
114+
current_version = get_project_template_version(target_path)
115+
cli_version = get_cli_version()
116+
117+
# Get template root
118+
template_root = get_template_root()
119+
120+
# Resolve target version
121+
if to_version:
122+
target_ref = resolve_version_to_ref(to_version, template_root)
123+
target_version_display = to_version
124+
else:
125+
# Default to latest
126+
latest = get_latest_version(template_root)
127+
if latest:
128+
target_ref = f"v{latest}"
129+
target_version_display = f"{latest} (latest)"
130+
else:
131+
target_ref = "HEAD"
132+
target_version_display = "HEAD (latest commit)"
133+
134+
# Display version information
135+
typer.echo("")
136+
typer.echo("📦 Version Information:")
137+
typer.echo(f" Current CLI: {cli_version}")
138+
if current_version:
139+
typer.echo(f" Current Template: {current_version}")
140+
elif current_commit:
141+
typer.echo(f" Current Template: {current_commit[:8]}... (commit)")
142+
else:
143+
typer.echo(" Current Template: unknown")
144+
typer.echo(f" Target Template: {target_version_display}")
145+
146+
# Check if already up to date (version-based)
147+
if current_version and to_version and current_version == to_version:
148+
typer.echo("")
149+
typer.secho("✅ Project is already at the requested version", fg="green")
150+
return
151+
152+
# Check if already at target commit (for HEAD/branch updates)
153+
if current_commit and target_ref:
154+
from ..core.copier_updater import resolve_ref_to_commit
155+
156+
target_commit = resolve_ref_to_commit(target_ref, template_root)
157+
158+
if target_commit and current_commit == target_commit:
159+
typer.echo("")
160+
typer.secho("✅ Project is already at the target commit", fg="green")
161+
typer.echo(f" Current: {current_commit[:8]}...")
162+
typer.echo(f" Target: {target_commit[:8]}...")
163+
return
164+
165+
# Get and display changelog
166+
if current_commit:
167+
typer.echo("")
168+
typer.echo("📋 Changelog:")
169+
typer.echo("-" * 50)
170+
changelog = get_changelog(current_commit, target_ref, template_root)
171+
typer.echo(changelog)
172+
typer.echo("-" * 50)
173+
174+
# Dry run mode
175+
if dry_run:
176+
typer.echo("")
177+
typer.secho("🔍 DRY RUN MODE - No changes will be applied", fg="cyan")
178+
typer.echo("")
179+
typer.echo("To apply this update, run:")
180+
if to_version:
181+
typer.echo(f" aegis update --to-version {to_version}")
182+
else:
183+
typer.echo(" aegis update")
184+
return
185+
186+
# Confirmation
187+
if not yes:
188+
typer.echo("")
189+
if not typer.confirm("🚀 Apply this update?"):
190+
typer.echo("❌ Update cancelled")
191+
raise typer.Exit(0)
192+
193+
# Perform update using Copier
194+
typer.echo("")
195+
typer.echo("🔄 Updating project...")
196+
197+
try:
198+
# Import here to avoid circular dependency
199+
from copier import run_update
200+
201+
# Run Copier update with git-aware merge
202+
run_update(
203+
dst_path=str(target_path),
204+
src_path=str(template_root),
205+
defaults=True, # Use existing answers as defaults
206+
overwrite=True, # Allow overwriting files
207+
conflict="rej", # Create .rej files for conflicts
208+
unsafe=False, # Disable _tasks (we run them ourselves)
209+
vcs_ref=target_ref, # Use specified version
210+
)
211+
212+
# Load answers to determine what services are enabled
213+
answers = load_copier_answers(target_path)
214+
include_auth = answers.get("include_auth", False)
215+
216+
# Run post-generation tasks
217+
typer.echo("🔨 Running post-generation tasks...")
218+
run_post_generation_tasks(target_path, include_auth=include_auth)
219+
220+
# Success!
221+
typer.echo("")
222+
typer.secho("✅ Update completed successfully!", fg="green")
223+
typer.echo("")
224+
typer.echo("📝 Next Steps:")
225+
typer.echo(" 1. Review changes: git diff")
226+
typer.echo(" 2. Check for conflicts (*.rej files)")
227+
typer.echo(" 3. Run tests: make check")
228+
typer.echo(" 4. Commit changes: git add . && git commit")
229+
230+
# Check for conflict files
231+
rej_files = list(target_path.rglob("*.rej"))
232+
if rej_files:
233+
typer.echo("")
234+
typer.secho(
235+
f"⚠️ Found {len(rej_files)} conflict file(s) - manual resolution required",
236+
fg="yellow",
237+
)
238+
typer.echo(" Conflicts:")
239+
for rej_file in rej_files[:5]: # Show first 5
240+
typer.echo(f" - {rej_file.relative_to(target_path)}")
241+
if len(rej_files) > 5:
242+
typer.echo(f" ... and {len(rej_files) - 5} more")
243+
244+
except Exception as e:
245+
typer.echo("")
246+
typer.secho(f"❌ Update failed: {e}", fg="red", err=True)
247+
typer.echo("")
248+
typer.echo("💡 Troubleshooting:")
249+
typer.echo(" - Ensure you have a clean git tree")
250+
typer.echo(" - Check that the version/commit exists")
251+
typer.echo(" - Review Copier documentation for update issues")
252+
raise typer.Exit(1)

0 commit comments

Comments
 (0)