diff --git a/src/static/local-workspace.js b/src/static/local-workspace.js index 2daa288..7179a3a 100644 --- a/src/static/local-workspace.js +++ b/src/static/local-workspace.js @@ -91,7 +91,7 @@ function isLibPath(path) { function isWritablePath(path) { if (!path) return false; const root = path.split('/')[0]; - return root === 'code'; + return root === 'code' || root === 'demos'; } function splitParts(path) { @@ -367,6 +367,7 @@ class FileSystemBackend { } async ensureSeed() { + await this._resolveDir(['demos'], true); const codeDir = await this._resolveDir(['code'], true); let mainExists = false; for await (const [name] of codeDir.entries()) { @@ -543,20 +544,22 @@ class FileSystemBackend { async listAllPyFiles() { const out = {}; - let codeDir; - try { - codeDir = await this._resolveDir(['code']); - } catch (_e) { - return out; - } - const collected = []; - await this._readAllRecursive(['code'], codeDir, collected); - for (const { path, handle } of collected) { - if (!path.toLowerCase().endsWith('.py')) continue; + for (const rootSeg of ['code', 'demos']) { + let dir; try { - out[path] = await (await handle.getFile()).text(); + dir = await this._resolveDir([rootSeg]); } catch (_e) { - // Skip unreadable files. + continue; + } + const collected = []; + await this._readAllRecursive([rootSeg], dir, collected); + for (const { path, handle } of collected) { + if (!path.toLowerCase().endsWith('.py')) continue; + try { + out[path] = await (await handle.getFile()).text(); + } catch (_e) { + // Skip unreadable files. + } } } return out; @@ -564,19 +567,21 @@ class FileSystemBackend { async listAllUserFiles() { const out = {}; - let codeDir; - try { - codeDir = await this._resolveDir(['code']); - } catch (_e) { - return out; - } - const collected = []; - await this._readAllRecursive(['code'], codeDir, collected); - for (const { path, handle } of collected) { + for (const rootSeg of ['code', 'demos']) { + let dir; try { - out[path] = await (await handle.getFile()).text(); + dir = await this._resolveDir([rootSeg]); } catch (_e) { - // Skip unreadable files (e.g. binaries the editor can't open). + continue; + } + const collected = []; + await this._readAllRecursive([rootSeg], dir, collected); + for (const { path, handle } of collected) { + try { + out[path] = await (await handle.getFile()).text(); + } catch (_e) { + // Skip unreadable files (e.g. binaries the editor can't open). + } } } return out; @@ -700,7 +705,7 @@ export class LocalWorkspaceClient { /* --- Lib bundle ------------------------------------------------- */ async _tryLoadStaticBundledLib() { - const names = ['machine.py', 'neopixel.py']; + const names = ['machine.py', 'neopixel.py', 'browser_fetch.py']; const map = {}; for (const name of names) { const r = await fetch(`/static/bundled-lib/${encodeURIComponent(name)}`, { @@ -770,6 +775,9 @@ export class LocalWorkspaceClient { if (!filtered.some((row) => row.name === 'code')) { filtered.push({ name: 'code', is_directory: true, size: null }); } + if (!filtered.some((row) => row.name === 'demos')) { + filtered.push({ name: 'demos', is_directory: true, size: null }); + } if (this.libFiles && Object.keys(this.libFiles).length) { filtered.push({ name: 'lib', is_directory: true, size: null }); } @@ -810,7 +818,7 @@ export class LocalWorkspaceClient { const path = normalizePath(rawPath); if (!path) return jsonResponse(400, { detail: 'Empty path' }); if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' }); - if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable (lib is read-only)' }); + if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable (lib is read-only)' }); const content = body && typeof body.content === 'string' ? body.content : ''; await this.backend.writeFile(path, content); return jsonResponse(200, { filename: basename(path) }); @@ -821,7 +829,7 @@ export class LocalWorkspaceClient { const path = normalizePath(rawPath); if (!path) return jsonResponse(400, { detail: 'Empty path' }); if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' }); - if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable' }); + if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' }); const r = await this.backend.deleteFile(path); if (!r.ok) { if (r.reason === 'missing') return jsonResponse(404, { detail: 'File not found' }); @@ -836,7 +844,7 @@ export class LocalWorkspaceClient { const path = normalizePath(rawPath); if (!path) return jsonResponse(400, { detail: 'Empty path' }); if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' }); - if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable' }); + if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' }); const r = await this.backend.createFolder(path); if (!r.ok) { if (r.reason === 'exists') return jsonResponse(400, { detail: 'Folder already exists' }); @@ -850,7 +858,7 @@ export class LocalWorkspaceClient { const path = normalizePath(rawPath); if (!path) return jsonResponse(400, { detail: 'Empty path' }); if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' }); - if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable' }); + if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' }); const r = await this.backend.deleteFolder(path); if (!r.ok) { if (r.reason === 'missing') return jsonResponse(404, { detail: 'Folder not found' }); @@ -867,7 +875,7 @@ export class LocalWorkspaceClient { if (!source) return jsonResponse(400, { detail: 'Missing source_path' }); if (isLibPath(source) || isLibPath(dest)) return jsonResponse(403, { detail: 'lib is read-only' }); if (!isWritablePath(source) || (dest && !isWritablePath(dest))) { - return jsonResponse(403, { detail: 'Only code/ is writable' }); + return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' }); } const r = await this.backend.movePath(source, dest); if (!r.ok) {