fix: add _resolve_precon_dir() auto-detection so CFG.precon_dir is not empty

The precon deck list was missing after the monolith refactor because
PRECON_DIR was hardcoded in services/precon_service.py with no CLI arg,
env var, or auto-resolve fallback — unlike forge_jar, forge_dir, etc.

Changes:
- Add precon_dir field to Config class (models/state.py)
- Add --precon-dir CLI arg and PRECON_DIR env var support
- Add _resolve_precon_dir() with project-root, parent-dir, and
  forge_dir-relative detection; creates dir as fallback for downloads
- Refactor precon_service.py to use _get_precon_dir() helper that reads
  CFG.precon_dir at runtime instead of a hardcoded module-level Path
- Update routes/precon.py to use _get_precon_dir() for install endpoints
- Wire _on_startup to resolve precon_dir for uvicorn-direct launches
- Remove unused PRECON_DIR/PRECON_INDEX imports from routes/lab.py
- Audit confirms all other CFG path fields already have fallbacks
This commit is contained in:
Commander AI Lab Agent committed 2026-03-18 04:56:03 +00:00
1 parent a5b9d70f04
commit fbc3bdb6b5
6 files changed
+61 -10

No files matched your search

+33
View File
@@ -64,6 +64,9 @@ app.include_router(ml_router)
async def _on_startup():
"""Ensure DB and precon index are ready (supports uvicorn --reload)."""
import routes.shared as _shared
# Ensure precon_dir is resolved even when started via uvicorn directly
if not CFG.precon_dir:
CFG.precon_dir = _resolve_precon_dir()
_shared.init_collection_db()
if not _shared.PRECON_INDEX:
_shared.download_precon_database()
@@ -98,6 +101,7 @@ def _parse_args():
p.add_argument("--forge-dir", default=os.environ.get("FORGE_DIR", ""))
p.add_argument("--forge-decks-dir", default=os.environ.get("FORGE_DECKS_DIR", ""))
p.add_argument("--lab-jar", default=os.environ.get("LAB_JAR", ""))
p.add_argument("--precon-dir", default=os.environ.get("PRECON_DIR", ""))
p.add_argument("--port", type=int, default=int(os.environ.get("LAB_PORT", "8080")))
p.add_argument("--ximilar-key", default=os.environ.get("XIMILAR_API_KEY", ""))
p.add_argument("--pplx-key", default=os.environ.get("PPLX_API_KEY", ""))
@@ -225,6 +229,33 @@ def _resolve_lab_jar() -> str:
return ""
def _resolve_precon_dir() -> str:
"""Auto-detect the precon-decks directory.
The precon-decks dir lives in the project root and contains downloaded
.dck files plus precon-index.json. We check relative to this file
(lab_api.py) first, then common sibling paths.
"""
project_root = Path(__file__).parent
# 1. Standard location: <project-root>/precon-decks
candidate = project_root / "precon-decks"
if candidate.is_dir():
return str(candidate)
# 2. Check parent directory (in case running from a subdirectory)
candidate = project_root.parent / "precon-decks"
if candidate.is_dir():
return str(candidate)
# 3. Derive from forge_dir if available
if CFG.forge_dir:
candidate = Path(CFG.forge_dir) / "precon-decks"
if candidate.is_dir():
return str(candidate)
# 4. Fall back: create the directory in the project root so downloads work
fallback = project_root / "precon-decks"
fallback.mkdir(parents=True, exist_ok=True)
return str(fallback)
def main():
args = _parse_args()
setup_logging(logging.DEBUG if args.verbose else logging.INFO)
@@ -233,6 +264,7 @@ def main():
CFG.forge_dir = args.forge_dir or _resolve_forge_dir()
CFG.forge_decks_dir = args.forge_decks_dir or _resolve_forge_decks_dir()
CFG.lab_jar = args.lab_jar or _resolve_lab_jar()
CFG.precon_dir = args.precon_dir or _resolve_precon_dir()
CFG.port = args.port
CFG.ximilar_api_key = args.ximilar_key
CFG.pplx_api_key = args.pplx_key
@@ -245,6 +277,7 @@ def main():
log.info(f" Forge JAR: {CFG.forge_jar or 'NOT SET'}")
log.info(f" Forge Dir: {CFG.forge_dir or 'NOT SET'}")
log.info(f" Decks Dir: {CFG.forge_decks_dir or 'NOT SET'}")
log.info(f" Precon Dir: {CFG.precon_dir or 'NOT SET'}")
log.info(f" Lab JAR: {CFG.lab_jar or 'NOT SET'}")
log.info(f" Results: {CFG.results_dir}")
log.info(f" Ximilar: {'configured' if CFG.ximilar_api_key else 'NOT SET'}")
+1
View File
@@ -20,6 +20,7 @@ class Config:
forge_dir: str = ""
forge_decks_dir: str = ""
lab_jar: str = ""
precon_dir: str = ""
results_dir: str = "results"
port: int = 8080
ximilar_api_key: str = ""
-2
View File
@@ -38,8 +38,6 @@ from routes.shared import (
StartResponse,
StatusResponse,
AI_PROFILES,
PRECON_DIR,
PRECON_INDEX,
build_java_command,
run_batch_subprocess,
_run_deepseek_batch_thread,
+4 -3
View File
@@ -16,7 +16,8 @@ from pathlib import Path
from fastapi import APIRouter, HTTPException
import routes.shared as _shared # module reference, not value import
from routes.shared import CFG, PRECON_DIR, download_precon_database, log
from routes.shared import CFG, download_precon_database, log
from services.precon_service import _get_precon_dir
router = APIRouter(tags=["precon"])
@@ -36,7 +37,7 @@ async def install_precon(req: dict):
if not file_name:
raise HTTPException(400, "fileName is required")
src = PRECON_DIR / file_name
src = _get_precon_dir() / file_name
if not src.exists():
raise HTTPException(404, f"Precon not found: {file_name}")
@@ -71,7 +72,7 @@ async def install_precons_batch(req: dict):
results = []
for file_name in file_names:
src = PRECON_DIR / file_name
src = _get_precon_dir() / file_name
if not src.exists():
results.append({"fileName": file_name, "installed": False, "error": "not found"})
continue
+1
View File
@@ -186,6 +186,7 @@ from services.import_service import (
from services.precon_service import (
PRECON_DIR, PRECON_INDEX, GITHUB_PRECON_URL, PRECON_CACHE_HOURS,
load_precon_index, _sanitize_filename, _deck_to_dck, download_precon_database,
_get_precon_dir,
)
# ══════════════════════════════════════════════════════════════
+22 -5
View File
@@ -12,7 +12,9 @@ from urllib.request import urlopen, Request
log = logging.getLogger("commander_ai_lab.api")
PRECON_DIR = Path(__file__).parent.parent / "precon-decks"
# Fallback default — used only when CFG.precon_dir has not been resolved yet.
_DEFAULT_PRECON_DIR = Path(__file__).parent.parent / "precon-decks"
PRECON_INDEX: list[dict] = []
GITHUB_PRECON_URL = (
"https://raw.githubusercontent.com/taw/magic-preconstructed-decks-data/"
@@ -21,9 +23,23 @@ GITHUB_PRECON_URL = (
PRECON_CACHE_HOURS = 168
def _get_precon_dir() -> Path:
"""Return the active precon-decks directory from CFG, with fallback."""
from models.state import CFG
if CFG.precon_dir:
return Path(CFG.precon_dir)
return _DEFAULT_PRECON_DIR
# Re-export for backwards compat — callers that import PRECON_DIR at module
# level will get the default. Runtime code should call _get_precon_dir().
PRECON_DIR = _DEFAULT_PRECON_DIR
def load_precon_index():
global PRECON_INDEX
idx_path = PRECON_DIR / "precon-index.json"
precon_dir = _get_precon_dir()
idx_path = precon_dir / "precon-index.json"
if idx_path.exists():
with open(idx_path, "r", encoding="utf-8") as f:
PRECON_INDEX = json.load(f)
@@ -61,7 +77,8 @@ def _deck_to_dck(deck_data: dict) -> str:
def download_precon_database(force: bool = False) -> dict:
global PRECON_INDEX
idx_path = PRECON_DIR / "precon-index.json"
precon_dir = _get_precon_dir()
idx_path = precon_dir / "precon-index.json"
if not force and idx_path.exists():
try:
with open(idx_path, "r", encoding="utf-8") as f:
@@ -91,7 +108,7 @@ def download_precon_database(force: bool = False) -> dict:
and (d.get('format') or '').lower() == 'commander'
]
log.info(f" Precons: Found {len(commander_decks)} Commander precon decks")
PRECON_DIR.mkdir(parents=True, exist_ok=True)
precon_dir.mkdir(parents=True, exist_ok=True)
name_counts = Counter(_sanitize_filename(d['name']) for d in commander_decks)
dup_names = {n for n, c in name_counts.items() if c > 1}
index = []
@@ -102,7 +119,7 @@ def download_precon_database(force: bool = False) -> dict:
sc = (deck.get('set_code') or 'unk').upper()
safe_name = f"{safe_name}_{sc}"
file_name = f"{safe_name}.dck"
dck_path = PRECON_DIR / file_name
dck_path = precon_dir / file_name
commanders = deck.get('commander', [])
cmdr_names = [c['name'] for c in commanders] if commanders else []
total_cards = sum(c.get('count', 1) for c in deck.get('cards', [])) + sum(c.get('count', 1) for c in commanders)