Migrate a project from the legacy root-level layout to ``.autoforge/``. The migration is incremental and safe: * If the agent is running (lock files present) the migration is skipped entirely to avoid corrupting in-use databases. * Each file/directory is migrated independently.
(project_dir: Path)
| 198 | # --------------------------------------------------------------------------- |
| 199 | |
| 200 | def migrate_project_layout(project_dir: Path) -> list[str]: |
| 201 | """Migrate a project from the legacy root-level layout to ``.autoforge/``. |
| 202 | |
| 203 | The migration is incremental and safe: |
| 204 | |
| 205 | * If the agent is running (lock files present) the migration is skipped |
| 206 | entirely to avoid corrupting in-use databases. |
| 207 | * Each file/directory is migrated independently. If any single step |
| 208 | fails the error is logged and migration continues with the remaining |
| 209 | items. Partial migration is safe because the dual-path resolution |
| 210 | strategy will find files at whichever location they ended up in. |
| 211 | |
| 212 | Returns: |
| 213 | A list of human-readable descriptions of what was migrated, e.g. |
| 214 | ``["prompts/ -> .autoforge/prompts/", "features.db -> .autoforge/features.db"]``. |
| 215 | An empty list means nothing was migrated (either everything is |
| 216 | already migrated, or the agent is running). |
| 217 | """ |
| 218 | # Safety: refuse to migrate while an agent is running |
| 219 | if has_agent_running(project_dir): |
| 220 | logger.warning("Migration skipped: agent or dev-server is running for %s", project_dir) |
| 221 | return [] |
| 222 | |
| 223 | # --- 0. Migrate .autocoder/ → .autoforge/ directory ------------------- |
| 224 | old_autocoder_dir = project_dir / ".autocoder" |
| 225 | new_autoforge_dir = project_dir / ".autoforge" |
| 226 | if old_autocoder_dir.exists() and old_autocoder_dir.is_dir() and not new_autoforge_dir.exists(): |
| 227 | try: |
| 228 | old_autocoder_dir.rename(new_autoforge_dir) |
| 229 | logger.info("Migrated .autocoder/ -> .autoforge/") |
| 230 | migrated: list[str] = [".autocoder/ -> .autoforge/"] |
| 231 | except Exception: |
| 232 | logger.warning("Failed to migrate .autocoder/ -> .autoforge/", exc_info=True) |
| 233 | migrated = [] |
| 234 | else: |
| 235 | migrated = [] |
| 236 | |
| 237 | autoforge_dir = ensure_autoforge_dir(project_dir) |
| 238 | |
| 239 | # --- 1. Migrate prompts/ directory ----------------------------------- |
| 240 | try: |
| 241 | old_prompts = project_dir / "prompts" |
| 242 | new_prompts = autoforge_dir / "prompts" |
| 243 | if old_prompts.exists() and old_prompts.is_dir() and not new_prompts.exists(): |
| 244 | shutil.copytree(str(old_prompts), str(new_prompts)) |
| 245 | shutil.rmtree(str(old_prompts)) |
| 246 | migrated.append("prompts/ -> .autoforge/prompts/") |
| 247 | logger.info("Migrated prompts/ -> .autoforge/prompts/") |
| 248 | except Exception: |
| 249 | logger.warning("Failed to migrate prompts/ directory", exc_info=True) |
| 250 | |
| 251 | # --- 2. Migrate SQLite databases (features.db, assistant.db) --------- |
| 252 | db_names = ("features.db", "assistant.db") |
| 253 | for db_name in db_names: |
| 254 | try: |
| 255 | old_db = project_dir / db_name |
| 256 | new_db = autoforge_dir / db_name |
| 257 | if old_db.exists() and not new_db.exists(): |
no test coverage detected