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