UNPKG

@workspace-fs/core

Version:

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

1,717 lines (1,703 loc) 64.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { BrowserCredentialProvider: () => BrowserCredentialProvider, CredentialManager: () => CredentialManager, EnvCredentialProvider: () => EnvCredentialProvider, FileSystemEvents: () => import_core4.FileSystemEvents, InteractiveCredentialProvider: () => InteractiveCredentialProvider, SourceConfigBuilder: () => SourceConfigBuilder, WorkspaceDatabase: () => WorkspaceDatabase, WorkspaceExporter: () => WorkspaceExporter, WorkspaceFileSystem: () => WorkspaceFileSystem, WorkspaceImporter: () => WorkspaceImporter }); module.exports = __toCommonJS(index_exports); // src/WorkspaceFileSystem.ts var import_core3 = require("@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/managers/ProjectManager.ts var import_core = require("@firesystem/core"); var ProjectManager = class { constructor(events, getProvider, credentialManager, saveProjectToDb, touchProjectInDb, estimateProjectMemoryUsage) { this.events = events; this.getProvider = getProvider; this.credentialManager = credentialManager; this.saveProjectToDb = saveProjectToDb; this.touchProjectInDb = touchProjectInDb; this.estimateProjectMemoryUsage = estimateProjectMemoryUsage; this.projects = /* @__PURE__ */ new Map(); this.autoDisableTimers = /* @__PURE__ */ new Map(); } /** * 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.saveProjectToDb(storedProject); this.events.emit("project:loaded", { project }); 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 (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()); } /** * Check if project exists */ hasProject(projectId) { return this.projects.has(projectId); } /** * Update recent projects */ async updateRecentProjects(projectId, recentProjectIds) { const updatedRecent = [ projectId, ...recentProjectIds.filter((id) => id !== projectId) ].slice(0, 10); await this.touchProjectInDb(projectId); return updatedRecent; } /** * Track project access */ trackProjectAccess(project) { project.lastAccessed = /* @__PURE__ */ new Date(); project.accessCount++; } /** * Reset auto-disable timer for a project */ resetAutoDisableTimer(projectId, autoDisableAfter, keepFocusedActive, activeProjectId, onAutoDisable) { const existingTimer = this.autoDisableTimers.get(projectId); if (existingTimer) { clearTimeout(existingTimer); } if (keepFocusedActive && activeProjectId === projectId) { return; } if (autoDisableAfter && autoDisableAfter > 0) { const timer = setTimeout(() => { onAutoDisable(projectId).catch((error) => { console.error(`Failed to auto-disable project ${projectId}:`, error); }); }, autoDisableAfter); this.autoDisableTimers.set(projectId, timer); } } /** * Setup event forwarding from project to workspace */ setupEventForwarding(project) { if (!project.fs.events) return; const events = [ import_core.FileSystemEvents.FILE_READING, import_core.FileSystemEvents.FILE_READ, import_core.FileSystemEvents.FILE_WRITING, import_core.FileSystemEvents.FILE_WRITTEN, import_core.FileSystemEvents.FILE_DELETING, import_core.FileSystemEvents.FILE_DELETED, import_core.FileSystemEvents.DIR_CREATING, import_core.FileSystemEvents.DIR_CREATED, import_core.FileSystemEvents.DIR_DELETING, import_core.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 }); } /** * 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); } /** * Recreate filesystem via provider */ async recreateFileSystem(stored) { const provider = this.getProvider(stored.source.type); if (!provider) { throw new Error(`Provider ${stored.source.type} not registered`); } return provider.createFileSystem(stored.source.config); } /** * Create project from stored with filesystem */ createProjectFromStored(stored, fs) { const project = { id: stored.id, name: stored.name, source: stored.source, fs, metadata: stored.metadata || {}, state: "loaded", lastAccessed: /* @__PURE__ */ new Date(), accessCount: 0, memoryUsage: 0 }; this.projects.set(project.id, project); return project; } }; // src/managers/PerformanceManager.ts var PerformanceManager = class { constructor(settings, getProjects, getActiveProjectId, disableProject) { this.settings = settings; this.getProjects = getProjects; this.getActiveProjectId = getActiveProjectId; this.disableProject = disableProject; } /** * Get workspace statistics */ async getProjectStats(totalProjectsCount, disabledCount) { const connections = {}; for (const project of this.getProjects()) { const type = project.source.type; connections[type] = (connections[type] || 0) + 1; } let totalMemory = 0; for (const project of this.getProjects()) { totalMemory += project.memoryUsage || 0; } return { total: totalProjectsCount, active: this.getProjects().length, disabled: disabledCount, focused: this.getActiveProjectId(), memoryUsage: this.formatBytes(totalMemory), connections }; } /** * Get project metrics */ async getProjectMetrics(project) { 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 currentMemory = await this.calculateTotalMemoryUsage(); if (currentMemory < (this.settings.memoryThreshold || Infinity)) { return report; } const projectList = Array.from(this.getProjects()).filter((p) => p.id !== this.getActiveProjectId()).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; } /** * 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.getProjects()) { 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]}`; } /** * Update settings */ updateSettings(settings) { Object.assign(this.settings, settings); } }; // src/managers/PersistenceManager.ts var PersistenceManager = class { constructor(database, checkProjectAccessible) { this.database = database; this.checkProjectAccessible = checkProjectAccessible; } /** * Restore workspace state from database */ async restoreWorkspaceState() { try { const state = await this.database.getWorkspaceState(); if (!state) return { state: null, activeProjectToLoad: null }; let stateChanged = false; if (state.activeProjectId) { const activeProjectAccessible = await this.checkProjectAccessible( 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.checkProjectAccessible(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" ); } let activeProjectToLoad = null; if (state.activeProjectId) { const storedProjects = await this.database.listProjects(); activeProjectToLoad = storedProjects.find( (p) => p.id === state.activeProjectId ) || null; } return { state, activeProjectToLoad }; } catch (error) { console.error("Failed to restore workspace state:", error); return { state: null, activeProjectToLoad: null }; } } /** * Save current workspace state to database */ async saveWorkspaceState(activeProjectId, recentProjectIds, settings) { const state = { id: "current", activeProjectId, recentProjectIds, settings }; await this.database.saveWorkspaceState(state); } /** * Save project to database */ async saveProject(project) { await this.database.saveProject(project); } /** * Touch project in database */ async touchProject(projectId) { await this.database.touchProject(projectId); } /** * Get project from database */ async getProject(projectId) { return this.database.getProject(projectId); } /** * Delete project from database */ async deleteProject(projectId) { await this.database.deleteProject(projectId); } /** * Update project state in database */ async updateProjectState(projectId, state) { await this.database.updateProjectState(projectId, state); } /** * List all projects from database */ async listProjects() { return this.database.listProjects(); } /** * List recent projects */ async listRecentProjects(limit = 10) { return this.database.listRecentProjects(limit); } /** * 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 } } })); } /** * Open database */ async open() { await this.database.open(); } /** * Close database */ close() { this.database.close(); } }; // src/managers/EventManager.ts var import_core2 = require("@firesystem/core"); var EventManager = class { constructor(events) { this.events = events; } /** * Setup event forwarding from project to workspace */ setupEventForwarding(project) { if (!project.fs.events) return; const events = [ import_core2.FileSystemEvents.FILE_READING, import_core2.FileSystemEvents.FILE_READ, import_core2.FileSystemEvents.FILE_WRITING, import_core2.FileSystemEvents.FILE_WRITTEN, import_core2.FileSystemEvents.FILE_DELETING, import_core2.FileSystemEvents.FILE_DELETED, import_core2.FileSystemEvents.DIR_CREATING, import_core2.FileSystemEvents.DIR_CREATED, import_core2.FileSystemEvents.DIR_DELETING, import_core2.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); }); }); } /** * Emit workspace event */ emit(event, payload) { this.events.emit(event, payload); } }; // src/operations/ProjectOperations.ts var ProjectOperations = class { constructor(getProject, events) { this.getProject = getProject; this.events = events; } /** * Copy files between projects */ async copyFiles(sourceId, pattern, targetId, targetPath) { const sourceProject = this.getProject(sourceId); const targetProject = this.getProject(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.getProject(sourceId); const targetProject = this.getProject(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.getProject(projectId1); const project2 = this.getProject(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; } }; // src/operations/FileSystemProxy.ts var FileSystemProxy = class { constructor(getActiveProject, trackProjectAccess, resetAutoDisableTimer) { this.getActiveProject = getActiveProject; this.trackProjectAccess = trackProjectAccess; this.resetAutoDisableTimer = resetAutoDisableTimer; } /** * Get active project's file system (for proxying) */ getActiveFS() { const project = this.getActiveProject(); if (!project) { throw new Error("No active project"); } this.trackProjectAccess(project); this.resetAutoDisableTimer(project.id); return project.fs; } // 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); } }; // src/WorkspaceFileSystem.ts var WorkspaceFileSystem = class { constructor(config) { this.activeProjectId = null; this.providers = /* @__PURE__ */ new Map(); this.recentProjectIds = []; 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 import_core3.TypedEventEmitter(); this.database = new WorkspaceDatabase(); this.credentialManager = new CredentialManager(); if (config?.settings) { this.settings = { ...this.settings, ...config.settings }; } this.projectManager = new ProjectManager( this.events, (scheme) => this.getProvider(scheme), this.credentialManager, (project) => this.persistenceManager.saveProject(project), (projectId) => this.persistenceManager.touchProject(projectId), (fs) => this.performanceManager.estimateProjectMemoryUsage(fs) ); this.performanceManager = new PerformanceManager( this.settings, () => this.projectManager.getProjects(), () => this.activeProjectId, (projectId) => this.disableProject(projectId) ); this.persistenceManager = new PersistenceManager( this.database, (projectId) => this.isProjectAccessible(projectId) ); this.eventManager = new EventManager(this.events); this.projectOperations = new ProjectOperations( (projectId) => this.projectManager.getProject(projectId), this.events ); this.fileSystemProxy = new FileSystemProxy( () => this.getActiveProject(), (project) => this.projectManager.trackProjectAccess(project), (projectId) => this.projectManager.resetAutoDisableTimer( projectId, this.settings.autoDisableAfter, this.settings.keepFocusedActive || false, this.activeProjectId, (id) => this.disableProject(id) ) ); } /** * Initialize workspace with optional config */ async initialize(config) { this.events.emit("workspace:initializing", void 0); await this.persistenceManager.open(); const { state, activeProjectToLoad } = await this.persistenceManager.restoreWorkspaceState(); if (state) { if (state.settings) { this.settings = { ...state.settings, ...this.settings }; this.performanceManager.updateSettings(this.settings); } this.recentProjectIds = state.recentProjectIds || []; if (activeProjectToLoad) { try { await this.projectManager.loadProjectFromStored(activeProjectToLoad); this.activeProjectId = state.activeProjectId; } catch (error) { console.warn("Failed to restore active project:", error); this.activeProjectId = null; await this.saveWorkspaceState(); } } } 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.projectManager.getProjects().length }); } /** * Load a project into the workspace */ async loadProject(config) { const project = await this.projectManager.loadProject(config); if (!this.activeProjectId) { await this.setActiveProject(project.id); } return project; } /** * Unload a project from the workspace */ async unloadProject(projectId) { if (this.activeProjectId === projectId) { this.activeProjectId = null; this.events.emit("project:deactivated", { projectId }); } await this.projectManager.unloadProject(projectId); } /** * Get a project by ID */ getProject(projectId) { return this.projectManager.getProject(projectId); } /** * Get all loaded projects */ getProjects() { return this.projectManager.getProjects(); } /** * Set the active project */ async setActiveProject(projectId) { const project = this.projectManager.getProject(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } const previousId = this.activeProjectId; this.activeProjectId = projectId; this.recentProjectIds = await this.projectManager.updateRecentProjects( projectId, this.recentProjectIds ); await this.saveWorkspaceState(); this.events.emit("project:activated", { projectId, previousId: previousId || void 0 }); } /** * Get the active project */ getActiveProject() { if (!this.activeProjectId) return null; retu