aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
310 lines (266 loc) • 7.87 kB
JavaScript
/**
* Dashboard Status Writer
*
* Writes the current AIOS agent status to .aios/dashboard/status.json
* This file is watched by the dashboard via SSE for real-time updates.
*
* Usage:
* const statusWriter = require('./dashboard-status-writer');
*
* // Activate an agent
* await statusWriter.activateAgent('dev', 'story-123');
*
* // Deactivate agent
* await statusWriter.deactivateAgent();
*
* // Update session info
* await statusWriter.updateSession({ commandsExecuted: 5 });
*/
const fs = require('fs').promises;
const path = require('path');
// Status file location (relative to project root)
const STATUS_DIR = '.aios/dashboard';
const STATUS_FILE = 'status.json';
/**
* Ensures the status directory exists
*/
async function ensureStatusDir(projectRoot) {
const dirPath = path.join(projectRoot, STATUS_DIR);
try {
await fs.mkdir(dirPath, { recursive: true });
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
}
return dirPath;
}
/**
* Gets the current project root
* Uses AIOS_PROJECT_ROOT env var or cwd
*/
function getProjectRoot() {
return process.env.AIOS_PROJECT_ROOT || process.cwd();
}
/**
* Reads the current status file
* Returns default status if file doesn't exist
*/
async function readStatus(projectRoot) {
const filePath = path.join(projectRoot, STATUS_DIR, STATUS_FILE);
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
// Return default status
return createDefaultStatus(projectRoot);
}
throw error;
}
}
/**
* Creates the default status object
*/
function createDefaultStatus(projectRoot) {
const projectName = path.basename(projectRoot);
return {
version: '1.0',
updatedAt: new Date().toISOString(),
project: {
name: projectName,
path: projectRoot,
},
activeAgent: null,
session: null,
stories: {
inProgress: [],
completed: [],
},
rateLimit: null,
};
}
/**
* Writes the status to file
*/
async function writeStatus(projectRoot, status) {
await ensureStatusDir(projectRoot);
const filePath = path.join(projectRoot, STATUS_DIR, STATUS_FILE);
status.updatedAt = new Date().toISOString();
await fs.writeFile(filePath, JSON.stringify(status, null, 2), 'utf-8');
return status;
}
// ============ Public API ============
/**
* Activates an agent with optional story context
*
* @param {string} agentId - Agent ID (dev, qa, architect, pm, po, analyst, devops)
* @param {string} [storyId] - Optional story ID the agent is working on
* @param {string} [projectRoot] - Optional project root path
*/
async function activateAgent(agentId, storyId, projectRoot) {
projectRoot = projectRoot || getProjectRoot();
const status = await readStatus(projectRoot);
const validAgents = ['dev', 'qa', 'architect', 'pm', 'po', 'analyst', 'devops'];
if (!validAgents.includes(agentId)) {
throw new Error(`Invalid agent ID: ${agentId}. Must be one of: ${validAgents.join(', ')}`);
}
const agentNames = {
dev: 'Dev',
qa: 'QA',
architect: 'Architect',
pm: 'PM',
po: 'PO',
analyst: 'Analyst',
devops: 'DevOps',
};
status.activeAgent = {
id: agentId,
name: agentNames[agentId],
activatedAt: new Date().toISOString(),
currentStory: storyId || undefined,
};
// Update stories in progress
if (storyId && !status.stories.inProgress.includes(storyId)) {
status.stories.inProgress.push(storyId);
}
// Create or update session
if (!status.session) {
status.session = {
startedAt: new Date().toISOString(),
commandsExecuted: 0,
};
}
return writeStatus(projectRoot, status);
}
/**
* Deactivates the current agent
*
* @param {string} [projectRoot] - Optional project root path
*/
async function deactivateAgent(projectRoot) {
projectRoot = projectRoot || getProjectRoot();
const status = await readStatus(projectRoot);
status.activeAgent = null;
return writeStatus(projectRoot, status);
}
/**
* Updates session information
*
* @param {Object} sessionData - Session data to update
* @param {number} [sessionData.commandsExecuted] - Number of commands executed
* @param {string} [sessionData.lastCommand] - Last command executed
* @param {string} [projectRoot] - Optional project root path
*/
async function updateSession(sessionData, projectRoot) {
projectRoot = projectRoot || getProjectRoot();
const status = await readStatus(projectRoot);
if (!status.session) {
status.session = {
startedAt: new Date().toISOString(),
commandsExecuted: 0,
};
}
if (typeof sessionData.commandsExecuted === 'number') {
status.session.commandsExecuted = sessionData.commandsExecuted;
}
if (sessionData.lastCommand) {
status.session.lastCommand = sessionData.lastCommand;
}
return writeStatus(projectRoot, status);
}
/**
* Increments the command count
*
* @param {string} [lastCommand] - Optional last command name
* @param {string} [projectRoot] - Optional project root path
*/
async function incrementCommands(lastCommand, projectRoot) {
projectRoot = projectRoot || getProjectRoot();
const status = await readStatus(projectRoot);
if (!status.session) {
status.session = {
startedAt: new Date().toISOString(),
commandsExecuted: 0,
};
}
status.session.commandsExecuted++;
if (lastCommand) {
status.session.lastCommand = lastCommand;
}
return writeStatus(projectRoot, status);
}
/**
* Marks a story as completed
*
* @param {string} storyId - Story ID to mark as completed
* @param {string} [projectRoot] - Optional project root path
*/
async function completeStory(storyId, projectRoot) {
projectRoot = projectRoot || getProjectRoot();
const status = await readStatus(projectRoot);
// Remove from in progress
status.stories.inProgress = status.stories.inProgress.filter((id) => id !== storyId);
// Add to completed
if (!status.stories.completed.includes(storyId)) {
status.stories.completed.push(storyId);
}
// Clear from active agent
if (status.activeAgent && status.activeAgent.currentStory === storyId) {
status.activeAgent.currentStory = undefined;
}
return writeStatus(projectRoot, status);
}
/**
* Updates rate limit information
*
* @param {Object} rateLimit - Rate limit data
* @param {number} rateLimit.used - Used requests
* @param {number} rateLimit.limit - Total limit
* @param {string} [rateLimit.resetsAt] - Reset timestamp
* @param {string} [projectRoot] - Optional project root path
*/
async function updateRateLimit(rateLimit, projectRoot) {
projectRoot = projectRoot || getProjectRoot();
const status = await readStatus(projectRoot);
status.rateLimit = {
used: rateLimit.used,
limit: rateLimit.limit,
resetsAt: rateLimit.resetsAt,
};
return writeStatus(projectRoot, status);
}
/**
* Clears the status file (useful for tests or reset)
*
* @param {string} [projectRoot] - Optional project root path
*/
async function clearStatus(projectRoot) {
projectRoot = projectRoot || getProjectRoot();
const status = createDefaultStatus(projectRoot);
return writeStatus(projectRoot, status);
}
/**
* Gets the current status without modifying it
*
* @param {string} [projectRoot] - Optional project root path
* @returns {Promise<Object>} Current status
*/
async function getStatus(projectRoot) {
projectRoot = projectRoot || getProjectRoot();
return readStatus(projectRoot);
}
module.exports = {
activateAgent,
deactivateAgent,
updateSession,
incrementCommands,
completeStory,
updateRateLimit,
clearStatus,
getStatus,
// Internal exports for testing
_ensureStatusDir: ensureStatusDir,
_readStatus: readStatus,
_writeStatus: writeStatus,
};