Files
python-editor/src/editor_app/services/user_workspace.py
Jimmy a2318f2244 Ship MicroPython stubs from repo lib/ and seed workspace lib on startup
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>
2026-05-10 02:59:34 +12:00

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)