UNPKG

@firesystem/workspace

Version:

Multi-project workspace manager for Firesystem with support for multiple sources

1,682 lines (1,675 loc) 54.4 kB
// src/WorkspaceFileSystem.ts import { FileSystemEvents, TypedEventEmitter } from "@firesystem/core"; // src/WorkspaceDatabase.ts var DB_NAME = "@firesystem/workspace"; var DB_VERSION = 1; var WorkspaceDatabase = class { constructor(dbName = DB_NAME, dbVersion = DB_VERSION) { this.db = null; this.dbName = dbName; this.dbVersion = dbVersion; } /** * Open or create the workspace database */ async open() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.dbVersion); request.onerror = () => { reject(new Error(`Failed to open database: ${request.error}`)); }; request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = request.result; const oldVersion = event.oldVersion; if (oldVersion < 1) { if (!db.objectStoreNames.contains("projects")) { const projectStore = db.createObjectStore("projects", { keyPath: "id" }); projectStore.createIndex("name", "name", { unique: false }); projectStore.createIndex("lastAccessed", "lastAccessed", { unique: false }); } if (!db.objectStoreNames.contains("workspaceState")) { db.createObjectStore("workspaceState", { keyPath: "id" }); } } }; }); } /** * Ensure database is open */ ensureOpen() { if (!this.db) { throw new Error("Database not opened. Call open() first."); } } /** * Close the database */ close() { if (this.db) { this.db.close(); this.db = null; } } // Project CRUD operations /** * Save or update a project configuration */ async saveProject(project) { this.ensureOpen(); const tx = this.db.transaction(["projects"], "readwrite"); const store = tx.objectStore("projects"); await new Promise((resolve, reject) => { const request = store.put(project); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Get a project by ID */ async getProject(id) { this.ensureOpen(); const tx = this.db.transaction(["projects"], "readonly"); const store = tx.objectStore("projects"); return new Promise((resolve, reject) => { const request = store.get(id); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); } /** * List all projects */ async listProjects() { this.ensureOpen(); const tx = this.db.transaction(["projects"], "readonly"); const store = tx.objectStore("projects"); return new Promise((resolve, reject) => { const request = store.getAll(); request.onsuccess = () => resolve(request.result || []); request.onerror = () => reject(request.error); }); } /** * List recent projects */ async listRecentProjects(limit = 10) { this.ensureOpen(); const tx = this.db.transaction(["projects"], "readonly"); const index = tx.objectStore("projects").index("lastAccessed"); const projects = []; return new Promise((resolve, reject) => { const request = index.openCursor(null, "prev"); request.onsuccess = () => { const cursor = request.result; if (cursor && projects.length < limit) { projects.push(cursor.value); cursor.continue(); } else { resolve(projects); } }; request.onerror = () => reject(request.error); }); } /** * Delete a project */ async deleteProject(id) { this.ensureOpen(); const tx = this.db.transaction(["projects"], "readwrite"); const store = tx.objectStore("projects"); await new Promise((resolve, reject) => { const request = store.delete(id); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Update project last accessed time */ async touchProject(id) { const project = await this.getProject(id); if (project) { project.lastAccessed = /* @__PURE__ */ new Date(); await this.saveProject(project); } } /** * Update project state */ async updateProjectState(id, updates) { const project = await this.getProject(id); if (project) { Object.assign(project, updates); await this.saveProject(project); } } // Workspace state operations /** * Save workspace state */ async saveWorkspaceState(state) { this.ensureOpen(); const tx = this.db.transaction(["workspaceState"], "readwrite"); const store = tx.objectStore("workspaceState"); await new Promise((resolve, reject) => { const request = store.put(state); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Get workspace state */ async getWorkspaceState() { this.ensureOpen(); const tx = this.db.transaction(["workspaceState"], "readonly"); const store = tx.objectStore("workspaceState"); return new Promise((resolve, reject) => { const request = store.get("current"); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); } /** * Clear all data */ async clear() { this.ensureOpen(); const tx = this.db.transaction(["projects", "workspaceState"], "readwrite"); await Promise.all([ new Promise((resolve, reject) => { const request = tx.objectStore("projects").clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }), new Promise((resolve, reject) => { const request = tx.objectStore("workspaceState").clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }) ]); } /** * Check if a project exists by checking IndexedDB databases */ static async projectDatabaseExists(dbName) { if (!("databases" in indexedDB)) { return new Promise((resolve) => { const testOpen = indexedDB.open(dbName); testOpen.onsuccess = () => { const db = testOpen.result; const exists = db.version > 0; db.close(); resolve(exists); }; testOpen.onerror = () => resolve(false); }); } const databases = await indexedDB.databases(); return databases.some((db) => db.name === dbName); } /** * Discover existing Firesystem IndexedDB databases */ async discoverIndexedDBProjects() { if (!("databases" in indexedDB)) { console.warn("IndexedDB.databases() not supported in this browser"); return []; } const databases = await indexedDB.databases(); return databases.filter((db) => { const name = db.name || ""; return name.startsWith("firesystem-") || name.startsWith("@firesystem/") || name.includes("-filesystem"); }).map((db) => db.name); } }; // src/import-export/WorkspaceImporter.ts var WorkspaceImporter = class { /** * Import workspace from JSON URL */ static async fromJsonUrl(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch workspace: ${response.statusText}`); } const data = await response.json(); return this.parseExport(data); } /** * Import workspace from GitHub Gist */ static async fromGitHubGist(gistId, token) { const headers = { "Accept": "application/vnd.github.v3+json" }; if (token) { headers["Authorization"] = `token ${token}`; } const response = await fetch(`https://api.github.com/gists/${gistId}`, { headers }); if (!response.ok) { throw new Error(`Failed to fetch gist: ${response.statusText}`); } const gist = await response.json(); const workspaceFile = Object.values(gist.files).find( (file) => file.filename === "workspace.json" ); if (!workspaceFile) { throw new Error("No workspace.json found in gist"); } const data = JSON.parse(workspaceFile.content); return this.parseExport(data); } /** * Import workspace from API endpoint */ static async fromApi(url, options) { const response = await fetch(url, { method: options?.method || "GET", headers: { "Content-Type": "application/json", ...options?.headers }, body: options?.body ? JSON.stringify(options.body) : void 0 }); if (!response.ok) { throw new Error(`Failed to fetch from API: ${response.statusText}`); } const data = await response.json(); return this.parseExport(data); } /** * Parse export data into workspace config */ static parseExport(data) { if (!data.version || !data.version.startsWith("1.")) { throw new Error(`Unsupported workspace version: ${data.version}`); } const projects = data.projects.map((p) => { let source; if (p.files && p.files.length > 0) { source = { type: "memory", config: { initialData: p.files.reduce((acc, file) => { acc[file.path] = { content: file.content, metadata: file.metadata }; return acc; }, {}) } }; } else if (p.type === "indexeddb") { source = { type: "indexeddb", config: { dbName: p.config?.dbName || `firesystem-${p.id}` } }; } else if (p.type === "s3") { source = { type: "s3", config: p.config }; } else { source = { type: "memory", config: {} }; } return { id: p.id, name: p.name, source }; }); return { version: data.version, projects, activeProjectId: data.workspace.activeProjectId, settings: data.workspace.settings }; } }; // src/credentials/SourceConfigBuilder.ts var SourceConfigBuilder = class { /** * Build IndexedDB source config */ static indexedDB(dbName) { return { type: "indexeddb", config: { dbName: dbName || `firesystem-${Date.now()}` } }; } /** * Build Memory source config */ static memory(initialData) { return { type: "memory", config: { initialData } }; } /** * Build S3 source config */ static s3(config) { const source = { type: "s3", config: { bucket: config.bucket, prefix: config.prefix || "", region: config.region || "us-east-1" } }; if (config.credentials) { source.auth = { type: "token", credentials: config.credentials }; } return source; } /** * Build GitHub source config */ static github(config) { const source = { type: "github", config: { owner: config.owner, repo: config.repo, branch: config.branch || "main", path: config.path || "/" } }; if (config.token) { source.auth = { type: "bearer", credentials: { token: config.token } }; } return source; } /** * Build API source config */ static api(config) { const source = { type: "api", config: { baseUrl: config.baseUrl, projectEndpoint: config.projectEndpoint || "/projects", headers: config.headers } }; if (config.apiKey) { source.auth = { type: "token", credentials: { apiKey: config.apiKey } }; } return source; } /** * Validate source configuration */ static validate(source) { const errors = []; switch (source.type) { case "indexeddb": if (!source.config.dbName) { errors.push("IndexedDB source requires dbName"); } break; case "s3": if (!source.config.bucket) { errors.push("S3 source requires bucket"); } break; case "github": if (!source.config.owner || !source.config.repo) { errors.push("GitHub source requires owner and repo"); } break; case "api": if (!source.config.baseUrl) { errors.push("API source requires baseUrl"); } break; case "memory": break; default: errors.push(`Unknown source type: ${source.type}`); } return { valid: errors.length === 0, errors }; } /** * Sanitize source for export (remove credentials) */ static sanitize(source) { const sanitized = { ...source }; delete sanitized.auth; switch (source.type) { case "s3": sanitized.config = { bucket: source.config.bucket, prefix: source.config.prefix }; break; case "api": const { headers, ...restConfig } = source.config; sanitized.config = restConfig; break; // For other types (memory, indexeddb, github), preserve config as-is default: break; } return sanitized; } }; // src/import-export/WorkspaceExporter.ts var WorkspaceExporter = class { /** * Export workspace to JSON */ static async toJson(projects, activeProjectId, settings, options = {}) { const exportedProjects = await Promise.all( projects.filter((p) => !options.projectIds || options.projectIds.includes(p.id)).map(async (p) => { const exported = { id: p.id, name: p.name, type: p.source.type }; if (p.source.type === "s3") { if (options.includeCredentials) { exported.config = { ...p.source.config }; } else { exported.config = { bucket: p.source.config.bucket, prefix: p.source.config.prefix }; } } else if (p.source.type === "indexeddb") { exported.config = { dbName: p.source.config.dbName }; } else { exported.config = options.includeCredentials ? { ...p.source.config } : SourceConfigBuilder.sanitize(p.source).config; } if (options.includeFiles && p.fs) { exported.files = await this.extractFiles(p.fs); } return exported; }) ); return { version: "1.0", exportedAt: /* @__PURE__ */ new Date(), workspace: { settings, activeProjectId: activeProjectId || void 0 }, projects: exportedProjects }; } /** * Export to GitHub Gist */ static async toGitHubGist(exportData, options) { const response = await fetch("https://api.github.com/gists", { method: "POST", headers: { "Authorization": `token ${options.token}`, "Accept": "application/vnd.github.v3+json", "Content-Type": "application/json" }, body: JSON.stringify({ description: options.description || "Firesystem Workspace Export", public: options.public ?? false, files: { "workspace.json": { content: JSON.stringify(exportData, null, 2) } } }) }); if (!response.ok) { throw new Error(`Failed to create gist: ${response.statusText}`); } const gist = await response.json(); return gist.id; } /** * Export to API endpoint */ static async toApi(exportData, url, options) { const response = await fetch(url, { method: options?.method || "POST", headers: { "Content-Type": "application/json", ...options?.headers }, body: JSON.stringify(exportData) }); if (!response.ok) { throw new Error(`Failed to export to API: ${response.statusText}`); } } /** * Extract all files from a file system */ static async extractFiles(fs) { const files = []; try { if (!fs.capabilities?.supportsGlob) { console.warn("File system does not support glob operations, skipping file extraction"); return files; } const paths = await fs.glob("**/*"); for (const path of paths) { try { const stat = await fs.stat(path); if (stat.type === "file") { const file = await fs.readFile(path); let content = file.content; if (typeof content !== "string") { content = JSON.stringify(content); } files.push({ path, content, metadata: file.metadata }); } } catch (error) { console.warn(`Failed to export file ${path}:`, error); } } } catch (error) { console.warn("Failed to glob files:", error); } return files; } }; // src/credentials/CredentialManager.ts var CredentialManager = class { constructor() { this.providers = /* @__PURE__ */ new Map(); this.memoryCache = /* @__PURE__ */ new Map(); } /** * Register a credential provider */ registerProvider(name, provider) { this.providers.set(name, provider); } /** * Get credentials for a project source */ async getCredentials(projectId, source) { const cacheKey = `${projectId}:${source.type}`; if (this.memoryCache.has(cacheKey)) { return this.memoryCache.get(cacheKey); } const provider = this.providers.get(source.type); if (provider) { const credentials = await provider.getCredentials(projectId, source); this.memoryCache.set(cacheKey, credentials); return credentials; } if (source.auth) { return this.extractCredentials(source.auth); } return {}; } /** * Extract credentials from auth config */ extractCredentials(auth) { switch (auth.type) { case "bearer": return { token: auth.credentials.token }; case "basic": return { username: auth.credentials.username, password: auth.credentials.password }; case "token": return { apiKey: auth.credentials.apiKey }; case "oauth2": return { accessToken: auth.credentials.accessToken, refreshToken: auth.credentials.refreshToken }; default: return auth.credentials; } } /** * Clear cached credentials */ clearCache(projectId) { if (projectId) { for (const [key] of this.memoryCache) { if (key.startsWith(`${projectId}:`)) { this.memoryCache.delete(key); } } } else { this.memoryCache.clear(); } } }; var BrowserCredentialProvider = class { constructor() { this.dbName = "@firesystem/credentials"; this.storeName = "credentials"; } async getCredentials(projectId, source) { return source.config; } async storeCredentials(projectId, credentials) { console.warn("Storing credentials in browser - consider security implications"); } async removeCredentials(projectId) { } }; var EnvCredentialProvider = class { async getCredentials(projectId, source) { const prefix = `FIRESYSTEM_${source.type.toUpperCase()}_`; switch (source.type) { case "s3": return { accessKeyId: process.env[`${prefix}ACCESS_KEY_ID`], secretAccessKey: process.env[`${prefix}SECRET_ACCESS_KEY`], region: process.env[`${prefix}REGION`] || "us-east-1", bucket: source.config.bucket // Bucket from config }; case "github": return { token: process.env[`${prefix}TOKEN`] || process.env.GITHUB_TOKEN }; case "api": return { apiKey: process.env[`${prefix}API_KEY`], baseUrl: source.config.baseUrl }; default: return source.config; } } async storeCredentials() { throw new Error("Cannot store credentials in environment variables"); } async removeCredentials() { throw new Error("Cannot remove credentials from environment variables"); } }; var InteractiveCredentialProvider = class { constructor(prompter) { this.prompter = prompter; } async getCredentials(projectId, source) { console.log(`Credentials needed for ${source.type} project: ${projectId}`); switch (source.type) { case "s3": return { accessKeyId: await this.prompter("AWS Access Key ID:"), secretAccessKey: await this.prompter("AWS Secret Access Key:", true), region: await this.prompter("AWS Region (default: us-east-1):") || "us-east-1", bucket: source.config.bucket }; case "github": return { token: await this.prompter("GitHub Personal Access Token:", true) }; case "api": return { apiKey: await this.prompter("API Key:", true), baseUrl: source.config.baseUrl }; default: return source.config; } } async storeCredentials() { } async removeCredentials() { } }; // src/WorkspaceFileSystem.ts var WorkspaceFileSystem = class { constructor(config) { this.projects = /* @__PURE__ */ new Map(); this.activeProjectId = null; this.providers = /* @__PURE__ */ new Map(); this.recentProjectIds = []; this.autoDisableTimers = /* @__PURE__ */ new Map(); this.settings = { maxActiveProjects: 10, autoDisableAfter: 30 * 60 * 1e3, // 30 minutes keepFocusedActive: true, autoSave: false, autoSaveInterval: 6e4, // 1 minute memoryThreshold: 500 * 1024 * 1024 // 500MB }; /** * Event system for workspace events */ this.events = new TypedEventEmitter(); this.database = new WorkspaceDatabase(); this.credentialManager = new CredentialManager(); this.setupCredentialProviders(); if (config?.settings) { this.settings = { ...this.settings, ...config.settings }; } } /** * Initialize workspace with optional config */ async initialize(config) { this.events.emit("workspace:initializing", void 0); await this.database.open(); const restored = await this.restoreWorkspaceState(); if (config) { for (const projectConfig of config.projects) { await this.loadProject(projectConfig); } if (config.activeProjectId) { await this.setActiveProject(config.activeProjectId); } } this.events.emit("workspace:initialized", { projectCount: this.projects.size }); } /** * Load a project into the workspace */ async loadProject(config) { this.events.emit("project:loading", { projectId: config.id }); try { if (this.projects.has(config.id)) { throw new Error(`Project ${config.id} already loaded`); } const provider = this.getProvider(config.source.type); if (!provider) { throw new Error(`No provider registered for type: ${config.source.type}`); } if (provider.validateConfiguration) { const validation = await provider.validateConfiguration(config.source.config); if (!validation.valid) { throw new Error(`Invalid source config: ${validation.errors?.join(", ") || "Validation failed"}`); } } const credentials = await this.credentialManager.getCredentials(config.id, config.source); const sourceWithCredentials = { ...config.source, config: { ...config.source.config, ...credentials } }; const fs = await provider.createFileSystem(sourceWithCredentials.config); const project = { id: config.id, name: config.name, source: config.source, fs, metadata: { created: /* @__PURE__ */ new Date(), modified: /* @__PURE__ */ new Date(), lastOpened: /* @__PURE__ */ new Date(), ...config.metadata }, state: "loaded", lastAccessed: /* @__PURE__ */ new Date(), accessCount: 0, memoryUsage: await this.estimateProjectMemoryUsage(fs) }; this.setupEventForwarding(project); this.projects.set(project.id, project); const storedProject = { id: project.id, name: project.name, source: project.source, metadata: project.metadata, lastAccessed: /* @__PURE__ */ new Date() }; await this.database.saveProject(storedProject); this.events.emit("project:loaded", { project }); if (!this.activeProjectId) { await this.setActiveProject(project.id); } return project; } catch (error) { this.events.emit("project:error", { projectId: config.id, error }); throw error; } } /** * Unload a project from the workspace */ async unloadProject(projectId) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } this.events.emit("project:unloading", { projectId }); if (this.activeProjectId === projectId) { this.activeProjectId = null; this.events.emit("project:deactivated", { projectId }); } if (project.fs.events) { } this.projects.delete(projectId); this.events.emit("project:unloaded", { projectId }); } /** * Get a project by ID */ getProject(projectId) { return this.projects.get(projectId) || null; } /** * Get all loaded projects */ getProjects() { return Array.from(this.projects.values()); } /** * Set the active project */ async setActiveProject(projectId) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } const previousId = this.activeProjectId; this.activeProjectId = projectId; this.recentProjectIds = [projectId, ...this.recentProjectIds.filter((id) => id !== projectId)].slice(0, 10); await this.database.touchProject(projectId); await this.saveWorkspaceState(); this.events.emit("project:activated", { projectId, previousId: previousId || void 0 }); } /** * Get the active project */ getActiveProject() { if (!this.activeProjectId) return null; return this.projects.get(this.activeProjectId) || null; } /** * Disable a project (unload but keep configuration) */ async disableProject(projectId) { const project = this.projects.get(projectId); if (!project) return; if (this.settings.keepFocusedActive && this.activeProjectId === projectId) { throw new Error("Cannot disable the focused project when keepFocusedActive is true"); } this.events.emit("project:disabling", { projectId }); const provider = this.getProvider(project.source.type); const hasLocalData = provider?.hasLocalData?.() || false; const wasActive = this.activeProjectId === projectId; await this.unloadProject(projectId); await this.database.updateProjectState(projectId, { enabled: false, disabledAt: /* @__PURE__ */ new Date() }); if (wasActive) { const remainingProjects = Array.from(this.projects.keys()); if (remainingProjects.length > 0) { await this.setActiveProject(remainingProjects[0]); } } this.events.emit("project:disabled", { projectId, hasLocalData, reason: "manual" }); } /** * Enable a disabled project */ async enableProject(projectId) { if (this.projects.has(projectId)) { return; } const storedProject = await this.database.getProject(projectId); if (!storedProject) { throw new Error(`Project ${projectId} not found in database`); } const provider = this.getProvider(storedProject.source.type); if (!provider) { throw new Error(`Provider ${storedProject.source.type} not registered`); } const fs = await provider.createFileSystem(storedProject.source.config); const project = { id: storedProject.id, name: storedProject.name, source: storedProject.source, fs, metadata: storedProject.metadata || {}, state: "loaded", lastAccessed: /* @__PURE__ */ new Date(), accessCount: 0, memoryUsage: 0 }; this.projects.set(projectId, project); await this.database.updateProjectState(projectId, { enabled: true, enabledAt: /* @__PURE__ */ new Date() }); this.events.emit("project:enabled", { projectId }); } /** * Batch disable projects */ async disableProjects(projectIds) { for (const projectId of projectIds) { await this.disableProject(projectId); } } /** * Batch enable projects */ async enableProjects(projectIds) { for (const projectId of projectIds) { await this.enableProject(projectId); } } /** * Get disabled projects */ async getDisabledProjects() { const allProjects = await this.database.listProjects(); return allProjects.filter((p) => p.enabled === false); } /** * Check if project is enabled */ isProjectEnabled(projectId) { return this.projects.has(projectId); } /** * Copy files between projects */ async copyFiles(sourceId, pattern, targetId, targetPath) { const sourceProject = this.projects.get(sourceId); const targetProject = this.projects.get(targetId); if (!sourceProject) throw new Error(`Source project ${sourceId} not found`); if (!targetProject) throw new Error(`Target project ${targetId} not found`); const files = await sourceProject.fs.glob(pattern); let copied = 0; for (const file of files) { const stat = await sourceProject.fs.stat(file); if (stat.type === "file") { const content = await sourceProject.fs.readFile(file); const targetFilePath = (targetPath + file).replace(/\/+/g, "/"); await targetProject.fs.writeFile(targetFilePath, content.content, content.metadata); copied++; } } this.events.emit("project:files-copied", { sourceId, targetId, fileCount: copied }); } /** * Sync projects */ async syncProjects(sourceId, targetId, options = {}) { const sourceProject = this.projects.get(sourceId); const targetProject = this.projects.get(targetId); if (!sourceProject) throw new Error(`Source project ${sourceId} not found`); if (!targetProject) throw new Error(`Target project ${targetId} not found`); const files = await sourceProject.fs.glob("**/*"); const total = files.length; let copied = 0; for (const file of files) { if (options.filter && !options.filter(file)) continue; const stat = await sourceProject.fs.stat(file); if (stat.type === "file") { const exists = await targetProject.fs.exists(file); if (!exists || options.overwrite) { const content = await sourceProject.fs.readFile(file); await targetProject.fs.writeFile(file, content.content, content.metadata); copied++; if (options.progress) { options.progress(copied, total); } } } } } /** * Compare projects */ async compareProjects(projectId1, projectId2) { const project1 = this.projects.get(projectId1); const project2 = this.projects.get(projectId2); if (!project1) throw new Error(`Project ${projectId1} not found`); if (!project2) throw new Error(`Project ${projectId2} not found`); const files1 = new Set(await project1.fs.glob("**/*")); const files2 = new Set(await project2.fs.glob("**/*")); const diff = { added: [], modified: [], deleted: [], unchanged: [] }; for (const file of files2) { if (!files1.has(file)) { diff.added.push(file); } } for (const file of files1) { if (!files2.has(file)) { diff.deleted.push(file); } } for (const file of files1) { if (files2.has(file)) { const stat1 = await project1.fs.stat(file); const stat2 = await project2.fs.stat(file); if (stat1.type === "file" && stat2.type === "file") { const content1 = await project1.fs.readFile(file); const content2 = await project2.fs.readFile(file); if (content1.content !== content2.content) { diff.modified.push(file); } else { diff.unchanged.push(file); } } } } return diff; } /** * Get workspace statistics */ async getProjectStats() { const allProjects = await this.database.listProjects(); const connections = {}; for (const project of this.projects.values()) { const type = project.source.type; connections[type] = (connections[type] || 0) + 1; } let totalMemory = 0; for (const project of this.projects.values()) { totalMemory += project.memoryUsage || 0; } const disabledCount = allProjects.filter((p) => p.enabled === false).length; return { total: allProjects.length, active: this.projects.size, disabled: disabledCount, focused: this.activeProjectId, memoryUsage: this.formatBytes(totalMemory), connections }; } /** * Get project metrics */ async getProjectMetrics(projectId) { const project = this.projects.get(projectId); if (!project) throw new Error(`Project ${projectId} not found`); const files = await project.fs.glob("**/*"); let totalSize = 0; let fileCount = 0; let largestFile = { path: "", size: 0 }; let lastModified = /* @__PURE__ */ new Date(0); for (const file of files) { const stat = await project.fs.stat(file); if (stat.type === "file") { fileCount++; totalSize += stat.size; if (stat.size > largestFile.size) { largestFile = { path: file, size: stat.size }; } if (stat.modified > lastModified) { lastModified = stat.modified; } } } return { fileCount, totalSize, lastModified, accessCount: project.accessCount, averageFileSize: fileCount > 0 ? Math.round(totalSize / fileCount) : 0, largestFile }; } /** * Optimize memory usage */ async optimizeMemoryUsage() { const report = { projectsDisabled: [], memoryFreed: 0, connectionsReleased: 0 }; const stats = await this.getProjectStats(); const currentMemory = await this.calculateTotalMemoryUsage(); if (currentMemory < (this.settings.memoryThreshold || Infinity)) { return report; } const projectList = Array.from(this.projects.values()).filter((p) => p.id !== this.activeProjectId).sort((a, b) => a.lastAccessed.getTime() - b.lastAccessed.getTime()); for (const project of projectList) { if (currentMemory - report.memoryFreed < (this.settings.memoryThreshold || Infinity)) { break; } const memoryBefore = project.memoryUsage || 0; await this.disableProject(project.id); report.projectsDisabled.push(project.id); report.memoryFreed += memoryBefore; report.connectionsReleased++; } return report; } /** * Get active project's file system (for proxying) */ getActiveFS() { const project = this.getActiveProject(); if (!project) { throw new Error("No active project"); } project.lastAccessed = /* @__PURE__ */ new Date(); project.accessCount++; this.resetAutoDisableTimer(project.id); return project.fs; } /** * Reset auto-disable timer for a project */ resetAutoDisableTimer(projectId) { const existingTimer = this.autoDisableTimers.get(projectId); if (existingTimer) { clearTimeout(existingTimer); } if (this.settings.keepFocusedActive && this.activeProjectId === projectId) { return; } if (this.settings.autoDisableAfter && this.settings.autoDisableAfter > 0) { const timer = setTimeout(() => { this.disableProject(projectId).catch((error) => { console.error(`Failed to auto-disable project ${projectId}:`, error); }); }, this.settings.autoDisableAfter); this.autoDisableTimers.set(projectId, timer); } } /** * Estimate project memory usage */ async estimateProjectMemoryUsage(fs) { try { const size = await fs.size(); return size * 1.2; } catch { return 0; } } /** * Calculate total memory usage */ async calculateTotalMemoryUsage() { let total = 0; for (const project of this.projects.values()) { total += project.memoryUsage || 0; } return total; } /** * Format bytes to human readable */ formatBytes(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } // Methods registerSourceLoader and getSourceLoader removed // Now using Provider Registry methods above /** * Setup credential providers */ setupCredentialProviders() { const isNode = typeof window === "undefined"; if (isNode) { this.credentialManager.registerProvider("s3", new EnvCredentialProvider()); this.credentialManager.registerProvider("github", new EnvCredentialProvider()); this.credentialManager.registerProvider("api", new EnvCredentialProvider()); } else { const browserProvider = new BrowserCredentialProvider(); this.credentialManager.registerProvider("s3", browserProvider); this.credentialManager.registerProvider("github", browserProvider); this.credentialManager.registerProvider("api", browserProvider); } } /** * Provider Registry Implementation */ registerProvider(provider) { this.providers.set(provider.scheme, provider); } unregisterProvider(scheme) { this.providers.delete(scheme); } getProvider(scheme) { return this.providers.get(scheme); } getRegisteredProviders() { return Array.from(this.providers.values()); } /** * Setup event forwarding from project to workspace */ setupEventForwarding(project) { if (!project.fs.events) return; const events = [ FileSystemEvents.FILE_READING, FileSystemEvents.FILE_READ, FileSystemEvents.FILE_WRITING, FileSystemEvents.FILE_WRITTEN, FileSystemEvents.FILE_DELETING, FileSystemEvents.FILE_DELETED, FileSystemEvents.DIR_CREATING, FileSystemEvents.DIR_CREATED, FileSystemEvents.DIR_DELETING, FileSystemEvents.DIR_DELETED ]; events.forEach((event) => { project.fs.events.on(event, (payload) => { const projectEvent = `project:${event.toLowerCase().replace(/_/g, ":")}`; const enrichedPayload = { projectId: project.id, ...payload }; this.events.emit(projectEvent, enrichedPayload); }); }); } /** * Convert a project to a different source */ async convertProject(projectId, targetSource) { const project = this.getProject(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } this.events.emit("project:converting", { projectId, targetSource }); const provider = this.getProvider(targetSource.type); if (!provider) { throw new Error(`No provider registered for type: ${targetSource.type}`); } const newFs = await provider.createFileSystem(targetSource.config); const paths = await project.fs.glob("**/*"); for (const path of paths) { const stat = await project.fs.stat(path); if (stat.type === "directory") { if (path !== "/") { await newFs.mkdir(path, true); } } else { const file = await project.fs.readFile(path); await newFs.writeFile(path, file.content, file.metadata); } } project.fs = newFs; project.source = targetSource; this.setupEventForwarding(project); this.events.emit("project:converted", { projectId, source: targetSource }); } /** * Convert to IndexedDB (convenience method) */ async convertToIndexedDB(projectId) { await this.convertProject(projectId, { type: "indexeddb", config: { dbName: `firesystem-${projectId}` } }); } /** * Export workspace configuration */ async export() { const projects = Array.from(this.projects.values()).map( (p) => ({ id: p.id, name: p.name, source: p.source, metadata: p.metadata }) ); return { version: "1.0.0", projects, activeProjectId: this.activeProjectId || void 0, settings: this.settings }; } /** * Import workspace configuration */ async import(config) { await this.clear(); if (config.settings) { this.settings = { ...this.settings, ...config.settings }; } for (const projectConfig of config.projects) { await this.loadProject(projectConfig); } if (config.activeProjectId) { this.setActiveProject(config.activeProjectId); } } /** * Clear all projects */ async clear() { this.events.emit("workspace:clearing", void 0); const projectIds = Array.from(this.projects.keys()); for (const projectId of projectIds) { await this.unloadProject(projectId); } const allStoredProjects = await this.database.listProjects(); for (const project of allStoredProjects) { await this.database.deleteProject(project.id); } this.activeProjectId = null; this.recentProjectIds = []; await this.saveWorkspaceState(); this.events.emit("workspace:cleared", void 0); } // IReactiveFileSystem implementation (proxy to active project) async readFile(path) { return this.getActiveFS().readFile(path); } async writeFile(path, content, metadata) { return this.getActiveFS().writeFile(path, content, metadata); } async deleteFile(path) { return this.getActiveFS().deleteFile(path); } async mkdir(path, recursive) { return this.getActiveFS().mkdir(path, recursive); } async rmdir(path, recursive) { return this.getActiveFS().rmdir(path, recursive); } async exists(path) { return this.getActiveFS().exists(path); } async stat(path) { return this.getActiveFS().stat(path); } async readDir(path) { return this.getActiveFS().readDir(path); } async rename(oldPath, newPath) { return this.getActiveFS().rename(oldPath, newPath); } async copy(sourcePath, targetPath) { return this.getActiveFS().copy(sourcePath, targetPath); } async move(sourcePaths, targetPath) { return this.getActiveFS().move(sourcePaths, targetPath); } async glob(pattern) { return this.getActiveFS().glob(pattern); } watch(path, callback) { return this.getActiveFS().watch(path, callback); } watchGlob(pattern, callback) { const fs = this.getActiveFS(); if ("watchGlob" in fs && typeof fs.watchGlob === "function") { return fs.watchGlob(pattern, callback); } throw new Error("Active file system does not support watchGlob"); } async size() { return this.getActiveFS().size(); } // Delegate permission methods to active FS async canModify(path) { return this.getActiveFS().canModify(path); } async canCreateIn(parentPath) { return this.getActiveFS().canCreateIn(parentPath); } /** * Check if a project exists and can be accessed */ async isProjectAccessible(projectId) { try { const storedProject = await this.database.getProject(projectId); if (!storedProject) return false; if (storedProject.source.type === "indexeddb") { const dbName = storedProject.source.config.dbName; if (dbName) { try { if (typeof indexedDB === "undefined" || !indexedDB.databases) { return true; } const databases = await indexedDB.databases(); const projectDbExists = databases.some((db) => db.name === dbName); return projectDbExists; } catch (error) { console.warn(`Failed to check database existence for project ${projectId}:`, error); return true; } } } return true; } catch (error) { console.warn(`Failed to check project accessibility for ${projectId}:`, error); return false; } } // Persistence methods /** * Restore workspace state from database */ async restoreWorkspaceState() { try { const state = await this.database.getWorkspaceState(); if (!state) return false; if (state.settings) { this.settings = { ...state.settings, ...this.settings }; } let stateChanged = false; if (state.activeProjectId) { const activeProjectAccessible = await this.isProjectAccessible(state.activeProjectId); if (!activeProjectAccessible) { console.warn(`Removing orphaned activeProjectId: ${state.activeProjectId}`); state.activeProjectId = null; stateChanged = true; } } if (state.recentProjectIds && state.recentProjectIds.length > 0) { const validRecentIds = []; for (const projectId of state.recentProjectIds) { const projectAccessible = await this.isProjectAccessible(projectId); if (projectAccessible) { validRecentIds.push(projectId); } else { console.warn(`Removing orphaned recentProjectId: ${projectId}`); stateChanged = true; } } state.recentProjectIds = validRecentIds; } if (stateChanged) { await this.database.saveWorkspaceState(state); console.info("Workspace state cleaned: removed orphaned project references"); } this.recentProjectIds = state.recentProjectIds || []; const storedProjects = await this.database.listProjects(); if (state.activeProjectId) { const activeProject = storedProjects.find((p) => p.id === state.activeProjectId); if (activeProject) { try { await this.loadProjectFromStored(activeProject); this.activeProjectId = state.activeProjectId; } catch (error) { console.warn("Failed to restore active project:", error); this.activeProjectId = null; state.activeProjectId = null; await this.database.saveWorkspaceState(state); } } } return true; } catch (error) { console.error("Failed to restore workspace state:", error); return false; } } /** * Save current workspace state to database */ async saveWorkspaceState() { const state = { id: "current", activeProjectId: this.activeProjectId, recentProjectIds: this.recentProjectIds, settings: this.settings }; await this.database.saveWorkspaceState(state); } /** * Load a project from stored configuration */ async loadProjectFromStored(stored) { const config = { id: stored.id, name: stored.name, source: stored.source, metadata: stored.metadata }; return this.loadProject(config); } /** * List all registered projects from database */ async listStoredProjects() { return this.database.listProjects(); } /** * List recent projects */ async listRecentProjects(limit = 10) { return this.database.listRecentProjects(limit); } /** * Delete a project from workspace and optionally delete its data */ async deleteProject(projectId, options = {}) { const project = this.projects.get(projectId); if (!project && !await this.database.getProject(projectId)) { return; } if (!options.skipConfirmation) { const confirmEvent = { projectId, project: project || await this.database.getProject(projectId), cancelled: false }; this.events.emit("project:delete-confirm", confirmEvent); if (confirmEvent.cancelled) { return; } } if (options.deleteData) { const projectData = project || await this.database.getProject(projectId); if (projectData) { const provider = this.getProvider(projectData.source.type); if (provider?.deleteProjectData) { try { await provider.deleteProjectData(projectData.source.config); } catch (error) { console.warn(`Failed to delete project data: ${error instanceof Error ? error.message : String(error)}`); } } } } if (this.projects.has(projectId)) { await this.unloadProject(projectId); } await this.database.deleteProject(projectId); if (this.activeProjectId === projectId) { this.activeProjectId = null; await this.saveWorkspaceState(); } this.events.emit("project:deleted", { projectId, deletedData: options.deleteData || false }); } /** * Discover existing IndexedDB projects */ async discoverIndexedDBProjects() { const dbNames = await this.database.discoverIndexedDBProjects(); return dbNames.map((dbName) => ({ id: `discovered-${dbName}`, name: dbName.replace(/^(firesystem-|@firesystem\/)/i, ""), source: { type: "indexeddb", config: { dbName } } })); } /** * Import workspace configuration from URL */ async importFromUrl(url) { this.events.emit("workspace:importing", { source: url }); try { const config = await WorkspaceImporter.fromJsonUrl(url); await this.importWorkspaceConfig(config); this.events.emit("workspace:imported", { source: url }); } catch (error) { this.events.emit("workspace:import-failed", { source: url, error }); throw error; } } /**