@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
253 lines (219 loc) • 7.41 kB
text/typescript
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { AtlasConfig, ProjectMode, StorageMode, ProjectDetectionResult, DashboardConfig } from './types.js';
import { StorageManager } from '../storage/storage-manager.js';
export class ConfigManager {
private config: AtlasConfig;
private configPath: string | null = null;
private storageManager: StorageManager;
constructor() {
this.storageManager = new StorageManager();
this.config = this.getDefaultConfig();
}
private getDefaultConfig(): AtlasConfig {
return {
projectMode: 'new',
workspaceRoot: process.cwd(),
dataLocation: '.atlas/',
integration: {
respectGitignore: true,
preserveExisting: true,
modules: {
kanban: true,
development: true,
documentation: true,
'product-requirements': true,
agile: true,
},
},
features: {
tddEnforcement: 'strict',
autoInit: false,
autoBackup: true,
},
storage: {
mode: 'local',
isolation: 'project',
},
webDashboard: {
enabled: true,
port: process.env.ATLAS_DASHBOARD_PORT ? parseInt(process.env.ATLAS_DASHBOARD_PORT) :
process.env.NODE_ENV === 'test' ? 0 : 3001,
host: 'localhost',
autoOpen: false,
features: {
performance: true,
security: true,
agile: true,
errors: true
},
realTimeUpdates: true,
exportEnabled: true
},
version: '1.0.0',
};
}
private async getConfigPath(): Promise<string> {
// Store config in the storage location, not in project root by default
const location = await this.storageManager.getStorageLocation();
return path.join(location.config, 'atlas.config.json');
}
async load(): Promise<void> {
// First try to load from project root
try {
const localConfigPath = path.join(process.cwd(), 'atlas.config.json');
const data = await fs.readFile(localConfigPath, 'utf-8');
this.config = { ...this.getDefaultConfig(), ...JSON.parse(data) };
return;
} catch {
// Not in project root, continue
}
// Then try from storage location
try {
if (!this.configPath) {
this.configPath = await this.getConfigPath();
}
const data = await fs.readFile(this.configPath, 'utf-8');
this.config = { ...this.getDefaultConfig(), ...JSON.parse(data) };
} catch (error) {
// Use default config if file doesn't exist
this.config = this.getDefaultConfig();
}
}
async save(): Promise<void> {
// Save to current directory for tests/local use
const localConfigPath = path.join(process.cwd(), 'atlas.config.json');
await fs.writeFile(localConfigPath, JSON.stringify(this.config, null, 2));
}
async detectProject(): Promise<ProjectDetectionResult> {
const result: ProjectDetectionResult = {
hasGit: false,
hasPackageJson: false,
hasTsConfig: false,
hasTests: false,
suggestedMode: 'new',
existingTools: [],
};
// Check for Git
try {
await fs.access(path.join(process.cwd(), '.git'));
result.hasGit = true;
result.existingTools.push('git');
} catch {}
// Check for Node.js project
try {
await fs.access(path.join(process.cwd(), 'package.json'));
result.hasPackageJson = true;
result.projectType = 'node';
const packageJson = JSON.parse(
await fs.readFile(path.join(process.cwd(), 'package.json'), 'utf-8')
);
// Detect test framework
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
if (deps.jest || deps['@jest/core']) {
result.testFramework = 'jest';
result.hasTests = true;
} else if (deps.mocha) {
result.testFramework = 'mocha';
result.hasTests = true;
} else if (deps.vitest) {
result.testFramework = 'vitest';
result.hasTests = true;
}
// Check for existing PM tools
if (deps['@atlassian/jira']) result.existingTools.push('jira');
if (deps.trello) result.existingTools.push('trello');
} catch {}
// Check for TypeScript
try {
await fs.access(path.join(process.cwd(), 'tsconfig.json'));
result.hasTsConfig = true;
result.existingTools.push('typescript');
} catch {}
// Determine suggested mode
if (result.hasGit && result.hasPackageJson) {
result.suggestedMode = 'existing';
}
// Check for monorepo indicators
try {
const files = await fs.readdir(process.cwd());
const hasMultiplePackageJsons = 0;
for (const file of files) {
const filePath = path.join(process.cwd(), file);
const stat = await fs.stat(filePath);
if (stat.isDirectory() && !file.startsWith('.')) {
try {
await fs.access(path.join(filePath, 'package.json'));
result.suggestedMode = 'multi-repo';
break;
} catch {}
}
}
// Check for lerna, yarn workspaces, etc.
if (files.includes('lerna.json') || files.includes('pnpm-workspace.yaml')) {
result.suggestedMode = 'multi-repo';
}
} catch {}
return result;
}
async getStoragePath(): Promise<string> {
const location = await this.storageManager.getStorageLocation();
return location.data;
}
// Backwards compatibility - sync version
getDataPath(): string {
// Return based on current config
if (this.config.storage?.mode === 'home' && this.config.projectName) {
return path.join(os.homedir(), '.atlas', 'projects', this.config.projectName);
}
if (this.config.storage?.mode === 'custom' && this.config.storage?.path) {
return this.config.storage.path;
}
return path.join(this.config.workspaceRoot || process.cwd(), '.atlas');
}
getStorageManager(): StorageManager {
return this.storageManager;
}
get(): AtlasConfig {
return { ...this.config };
}
set(config: Partial<AtlasConfig>): void {
this.config = { ...this.config, ...config };
}
isModuleEnabled(module: string): boolean {
const modules = this.config.integration?.modules as any;
return modules?.[module] ?? true;
}
getProjectMode(): ProjectMode {
return this.config.projectMode;
}
getProjectId(): string | undefined {
return this.config.projectId;
}
isMultiRepo(): boolean {
return this.config.projectMode === 'multi-repo';
}
getRepositories(): NonNullable<AtlasConfig['repositories']> {
return this.config.repositories || [];
}
isDashboardEnabled(): boolean {
return this.config.webDashboard?.enabled ?? true;
}
getDashboardConfig(): DashboardConfig {
const defaults = this.getDefaultConfig().webDashboard!;
return {
...defaults,
...this.config.webDashboard
} as Required<DashboardConfig>;
}
updateDashboardConfig(dashboardConfig: Partial<DashboardConfig>): void {
this.config.webDashboard = {
...this.config.webDashboard,
...dashboardConfig
};
}
logModuleLoad(moduleName: string, type: 'new' | 'legacy', toolCount: number): void {
console.log(`✅ Loaded ${moduleName} module (${type}) with ${toolCount} tools`);
}
}