@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
230 lines (195 loc) • 6.66 kB
text/typescript
import path from 'path';
import crypto from 'crypto';
import { Workspace, Repository, WorkspaceStore, CrossRepoTask } from './types.js';
import { ConfigManager } from '../../config/config-manager.js';
import { FileSystemAdapter, NodeFileSystemAdapter } from './file-system-adapter.js';
export class WorkspaceDataStore {
private store: WorkspaceStore = { workspaces: {} };
private configManager: ConfigManager;
private readonly MODULE_NAME = 'workspace';
private readonly DATA_FILE = 'workspace.json';
private fs: FileSystemAdapter;
constructor(configManager?: ConfigManager, fs?: FileSystemAdapter) {
this.configManager = configManager || new ConfigManager();
this.fs = fs || new NodeFileSystemAdapter();
}
async init(): Promise<void> {
const storageManager = this.configManager.getStorageManager();
await storageManager.ensureStorageDirectories();
const data = await storageManager.loadData(this.MODULE_NAME, this.DATA_FILE);
if (data) {
this.store = data;
} else {
await this.save();
}
}
async save(): Promise<void> {
const storageManager = this.configManager.getStorageManager();
await storageManager.saveData(this.MODULE_NAME, this.DATA_FILE, this.store);
}
createWorkspace(name: string, rootPath: string, description?: string): Workspace {
const workspace: Workspace = {
id: this.generateId('ws'),
name,
description,
rootPath,
repositories: [],
createdAt: new Date(),
updatedAt: new Date(),
settings: {
autoSync: true,
syncInterval: 3600000, // 1 hour
crossRepoTags: true,
},
};
this.store.workspaces[workspace.id] = workspace;
if (!this.store.activeWorkspaceId) {
this.store.activeWorkspaceId = workspace.id;
}
return workspace;
}
getWorkspace(nameOrId: string): Workspace | undefined {
if (this.store.workspaces[nameOrId]) {
return this.store.workspaces[nameOrId];
}
return Object.values(this.store.workspaces).find(ws => ws.name === nameOrId);
}
getActiveWorkspace(): Workspace | undefined {
if (!this.store.activeWorkspaceId) {
return undefined;
}
return this.store.workspaces[this.store.activeWorkspaceId];
}
setActiveWorkspace(workspaceNameOrId: string): boolean {
// Try by ID first
if (this.store.workspaces[workspaceNameOrId]) {
this.store.activeWorkspaceId = workspaceNameOrId;
return true;
}
// Try by name
const workspace = this.getWorkspace(workspaceNameOrId);
if (workspace) {
this.store.activeWorkspaceId = workspace.id;
return true;
}
return false;
}
addRepository(
workspaceNameOrId: string,
repo: Omit<Repository, 'id' | 'lastSync'>
): Repository | null {
const workspace = this.getWorkspace(workspaceNameOrId);
if (!workspace) {
return null;
}
const newRepo: Repository = {
...repo,
id: this.generateId('repo'),
lastSync: new Date(),
};
workspace.repositories.push(newRepo);
workspace.updatedAt = new Date();
return newRepo;
}
async detectRepositories(rootPath: string): Promise<Repository[]> {
const repositories: Repository[] = [];
try {
const entries = await this.fs.readdir(rootPath);
for (const entry of entries) {
const isDir = typeof entry.isDirectory === 'function' ? entry.isDirectory() : entry.isDirectory;
if (isDir && !entry.name.startsWith('.')) {
const repoPath = path.join(rootPath, entry.name);
// Check if it's a git repository
try {
await this.fs.access(path.join(repoPath, '.git'));
repositories.push({
id: this.generateId('repo'),
name: entry.name,
path: repoPath,
type: 'git',
lastSync: new Date(),
});
} catch {
// Check if it has package.json (might be a workspace package)
try {
await this.fs.access(path.join(repoPath, 'package.json'));
repositories.push({
id: this.generateId('repo'),
name: entry.name,
path: repoPath,
type: 'local',
lastSync: new Date(),
});
} catch {
// Not a repository
}
}
}
}
} catch (error) {
console.error('Error detecting repositories:', error);
}
return repositories;
}
getRepositoriesForWorkspace(workspaceNameOrId: string): Repository[] {
const workspace = this.getWorkspace(workspaceNameOrId);
return workspace?.repositories || [];
}
updateRepository(
workspaceNameOrId: string,
repoId: string,
updates: Partial<Omit<Repository, 'id'>>
): boolean {
const workspace = this.getWorkspace(workspaceNameOrId);
if (!workspace) {
return false;
}
const repo = workspace.repositories.find(r => r.id === repoId);
if (!repo) {
return false;
}
Object.assign(repo, updates);
repo.lastSync = new Date();
workspace.updatedAt = new Date();
return true;
}
getWorkspaces(): Workspace[] {
return Object.values(this.store.workspaces);
}
findRepository(name: string): Repository | undefined {
for (const workspace of Object.values(this.store.workspaces)) {
const repo = workspace.repositories.find(r => r.name === name);
if (repo) {
return repo;
}
}
return undefined;
}
findRepositoryByPath(repoPath: string): Repository | undefined {
for (const workspace of Object.values(this.store.workspaces)) {
const repo = workspace.repositories.find(r => r.path === repoPath);
if (repo) {
return repo;
}
}
return undefined;
}
getPrimaryRepository(): Repository | undefined {
const activeWorkspace = this.getActiveWorkspace();
if (!activeWorkspace || activeWorkspace.repositories.length === 0) {
return undefined;
}
// Return the first repository marked as primary, or the first repository
return activeWorkspace.repositories.find(r => r.primary) || activeWorkspace.repositories[0];
}
private generateId(prefix: string = ''): string {
let uuid: string;
try {
uuid = crypto.randomUUID();
} catch {
// Fallback for environments where crypto.randomUUID is not available
uuid = Date.now().toString(16) + '-' + Math.random().toString(16).substr(2);
}
return prefix ? `${prefix}-${uuid}` : uuid;
}
}