Support demos/ in the local-mode workspace client
Treat demos as a writable top-level folder alongside code/, seed it on first open, and ship browser_fetch.py with bundled lib stubs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user