BMAD-METHOD/tools/installer/lib/workspace-setup-old.js

721 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const path = require("path");
const fs = require("fs-extra");
const chalk = require("chalk");
class WorkspaceSetup {
constructor() {
this.workspaceStructure = {
'.workspace': {
'sessions': {},
'context': {},
'handoffs': {},
'decisions': {},
'progress': {},
'quality': {},
'archive': {}
}
};
}
async createWorkspaceDirectory(installDir, spinner) {
try {
spinner.text = 'Creating collaborative workspace structure...';
const workspacePath = path.join(installDir, '.workspace');
// Create main workspace directory
await fs.ensureDir(workspacePath);
// Create subdirectories
const subdirs = ['sessions', 'context', 'handoffs', 'decisions', 'progress', 'quality', 'archive'];
for (const subdir of subdirs) {
await fs.ensureDir(path.join(workspacePath, subdir));
}
// Create initial workspace configuration
const workspaceConfig = {
version: "1.0",
created: new Date().toISOString(),
structure: subdirs,
settings: {
maxContextSize: "10MB",
sessionTimeout: "2h",
archiveAfter: "30d",
maxConcurrentSessions: 5
}
};
await fs.writeJSON(
path.join(workspacePath, 'workspace-config.json'),
workspaceConfig,
{ spaces: 2 }
);
// Create initial README
const readmeContent = `# BMAD Collaborative Workspace
This directory contains the collaborative workspace system for multi-session AI agent coordination.
## Directory Structure
- \`sessions/\` - Active session tracking
- \`context/\` - Shared context files and decisions
- \`handoffs/\` - Agent transition packages
- \`decisions/\` - Architectural and design decisions
- \`progress/\` - Story and task progress tracking
- \`quality/\` - Quality metrics and audit results
- \`archive/\` - Compressed historical context
## Usage
### Claude Code CLI Users
- Use \`*workspace-init\` to initialize a collaborative session
- Use \`*workspace-status\` to see active sessions and progress
- Use \`*workspace-cleanup\` for maintenance
### Other IDE Users
- Run \`npm run workspace-init\` to initialize
- Run \`npm run workspace-status\` for status
- Run \`npm run workspace-cleanup\` for maintenance
## Configuration
Workspace settings can be modified in \`workspace-config.json\`.
`;
await fs.writeFile(path.join(workspacePath, 'README.md'), readmeContent);
return true;
} catch (error) {
console.error(chalk.red('Failed to create workspace directory:'), error.message);
return false;
}
}
async createWorkspaceUtilities(installDir, selectedIDEs, spinner) {
try {
spinner.text = 'Installing workspace utilities...';
const utilsPath = path.join(installDir, 'workspace-utils');
await fs.ensureDir(utilsPath);
// Create utility scripts
await this.createInitScript(utilsPath);
await this.createStatusScript(utilsPath);
await this.createCleanupScript(utilsPath);
await this.createHandoffScript(utilsPath);
await this.createSyncScript(utilsPath);
// Create package.json scripts if package.json exists
await this.addPackageJsonScripts(installDir);
// Create IDE-specific documentation
await this.createIDEDocumentation(utilsPath, selectedIDEs);
return true;
} catch (error) {
console.error(chalk.red('Failed to create workspace utilities:'), error.message);
return false;
}
}
async createInitScript(utilsPath) {
const initScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
async function initWorkspace() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found. Run \`npx bmad-method install\` first.');
process.exit(1);
}
// Generate session ID
const sessionId = crypto.randomBytes(8).toString('hex');
const timestamp = new Date().toISOString();
// Create session file
const sessionData = {
id: sessionId,
created: timestamp,
lastHeartbeat: timestamp,
ide: process.env.IDE_TYPE || 'unknown',
pid: process.pid,
user: process.env.USER || process.env.USERNAME || 'unknown'
};
const sessionsPath = path.join(workspacePath, 'sessions');
if (!fs.existsSync(sessionsPath)) {
fs.mkdirSync(sessionsPath, { recursive: true });
}
const sessionFile = path.join(sessionsPath, \`\${sessionId}.json\`);
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2));
console.log('✅ Workspace initialized successfully');
console.log(\`📍 Session ID: \${sessionId}\`);
console.log(\`🕐 Created: \${timestamp}\`);
return sessionId;
} catch (error) {
console.error('❌ Failed to initialize workspace:', error.message);
process.exit(1);
}
}
if (require.main === module) {
initWorkspace();
}
module.exports = { initWorkspace };
`;
await fs.writeFile(path.join(utilsPath, 'init.js'), initScript);
await fs.chmod(path.join(utilsPath, 'init.js'), 0o755);
}
async createStatusScript(utilsPath) {
const statusScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
async function getWorkspaceStatus() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found.');
process.exit(1);
}
// Read workspace config
const configPath = path.join(workspacePath, 'workspace-config.json');
let config = {};
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configContent);
}
// Get active sessions
const sessionsPath = path.join(workspacePath, 'sessions');
let sessionFiles = [];
if (fs.existsSync(sessionsPath)) {
sessionFiles = fs.readdirSync(sessionsPath);
}
const activeSessions = [];
for (const file of sessionFiles) {
if (file.endsWith('.json')) {
try {
const sessionPath = path.join(sessionsPath, file);
const sessionContent = fs.readFileSync(sessionPath, 'utf8');
const sessionData = JSON.parse(sessionContent);
activeSessions.push(sessionData);
} catch (e) {
// Skip corrupted session files
}
}
}
// Display status
console.log('🤝 BMAD Collaborative Workspace Status');
console.log('=====================================');
console.log(\`📁 Workspace: \${workspacePath}\`);
console.log(\`⚙️ Version: \${config.version || 'Unknown'}\`);
console.log(\`🕐 Created: \${config.created || 'Unknown'}\`);
console.log(\`👥 Active Sessions: \${activeSessions.length}\`);
if (activeSessions.length > 0) {
console.log('\\n📍 Session Details:');
activeSessions.forEach((session, index) => {
console.log(\` \${index + 1}. \${session.id} (\${session.ide}) - \${session.user}\`);
console.log(\` Created: \${new Date(session.created).toLocaleString()}\`);
console.log(\` Last Heartbeat: \${new Date(session.lastHeartbeat).toLocaleString()}\`);
});
}
// Check directory structure
const directories = ['context', 'handoffs', 'decisions', 'progress', 'quality', 'archive'];
const missingDirs = [];
for (const dir of directories) {
if (!fs.existsSync(path.join(workspacePath, dir))) {
missingDirs.push(dir);
}
}
if (missingDirs.length > 0) {
console.log(\`\\n⚠ Missing directories: \${missingDirs.join(', ')}\`);
console.log(' Run \`npm run workspace-cleanup\` to repair.');
} else {
console.log('\\n✅ Workspace structure is healthy');
}
} catch (error) {
console.error('❌ Failed to get workspace status:', error.message);
process.exit(1);
}
}
if (require.main === module) {
getWorkspaceStatus();
}
module.exports = { getWorkspaceStatus };
`;
await fs.writeFile(path.join(utilsPath, 'status.js'), statusScript);
await fs.chmod(path.join(utilsPath, 'status.js'), 0o755);
}
async createCleanupScript(utilsPath) {
const cleanupScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function removeFile(filePath) {
try {
fs.unlinkSync(filePath);
return true;
} catch (e) {
return false;
}
}
function moveFile(sourcePath, targetPath) {
try {
const data = fs.readFileSync(sourcePath);
fs.writeFileSync(targetPath, data);
fs.unlinkSync(sourcePath);
return true;
} catch (e) {
return false;
}
}
async function cleanupWorkspace() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!await fs.pathExists(workspacePath)) {
console.error('❌ Workspace directory not found.');
process.exit(1);
}
console.log('🧹 Starting workspace cleanup...');
// Repair directory structure
const directories = ['sessions', 'context', 'handoffs', 'decisions', 'progress', 'quality', 'archive'];
let repairedDirs = 0;
for (const dir of directories) {
const dirPath = path.join(workspacePath, dir);
if (!await fs.pathExists(dirPath)) {
await fs.ensureDir(dirPath);
repairedDirs++;
}
}
if (repairedDirs > 0) {
console.log(\`✅ Repaired \${repairedDirs} missing directories\`);
}
// Clean up expired sessions (older than 2 hours)
const sessionsPath = path.join(workspacePath, 'sessions');
const sessionFiles = await fs.readdir(sessionsPath).catch(() => []);
const twoHoursAgo = Date.now() - (2 * 60 * 60 * 1000);
let cleanedSessions = 0;
for (const file of sessionFiles) {
if (file.endsWith('.json')) {
try {
const sessionPath = path.join(sessionsPath, file);
const sessionData = await fs.readJSON(sessionPath);
const lastHeartbeat = new Date(sessionData.lastHeartbeat).getTime();
if (lastHeartbeat < twoHoursAgo) {
await fs.remove(sessionPath);
cleanedSessions++;
}
} catch (e) {
// Remove corrupted session files
await fs.remove(path.join(sessionsPath, file));
cleanedSessions++;
}
}
}
if (cleanedSessions > 0) {
console.log(\`✅ Cleaned up \${cleanedSessions} expired sessions\`);
}
// Archive old context files (older than 30 days)
const contextPath = path.join(workspacePath, 'context');
const archivePath = path.join(workspacePath, 'archive');
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
if (await fs.pathExists(contextPath)) {
const contextFiles = await fs.readdir(contextPath).catch(() => []);
let archivedFiles = 0;
for (const file of contextFiles) {
const filePath = path.join(contextPath, file);
const stats = await fs.stat(filePath).catch(() => null);
if (stats && stats.mtime.getTime() < thirtyDaysAgo) {
const archiveFile = path.join(archivePath, \`archived-\${Date.now()}-\${file}\`);
await fs.move(filePath, archiveFile);
archivedFiles++;
}
}
if (archivedFiles > 0) {
console.log(\`✅ Archived \${archivedFiles} old context files\`);
}
}
console.log('✅ Workspace cleanup completed successfully');
} catch (error) {
console.error('❌ Failed to cleanup workspace:', error.message);
process.exit(1);
}
}
if (require.main === module) {
cleanupWorkspace();
}
module.exports = { cleanupWorkspace };
`;
await fs.writeFile(path.join(utilsPath, 'cleanup.js'), cleanupScript);
await fs.chmod(path.join(utilsPath, 'cleanup.js'), 0o755);
}
async createHandoffScript(utilsPath) {
const handoffScript = `#!/usr/bin/env node
const fs = require('fs-extra');
const path = require('path');
async function createHandoff(fromAgent, toAgent, context = '') {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
const handoffsPath = path.join(workspacePath, 'handoffs');
if (!await fs.pathExists(handoffsPath)) {
console.error('❌ Workspace handoffs directory not found.');
process.exit(1);
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const handoffId = \`\${fromAgent}-to-\${toAgent}-\${timestamp}\`;
const handoffFile = path.join(handoffsPath, \`\${handoffId}.md\`);
const handoffContent = \`# Agent Handoff: \${fromAgent} → \${toAgent}
**Created:** \${new Date().toISOString()}
**Handoff ID:** \${handoffId}
**Source Agent:** \${fromAgent}
**Target Agent:** \${toAgent}
## Context Summary
\${context || 'No additional context provided.'}
## Key Decisions Made
[To be filled by source agent]
## Current Progress
[To be filled by source agent]
## Next Actions for \${toAgent}
- [ ] [Action item 1]
- [ ] [Action item 2]
- [ ] [Action item 3]
## Files and References
[List of relevant files and documentation]
## Blockers and Dependencies
[Any blockers or dependencies the target agent should be aware of]
## Handoff Validation
- [ ] Context completeness verified
- [ ] Decisions documented
- [ ] Next actions clearly defined
- [ ] References included
\`;
await fs.writeFile(handoffFile, handoffContent);
console.log('✅ Handoff package created successfully');
console.log(\`📦 Handoff ID: \${handoffId}\`);
console.log(\`📁 File: \${handoffFile}\`);
return handoffId;
} catch (error) {
console.error('❌ Failed to create handoff:', error.message);
process.exit(1);
}
}
// Command line usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('Usage: node handoff.js <from-agent> <to-agent> [context]');
process.exit(1);
}
createHandoff(args[0], args[1], args[2] || '');
}
module.exports = { createHandoff };
`;
await fs.writeFile(path.join(utilsPath, 'handoff.js'), handoffScript);
await fs.chmod(path.join(utilsPath, 'handoff.js'), 0o755);
}
async createSyncScript(utilsPath) {
const syncScript = `#!/usr/bin/env node
const fs = require('fs-extra');
const path = require('path');
async function syncWorkspace() {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!await fs.pathExists(workspacePath)) {
console.error('❌ Workspace directory not found.');
process.exit(1);
}
console.log('🔄 Synchronizing workspace context...');
// Update session heartbeat
const sessionsPath = path.join(workspacePath, 'sessions');
const sessionFiles = await fs.readdir(sessionsPath).catch(() => []);
// For simplicity, update the most recent session
let latestSession = null;
let latestTime = 0;
for (const file of sessionFiles) {
if (file.endsWith('.json')) {
try {
const sessionPath = path.join(sessionsPath, file);
const sessionData = await fs.readJSON(sessionPath);
const created = new Date(sessionData.created).getTime();
if (created > latestTime) {
latestTime = created;
latestSession = { path: sessionPath, data: sessionData };
}
} catch (e) {
// Skip corrupted files
}
}
}
if (latestSession) {
latestSession.data.lastHeartbeat = new Date().toISOString();
await fs.writeJSON(latestSession.path, latestSession.data, { spaces: 2 });
console.log(\`✅ Updated session heartbeat: \${latestSession.data.id}\`);
}
// Load and display recent context
const contextPath = path.join(workspacePath, 'context');
const sharedContext = path.join(contextPath, 'shared-context.md');
if (await fs.pathExists(sharedContext)) {
const content = await fs.readFile(sharedContext, 'utf8');
console.log('\\n📄 Current Shared Context:');
console.log('=' .repeat(50));
console.log(content.substring(0, 500) + (content.length > 500 ? '...' : ''));
} else {
console.log('\\n📄 No shared context available yet.');
}
console.log('\\n✅ Workspace synchronization completed');
} catch (error) {
console.error('❌ Failed to sync workspace:', error.message);
process.exit(1);
}
}
if (require.main === module) {
syncWorkspace();
}
module.exports = { syncWorkspace };
`;
await fs.writeFile(path.join(utilsPath, 'sync.js'), syncScript);
await fs.chmod(path.join(utilsPath, 'sync.js'), 0o755);
}
async addPackageJsonScripts(installDir) {
const packageJsonPath = path.join(installDir, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath);
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Add workspace scripts
packageJson.scripts['workspace-init'] = 'node workspace-utils/init.js';
packageJson.scripts['workspace-status'] = 'node workspace-utils/status.js';
packageJson.scripts['workspace-cleanup'] = 'node workspace-utils/cleanup.js';
packageJson.scripts['workspace-handoff'] = 'node workspace-utils/handoff.js';
packageJson.scripts['workspace-sync'] = 'node workspace-utils/sync.js';
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
}
}
async createIDEDocumentation(utilsPath, selectedIDEs) {
const docsPath = path.join(utilsPath, 'docs');
await fs.ensureDir(docsPath);
const ideDocuments = {
'cursor': `# Workspace Usage in Cursor
## Getting Started
1. Open terminal in Cursor
2. Run \`npm run workspace-init\` to start collaborative session
3. Use \`npm run workspace-status\` to see active sessions
## Best Practices
- Use @dev, @qa, @architect mentions to invoke BMAD agents
- Run \`npm run workspace-sync\` before major context switches
- Check \`npm run workspace-status\` to see other team members' progress
`,
'windsurf': `# Workspace Usage in Windsurf
## Getting Started
1. Open terminal in Windsurf
2. Run \`npm run workspace-init\` to start collaborative session
3. Use \`npm run workspace-status\` to see active sessions
## Best Practices
- Use @agent-name to invoke BMAD agents
- Run \`npm run workspace-sync\` to stay synchronized
- Check workspace status regularly for team coordination
`,
'claude-code': `# Workspace Usage in Claude Code CLI
## Getting Started
Claude Code CLI users get enhanced workspace experience with native commands:
- \`*workspace-init\` - Initialize collaborative session (automatic)
- \`*workspace-status\` - Show active sessions and progress
- \`*workspace-cleanup\` - Clean up and optimize workspace
- \`*workspace-handoff [agent]\` - Prepare handoff to another agent
- \`*workspace-sync\` - Synchronize with latest context
## Native Integration
Workspace features are automatically integrated into your Claude Code CLI session:
- Automatic session registration and heartbeat
- Context-aware agent handoffs
- Intelligent workspace suggestions
`,
'trae': `# Workspace Usage in Trae
## Getting Started
1. Open terminal in Trae
2. Run \`npm run workspace-init\` to start collaborative session
3. Use \`npm run workspace-status\` to see active sessions
## Integration
- Use @agent mentions to work with BMAD agents
- Workspace context automatically persists across sessions
- Use \`npm run workspace-handoff dev qa\` for explicit handoffs
`
};
for (const ide of selectedIDEs) {
if (ideDocuments[ide]) {
await fs.writeFile(
path.join(docsPath, `${ide}.md`),
ideDocuments[ide]
);
}
}
}
async setupClaudeCodeWorkspaceCommands(installDir, spinner) {
try {
spinner.text = 'Integrating workspace commands with Claude Code CLI agents...';
const bmadCorePath = path.join(installDir, '.bmad-core');
const agentsPath = path.join(bmadCorePath, 'agents');
if (!await fs.pathExists(agentsPath)) {
console.warn('⚠️ .bmad-core/agents directory not found. Skipping Claude Code integration.');
return false;
}
// Add workspace commands to key agents
const agentsToUpdate = ['dev.md', 'qa.md', 'sm.md'];
for (const agentFile of agentsToUpdate) {
const agentPath = path.join(agentsPath, agentFile);
if (await fs.pathExists(agentPath)) {
let content = await fs.readFile(agentPath, 'utf8');
// Check if workspace commands already exist
if (!content.includes('*workspace-init')) {
// Add workspace commands section
const workspaceCommands = `
## Workspace Commands
You have access to collaborative workspace commands for multi-session coordination:
- \`*workspace-init\` - Initialize collaborative workspace session
- \`*workspace-status\` - Show current workspace status and active sessions
- \`*workspace-cleanup\` - Clean up workspace files and optimize storage
- \`*workspace-handoff [target-agent]\` - Prepare context handoff to specified agent
- \`*workspace-sync\` - Synchronize with latest workspace context
Use these commands to coordinate with other AI agents and maintain context across sessions.
`;
// Insert before the last section (usually before final instructions)
const insertPoint = content.lastIndexOf('\n## ');
if (insertPoint > -1) {
content = content.slice(0, insertPoint) + workspaceCommands + '\n' + content.slice(insertPoint);
} else {
content += workspaceCommands;
}
await fs.writeFile(agentPath, content);
}
}
}
return true;
} catch (error) {
console.error(chalk.red('Failed to integrate Claude Code workspace commands:'), error.message);
return false;
}
}
}
module.exports = WorkspaceSetup;