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>
This commit is contained in:
@@ -14,12 +14,13 @@ from editor_app.routers.auth_routes import router as auth_router
|
||||
from editor_app.routers.files import router as files_router
|
||||
from editor_app.routers.frontend import router as frontend_router
|
||||
from editor_app.routers.users_admin import router as users_admin_router
|
||||
from editor_app.services import accounts
|
||||
from editor_app.services import accounts, user_workspace
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
(WORKSPACE_ROOT / "lib").mkdir(parents=True, exist_ok=True)
|
||||
user_workspace.ensure_workspace_lib()
|
||||
engine = get_engine()
|
||||
Base.metadata.create_all(bind=engine)
|
||||
with engine.begin() as conn:
|
||||
|
||||
@@ -15,7 +15,12 @@ def _workspace_root(workspace_root: Path | None = None) -> Path:
|
||||
|
||||
|
||||
def _shared_lib_root() -> Path:
|
||||
return (config.WORKSPACE_ROOT.resolve() / LIB_DIR_NAME).resolve()
|
||||
"""Shared MicroPython stubs live under WORKSPACE_ROOT/lib; seed from bundle if missing (e.g. volume wiped)."""
|
||||
lib = (config.WORKSPACE_ROOT.resolve() / LIB_DIR_NAME).resolve()
|
||||
from editor_app.services.user_workspace import ensure_workspace_lib
|
||||
|
||||
ensure_workspace_lib()
|
||||
return lib
|
||||
|
||||
|
||||
def normalize_relative_path(relative_path: str) -> str:
|
||||
@@ -213,25 +218,24 @@ def delete_folder(folder_path: str, workspace_root: Path | None = None) -> None:
|
||||
|
||||
|
||||
def collect_python_sources(workspace_root: Path | None = None) -> dict[str, str]:
|
||||
"""Return all UTF-8 .py files under the workspace for browser-side Pyodide sync."""
|
||||
"""Return UTF-8 `.py` under the scoped workspace plus shared stubs from `WORKSPACE_ROOT/lib/` (bundled Micropython mocks for Jedi/completion and Pyodide)."""
|
||||
result: dict[str, str] = {}
|
||||
workspace = _workspace_root(workspace_root)
|
||||
if not workspace.exists():
|
||||
return result
|
||||
for path in workspace.rglob("*.py"):
|
||||
try:
|
||||
rel = path.relative_to(workspace)
|
||||
except ValueError:
|
||||
continue
|
||||
if any(part.startswith(".") for part in rel.parts):
|
||||
continue
|
||||
try:
|
||||
key = str(rel).replace("\\", "/")
|
||||
result[key] = path.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
continue
|
||||
if workspace.exists():
|
||||
for path in workspace.rglob("*.py"):
|
||||
try:
|
||||
rel = path.relative_to(workspace)
|
||||
except ValueError:
|
||||
continue
|
||||
if any(part.startswith(".") for part in rel.parts):
|
||||
continue
|
||||
try:
|
||||
key = str(rel).replace("\\", "/")
|
||||
result[key] = path.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
continue
|
||||
shared_lib = _shared_lib_root()
|
||||
if shared_lib.exists() and shared_lib.is_dir() and shared_lib != (workspace / LIB_DIR_NAME).resolve():
|
||||
if shared_lib.is_dir():
|
||||
for path in shared_lib.rglob("*.py"):
|
||||
try:
|
||||
rel = path.relative_to(shared_lib)
|
||||
|
||||
@@ -16,6 +16,22 @@ _CANONICAL_DEMO_FILENAMES = (
|
||||
)
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
@@ -87,6 +87,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/static/script.js?v=25"></script>
|
||||
<script type="module" src="/static/script.js?v=26"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -293,9 +293,6 @@ class TextEditor {
|
||||
return;
|
||||
}
|
||||
for (const path of session.openTabPaths) {
|
||||
if (!this.isSuperuser && typeof path === 'string' && path.startsWith('lib/')) {
|
||||
continue;
|
||||
}
|
||||
await this.openFile(path);
|
||||
}
|
||||
if (session.activeTabPath && this.findTab(session.activeTabPath)) {
|
||||
@@ -338,7 +335,8 @@ class TextEditor {
|
||||
}
|
||||
|
||||
getVisibleTopLevelNames() {
|
||||
return this.isSuperuser ? new Set(['code', 'lib']) : new Set(['code']);
|
||||
/* lib is shared read-only for everyone; browsing is allowed, saves are blocked in API/UI. */
|
||||
return new Set(['code', 'lib']);
|
||||
}
|
||||
|
||||
getDefaultEditableRoot() {
|
||||
@@ -352,10 +350,7 @@ class TextEditor {
|
||||
this.selectedIsDirectory = true;
|
||||
this.expandedDirs.add('code');
|
||||
await this.loadDirectory('code', { suppressError: true });
|
||||
if (this.isSuperuser) {
|
||||
await this.ensureFolderExists('lib');
|
||||
await this.loadDirectory('lib', { suppressError: true });
|
||||
}
|
||||
await this.loadDirectory('lib', { suppressError: true });
|
||||
this.renderFileTree();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user