# offline_preview.py """ ShaiWatcher offline preview (Discord-less) Run from anywhere: # optional (forces root if your layout is odd) # export SHAI_PROJECT_ROOT=/absolute/path/to/shaiwatcher # export SHAI_DOCS_HOST=127.0.0.1 # export SHAI_DOCS_PORT=8910 # export SHAI_OFFLINE=1 python3 offline_preview.py """ import os import sys import asyncio import pathlib import traceback VERSION = "offline-preview-3" # ---------- repo root discovery ---------- def _find_project_root() -> pathlib.Path: cand = [] env = os.environ.get("SHAI_PROJECT_ROOT") if env: cand.append(pathlib.Path(env).resolve()) here = pathlib.Path(__file__).resolve().parent cand.extend([ pathlib.Path.cwd().resolve(), # current working dir here, # folder containing this file here.parent, # one level up here.parent.parent, # two levels up ]) # Also walk upwards from CWD a few levels to be forgiving cur = pathlib.Path.cwd().resolve() for _ in range(5): cand.append(cur) cur = cur.parent tried = [] for c in cand: tried.append(str(c)) if (c / "modules").is_dir() and (c / "modules" / "common").is_dir(): return c raise FileNotFoundError( "Could not locate project root with a 'modules/common' folder.\n" f"Tried:\n - " + "\n - ".join(tried) + "\nTip: set SHAI_PROJECT_ROOT=/absolute/path/to/repo" ) PROJECT_ROOT = _find_project_root() if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) # ---------- now safe to import project modules ---------- import discord # type: ignore from discord.ext import commands # type: ignore # Optional: your config helper if cogs expect it to exist try: from modules.common.settings import cfg as cfg_helper # noqa: F401 except Exception as e: print("[OFFLINE] Warning: couldn't import cfg helper:", repr(e)) def _discover_extensions(project_root: pathlib.Path): modules_path = project_root / "modules" exts = [] for folder in modules_path.iterdir(): if not folder.is_dir(): continue if folder.name == "common": # match your prod loader continue for file in folder.glob("*.py"): if file.name == "__init__.py": continue exts.append(f"modules.{folder.name}.{file.stem}") return exts async def main(): print(f"[OFFLINE] ShaiWatcher offline preview v{VERSION}") print(f"[OFFLINE] Project root -> {PROJECT_ROOT}") # Keep intents minimal; we never connect anyway intents = discord.Intents.none() intents.guilds = True bot = commands.Bot(command_prefix="!", intents=intents) # Mark environment as offline for any cogs that check it os.environ.setdefault("SHAI_OFFLINE", "1") # Bind docs to localhost by default while testing os.environ.setdefault("SHAI_DOCS_HOST", "127.0.0.1") os.environ.setdefault("SHAI_DOCS_PORT", "8910") os.environ.setdefault("SHAI_DOCS_TITLE", "ShaiWatcher (Offline Preview)") # Optional: isolate data file so we don't touch prod paths data_file = os.environ.get("SHAI_DATA", str(PROJECT_ROOT / ".offline_data.json")) try: from data_manager import DataManager # if your project has this at root os.makedirs(os.path.dirname(data_file) or ".", exist_ok=True) if not os.path.exists(data_file): with open(data_file, "w", encoding="utf-8") as f: f.write("{}") bot.data_manager = DataManager(data_file) print(f"[OFFLINE] DATA_FILE -> {data_file}") except Exception as e: print("[OFFLINE] DataManager unavailable/failed:", repr(e)) os.environ.setdefault("SHAI_OFFLINE", "1") # before loading cogs # Load extensions exactly like prod failures = 0 for ext in _discover_extensions(PROJECT_ROOT): try: await bot.load_extension(ext) print(f"[Modules] Loaded: {ext}") except Exception as e: failures += 1 print(f"[Modules] Failed to load {ext}: {e}") traceback.print_exc() if failures: print(f"[OFFLINE] Loaded with {failures} module error(s). See logs above.") docs = bot.get_cog("DocsSite") if docs and hasattr(docs, "force_ready"): docs.force_ready(True) # Make is_ready() == True so DocsSite serves immediately try: # discord.py sets this in login/READY; we emulate it if not hasattr(bot, "_ready") or bot._ready is None: # type: ignore[attr-defined] bot._ready = asyncio.Event() # type: ignore[attr-defined] bot._ready.set() # type: ignore[attr-defined] except Exception: pass print("[OFFLINE] Docs: http://%s:%s/" % (os.environ.get("SHAI_DOCS_HOST", "0.0.0.0"), os.environ.get("SHAI_DOCS_PORT", "8911"))) print("[OFFLINE] This runner does NOT connect to Discord.") # Idle forever; DocsSite runs in its own daemon thread await asyncio.Event().wait() if __name__ == "__main__": asyncio.run(main())