Move machine.py and neopixel.py into a tracked /lib/ at the repo root and auto-copy them into WORKSPACE_ROOT/lib whenever files are missing, so empty volumes and fresh per-user workspaces always have the read-only stubs available to Jedi and Pyodide. Allow all users to browse lib/ in the UI (writes still gated by the API), and add tests covering initial seeding and re-population after the dir is wiped. Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from editor_app import config
|
|
|
|
DEFAULT_MAIN_PY = 'print("Hello, World!")\n'
|
|
|
|
# Self-contained demos copied from shipped `workspace/code/` (stdlib + machine/neopixel/time only).
|
|
_CANONICAL_DEMO_FILENAMES = (
|
|
"pattern_rainbow_demo.py",
|
|
"pattern_twinkle_demo.py",
|
|
"pattern_chase_demo.py",
|
|
)
|
|
|
|
|
|
def ensure_workspace_lib(workspace_root: Path | None = None) -> None:
|
|
"""Copy shipped MicroPython stubs from the repo into WORKSPACE_ROOT/lib when each file is absent."""
|
|
dst_root = (workspace_root or config.WORKSPACE_ROOT).resolve() / "lib"
|
|
dst_root.mkdir(parents=True, exist_ok=True)
|
|
src_root = config.PROJECT_ROOT.resolve() / "lib"
|
|
if not src_root.is_dir():
|
|
return
|
|
for src in sorted(src_root.glob("*.py")):
|
|
if not src.is_file():
|
|
continue
|
|
dst = dst_root / src.name
|
|
if dst.exists():
|
|
continue
|
|
shutil.copy2(src, dst)
|
|
|
|
|
|
def safe_workspace_leaf(username: str, user_id: int) -> str:
|
|
base = re.sub(r"[^a-zA-Z0-9._-]+", "-", username.strip()).strip("-").lower() or "user"
|
|
return f"{base}-{user_id}"
|
|
|
|
|
|
def user_workspace_root(user_id: int, username: str, workspace_root: Path | None = None) -> Path:
|
|
root = (workspace_root or config.WORKSPACE_ROOT).resolve()
|
|
return root / "users" / safe_workspace_leaf(username, user_id)
|
|
|
|
|
|
def _seed_canonical_demos_into_code(code_dir: Path) -> None:
|
|
src_root = config.PROJECT_ROOT.resolve() / "workspace" / "code"
|
|
for filename in _CANONICAL_DEMO_FILENAMES:
|
|
dst = code_dir / filename
|
|
if dst.exists():
|
|
continue
|
|
src = src_root / filename
|
|
if src.is_file():
|
|
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
|
|
|
|
|
def ensure_default_code_main(user_root: Path) -> None:
|
|
"""Ensure code/ has main.py and self-contained NeoPixel demos (copied from repo workspace/code/)."""
|
|
code_dir = user_root / "code"
|
|
code_dir.mkdir(parents=True, exist_ok=True)
|
|
main_py = code_dir / "main.py"
|
|
if not main_py.exists():
|
|
main_py.write_text(DEFAULT_MAIN_PY, encoding="utf-8")
|
|
_seed_canonical_demos_into_code(code_dir)
|
|
|
|
|
|
def rename_user_workspace_leaf(
|
|
user_id: int, old_username: str, new_username: str, workspace_root: Path | None = None
|
|
) -> None:
|
|
"""Rename per-user workspace directory when login name changes."""
|
|
root = (workspace_root or config.WORKSPACE_ROOT).resolve()
|
|
users_dir = root / "users"
|
|
src = users_dir / safe_workspace_leaf(old_username, user_id)
|
|
dst = users_dir / safe_workspace_leaf(new_username, user_id)
|
|
if src.resolve() == dst.resolve():
|
|
return
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
if dst.exists():
|
|
raise ValueError("Workspace folder for new username already exists; pick another username.")
|
|
if src.exists():
|
|
shutil.move(str(src), str(dst))
|
|
else:
|
|
ensure_default_code_main(dst)
|