|
28 | 28 | else: |
29 | 29 | import tomli as tomllib |
30 | 30 | from jinja2 import Environment |
| 31 | +from packaging import version as pkg_version |
31 | 32 | from rich.console import Console |
32 | 33 |
|
| 34 | +from src.cli.utils.version import get_current_version |
| 35 | + |
33 | 36 |
|
34 | 37 | @dataclass |
35 | 38 | class RemoteTemplateSpec: |
@@ -127,15 +130,113 @@ def parse_agent_spec(agent_spec: str) -> RemoteTemplateSpec | None: |
127 | 130 | return None |
128 | 131 |
|
129 | 132 |
|
| 133 | +def check_and_execute_with_version_lock( |
| 134 | + template_dir: pathlib.Path, |
| 135 | + original_agent_spec: str | None = None, |
| 136 | + locked: bool = False, |
| 137 | +) -> bool: |
| 138 | + """Check if remote template has agent-starter-pack version lock and execute if found. |
| 139 | +
|
| 140 | + Args: |
| 141 | + template_dir: Path to the fetched template directory |
| 142 | + original_agent_spec: The original agent spec (remote URL) to replace with local path |
| 143 | + locked: Whether this is already a locked execution (prevents recursion) |
| 144 | +
|
| 145 | + Returns: |
| 146 | + True if version lock was found and executed, False otherwise |
| 147 | + """ |
| 148 | + # Skip version locking if we're already in a locked execution (prevents recursion) |
| 149 | + if locked: |
| 150 | + return False |
| 151 | + uv_lock_path = template_dir / "uv.lock" |
| 152 | + version = parse_agent_starter_pack_version_from_lock(uv_lock_path) |
| 153 | + |
| 154 | + if version: |
| 155 | + console = Console() |
| 156 | + console.print( |
| 157 | + f"🔒 Remote template requires agent-starter-pack version {version}", |
| 158 | + style="bold blue", |
| 159 | + ) |
| 160 | + console.print( |
| 161 | + f"📦 Switching to version {version}...", |
| 162 | + style="dim", |
| 163 | + ) |
| 164 | + |
| 165 | + # Reconstruct the original command but with version constraint |
| 166 | + import sys |
| 167 | + |
| 168 | + original_args = sys.argv[1:] # Skip 'agent-starter-pack' or script name |
| 169 | + |
| 170 | + # Add version lock specific parameters and handle remote URL replacement |
| 171 | + if original_agent_spec: |
| 172 | + # Replace remote agent spec with local path |
| 173 | + modified_args = [] |
| 174 | + for arg in original_args: |
| 175 | + if arg == original_agent_spec: |
| 176 | + # Replace remote URL with local template directory |
| 177 | + modified_args.append(f"local@{template_dir}") |
| 178 | + else: |
| 179 | + modified_args.append(arg) |
| 180 | + original_args = modified_args |
| 181 | + |
| 182 | + # Add version lock flags only for ASP versions 0.14.1 and above |
| 183 | + current_version = get_current_version() |
| 184 | + if pkg_version.parse(current_version) > pkg_version.parse("0.14.1"): |
| 185 | + original_args.extend(["--skip-welcome", "--locked"]) |
| 186 | + |
| 187 | + try: |
| 188 | + # Check if uvx is available |
| 189 | + subprocess.run(["uvx", "--version"], capture_output=True, check=True) |
| 190 | + except (subprocess.CalledProcessError, FileNotFoundError): |
| 191 | + console.print( |
| 192 | + f"❌ Remote template requires agent-starter-pack version {version}, but 'uvx' is not installed", |
| 193 | + style="bold red", |
| 194 | + ) |
| 195 | + console.print( |
| 196 | + "💡 Install uv to use version-locked remote templates:", |
| 197 | + style="bold blue", |
| 198 | + ) |
| 199 | + console.print(" curl -LsSf https://astral.sh/uv/install.sh | sh") |
| 200 | + console.print( |
| 201 | + " OR visit: https://docs.astral.sh/uv/getting-started/installation/" |
| 202 | + ) |
| 203 | + sys.exit(1) |
| 204 | + |
| 205 | + try: |
| 206 | + # Execute uvx with the locked version |
| 207 | + cmd = ["uvx", f"agent-starter-pack=={version}", *original_args] |
| 208 | + logging.debug(f"Executing nested command: {' '.join(cmd)}") |
| 209 | + subprocess.run(cmd, check=True) |
| 210 | + return True |
| 211 | + |
| 212 | + except subprocess.CalledProcessError as e: |
| 213 | + console.print( |
| 214 | + f"❌ Failed to execute with locked version {version}: {e}", |
| 215 | + style="bold red", |
| 216 | + ) |
| 217 | + console.print( |
| 218 | + "⚠️ Continuing with current version, but compatibility is not guaranteed", |
| 219 | + style="yellow", |
| 220 | + ) |
| 221 | + # Continue with current execution instead of failing completely |
| 222 | + |
| 223 | + return False |
| 224 | + |
| 225 | + |
130 | 226 | def fetch_remote_template( |
131 | 227 | spec: RemoteTemplateSpec, |
| 228 | + original_agent_spec: str | None = None, |
| 229 | + locked: bool = False, |
132 | 230 | ) -> tuple[pathlib.Path, pathlib.Path]: |
133 | 231 | """Fetch remote template and return path to template directory. |
134 | 232 |
|
135 | | - Uses Git to clone the remote repository. |
| 233 | + Uses Git to clone the remote repository. If the template contains a uv.lock |
| 234 | + with agent-starter-pack version constraint, will execute nested uvx command. |
136 | 235 |
|
137 | 236 | Args: |
138 | 237 | spec: Remote template specification |
| 238 | + original_agent_spec: Original agent spec string (used to prevent recursion) |
| 239 | + locked: Whether this is already a locked execution (prevents recursion) |
139 | 240 |
|
140 | 241 | Returns: |
141 | 242 | A tuple containing: |
@@ -188,6 +289,16 @@ def fetch_remote_template( |
188 | 289 | f"Template path not found in the repository: {spec.template_path}" |
189 | 290 | ) |
190 | 291 |
|
| 292 | + # Check for version lock and execute nested command if found |
| 293 | + if check_and_execute_with_version_lock( |
| 294 | + template_dir, original_agent_spec, locked |
| 295 | + ): |
| 296 | + # If we executed with locked version, the nested process will handle everything |
| 297 | + # Clean up and exit successfully |
| 298 | + shutil.rmtree(temp_path, ignore_errors=True) |
| 299 | + # Exit with success since the nested command will handle the rest |
| 300 | + sys.exit(0) |
| 301 | + |
191 | 302 | return template_dir, temp_path |
192 | 303 | except Exception as e: |
193 | 304 | # Clean up on error |
@@ -458,6 +569,41 @@ def display_adk_caveat_if_needed(agents: dict[int, dict[str, Any]]) -> None: |
458 | 569 | ) |
459 | 570 |
|
460 | 571 |
|
| 572 | +def parse_agent_starter_pack_version_from_lock( |
| 573 | + uv_lock_path: pathlib.Path, |
| 574 | +) -> str | None: |
| 575 | + """Parse agent-starter-pack version from uv.lock file. |
| 576 | +
|
| 577 | + Args: |
| 578 | + uv_lock_path: Path to uv.lock file |
| 579 | +
|
| 580 | + Returns: |
| 581 | + Version string if found, None otherwise |
| 582 | + """ |
| 583 | + if not uv_lock_path.exists(): |
| 584 | + return None |
| 585 | + |
| 586 | + try: |
| 587 | + with open(uv_lock_path, "rb") as f: |
| 588 | + lock_data = tomllib.load(f) |
| 589 | + |
| 590 | + # Look for agent-starter-pack in the packages section |
| 591 | + packages = lock_data.get("package", []) |
| 592 | + for package in packages: |
| 593 | + if package.get("name") == "agent-starter-pack": |
| 594 | + version = package.get("version") |
| 595 | + if version: |
| 596 | + logging.debug( |
| 597 | + f"Found agent-starter-pack version {version} in uv.lock" |
| 598 | + ) |
| 599 | + return version |
| 600 | + |
| 601 | + except Exception as e: |
| 602 | + logging.warning(f"Error parsing uv.lock file {uv_lock_path}: {e}") |
| 603 | + |
| 604 | + return None |
| 605 | + |
| 606 | + |
461 | 607 | def render_and_merge_makefiles( |
462 | 608 | base_template_path: pathlib.Path, |
463 | 609 | final_destination: pathlib.Path, |
|
0 commit comments