@firesystem/workspace
Version:
Multi-project workspace manager for Firesystem with support for multiple sources
189 lines (170 loc) • 5.09 kB
text/typescript
import type { IReactiveFileSystem } from "@firesystem/core";
import type { Project, WorkspaceSettings } from "../types";
import type { WorkspaceExport } from "./WorkspaceImporter";
import { SourceConfigBuilder } from "../credentials/SourceConfigBuilder";
export interface ExportOptions {
includeFiles?: boolean;
includeCredentials?: boolean;
projectIds?: string[];
}
/**
* Handles exporting workspace configurations
*/
export class WorkspaceExporter {
/**
* Export workspace to JSON
*/
static async toJson(
projects: Project[],
activeProjectId: string | null,
settings: WorkspaceSettings,
options: ExportOptions = {}
): Promise<WorkspaceExport> {
const exportedProjects = await Promise.all(
projects
.filter(p => !options.projectIds || options.projectIds.includes(p.id))
.map(async p => {
const exported: any = {
id: p.id,
name: p.name,
type: p.source.type as any
};
// Include config based on credentials option
if (p.source.type === "s3") {
if (options.includeCredentials) {
// Include full config with credentials
exported.config = { ...p.source.config };
} else {
// Strip credentials from S3 config
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 {
// For other types, include full config unless it's sensitive
exported.config = options.includeCredentials ?
{ ...p.source.config } :
SourceConfigBuilder.sanitize(p.source).config;
}
// Include files if requested
if (options.includeFiles && p.fs) {
exported.files = await this.extractFiles(p.fs);
}
return exported;
})
);
return {
version: "1.0",
exportedAt: new Date(),
workspace: {
settings,
activeProjectId: activeProjectId || undefined
},
projects: exportedProjects
};
}
/**
* Export to GitHub Gist
*/
static async toGitHubGist(
exportData: WorkspaceExport,
options: {
token: string;
description?: string;
public?: boolean;
}
): Promise<string> {
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: WorkspaceExport,
url: string,
options?: {
headers?: Record<string, string>;
method?: string;
}
): Promise<void> {
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
*/
private static async extractFiles(fs: IReactiveFileSystem): Promise<Array<{
path: string;
content: string;
metadata?: Record<string, any>;
}>> {
const files: any[] = [];
try {
// Check if file system supports glob operations
if (!fs.capabilities?.supportsGlob) {
console.warn("File system does not support glob operations, skipping file extraction");
return files;
}
// Get all file paths
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);
// Convert content to string if needed
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;
}
}