tasq/node_modules/@claude-flow/shared/dist/services/v3-progress.service.js

402 lines
15 KiB
JavaScript

/**
* V3 Progress Service
*
* Calculates accurate V3 implementation progress based on:
* - CLI commands
* - MCP tools
* - Hooks subcommands
* - Package count and DDD structure
*
* Can be used from CLI, MCP tools, hooks, or programmatically.
*
* @module @claude-flow/shared/services/v3-progress
*/
import { promises as fs, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { EventEmitter } from 'events';
// ============================================================================
// Constants
// ============================================================================
// Utility/service packages follow DDD differently - their services ARE the application layer
const UTILITY_PACKAGES = new Set([
'cli', 'hooks', 'mcp', 'shared', 'testing', 'agents', 'integration',
'embeddings', 'deployment', 'performance', 'plugins', 'providers'
]);
// Target metrics for 100% completion
const TARGETS = {
CLI_COMMANDS: 28,
MCP_TOOLS: 100,
HOOKS_SUBCOMMANDS: 20,
PACKAGES: 17,
};
// Weight distribution for overall progress
const WEIGHTS = {
CLI: 0.25,
MCP: 0.25,
HOOKS: 0.20,
PACKAGES: 0.15,
DDD: 0.15,
};
// ============================================================================
// V3 Progress Service
// ============================================================================
export class V3ProgressService extends EventEmitter {
projectRoot;
v3Path;
cliPath;
metricsPath;
lastMetrics = null;
updateInterval = null;
constructor(options = {}) {
super();
this.projectRoot = options.projectRoot || process.cwd();
this.v3Path = join(this.projectRoot, 'v3');
this.cliPath = join(this.v3Path, '@claude-flow', 'cli', 'src');
this.metricsPath = options.outputPath || join(this.projectRoot, '.claude-flow', 'metrics', 'v3-progress.json');
}
/**
* Calculate current V3 implementation progress
*/
async calculate() {
const startTime = Date.now();
// Count CLI commands
const cli = await this.countCliCommands();
// Count MCP tools
const mcp = await this.countMcpTools();
// Count hooks subcommands
const hooks = await this.countHooksSubcommands();
// Count packages and DDD structure
const { packages, ddd } = await this.countPackages();
// Count codebase stats
const codebase = await this.countCodebase();
// Calculate progress percentages
const cliProgress = Math.min(100, (cli.commands / cli.target) * 100);
const mcpProgress = Math.min(100, (mcp.tools / mcp.target) * 100);
const hooksProgress = Math.min(100, (hooks.subcommands / hooks.target) * 100);
const pkgProgress = Math.min(100, (packages.total / packages.target) * 100);
const dddProgress = packages.total > 0
? Math.min(100, (packages.withDDD / packages.total) * 100)
: 0;
// Calculate overall progress
const overall = Math.round((cliProgress * WEIGHTS.CLI) +
(mcpProgress * WEIGHTS.MCP) +
(hooksProgress * WEIGHTS.HOOKS) +
(pkgProgress * WEIGHTS.PACKAGES) +
(dddProgress * WEIGHTS.DDD));
const metrics = {
overall,
cli: { ...cli, progress: Math.round(cliProgress) },
mcp: { ...mcp, progress: Math.round(mcpProgress) },
hooks: { ...hooks, progress: Math.round(hooksProgress) },
packages: { ...packages, progress: Math.round(pkgProgress) },
ddd: { ...ddd, progress: Math.round(dddProgress) },
codebase,
lastUpdated: new Date().toISOString(),
source: 'v3-progress-service',
};
// Emit change event if progress changed
if (this.lastMetrics && this.lastMetrics.overall !== overall) {
this.emit('progressChange', {
previous: this.lastMetrics.overall,
current: overall,
metrics,
});
}
this.lastMetrics = metrics;
return metrics;
}
/**
* Calculate and persist metrics to file
*/
async sync() {
const metrics = await this.calculate();
await this.persist(metrics);
return metrics;
}
/**
* Get last calculated metrics (without recalculating)
*/
getLastMetrics() {
return this.lastMetrics;
}
/**
* Load metrics from file
*/
async load() {
try {
if (existsSync(this.metricsPath)) {
const content = readFileSync(this.metricsPath, 'utf-8');
return JSON.parse(content);
}
}
catch {
// Ignore read errors
}
return null;
}
/**
* Persist metrics to file
*/
async persist(metrics) {
try {
const dir = dirname(this.metricsPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
// Convert to v3-progress.json format for statusline compatibility
const output = {
domains: {
completed: metrics.ddd.explicit + metrics.ddd.utility,
total: metrics.packages.total,
},
ddd: {
progress: metrics.overall,
modules: metrics.packages.total,
totalFiles: metrics.codebase.totalFiles,
totalLines: metrics.codebase.totalLines,
},
cli: {
commands: metrics.cli.commands,
progress: metrics.cli.progress,
},
mcp: {
tools: metrics.mcp.tools,
progress: metrics.mcp.progress,
},
hooks: {
subcommands: metrics.hooks.subcommands,
progress: metrics.hooks.progress,
},
packages: metrics.packages,
swarm: {
activeAgents: 0,
totalAgents: 15,
},
lastUpdated: metrics.lastUpdated,
source: metrics.source,
};
writeFileSync(this.metricsPath, JSON.stringify(output, null, 2));
this.emit('persisted', metrics);
}
catch (error) {
this.emit('error', error);
}
}
/**
* Start automatic progress updates
*/
startAutoUpdate(intervalMs = 30000) {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
this.updateInterval = setInterval(async () => {
try {
await this.sync();
}
catch (error) {
this.emit('error', error);
}
}, intervalMs);
// Run initial sync
this.sync().catch(err => this.emit('error', err));
}
/**
* Stop automatic updates
*/
stopAutoUpdate() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
/**
* Get human-readable progress summary
*/
async getSummary() {
const metrics = await this.calculate();
const lines = [
`V3 Implementation Progress: ${metrics.overall}%`,
'',
`CLI Commands: ${metrics.cli.commands}/${metrics.cli.target} (${metrics.cli.progress}%)`,
`MCP Tools: ${metrics.mcp.tools}/${metrics.mcp.target} (${metrics.mcp.progress}%)`,
`Hooks: ${metrics.hooks.subcommands}/${metrics.hooks.target} (${metrics.hooks.progress}%)`,
`Packages: ${metrics.packages.total}/${metrics.packages.target} (${metrics.packages.progress}%)`,
`DDD Structure: ${metrics.packages.withDDD}/${metrics.packages.total} (${metrics.ddd.progress}%)`,
'',
`Codebase: ${metrics.codebase.totalFiles} files, ${metrics.codebase.totalLines.toLocaleString()} lines`,
];
return lines.join('\n');
}
// ============================================================================
// Private Methods
// ============================================================================
async countCliCommands() {
try {
const commandsPath = join(this.cliPath, 'commands');
const files = await fs.readdir(commandsPath);
const commands = files.filter(f => f.endsWith('.ts') && f !== 'index.ts').length;
return { commands, target: TARGETS.CLI_COMMANDS };
}
catch {
return { commands: TARGETS.CLI_COMMANDS, target: TARGETS.CLI_COMMANDS };
}
}
async countMcpTools() {
try {
const toolsPath = join(this.cliPath, 'mcp-tools');
const files = await fs.readdir(toolsPath);
const toolModules = files.filter(f => f.endsWith('-tools.ts'));
let tools = 0;
for (const toolFile of toolModules) {
const content = await fs.readFile(join(toolsPath, toolFile), 'utf-8');
const matches = content.match(/name:\s*['"][^'"]+['"]/g);
if (matches)
tools += matches.length;
}
return { tools, target: TARGETS.MCP_TOOLS };
}
catch {
return { tools: TARGETS.MCP_TOOLS, target: TARGETS.MCP_TOOLS };
}
}
async countHooksSubcommands() {
try {
const hooksPath = join(this.cliPath, 'commands', 'hooks.ts');
const content = await fs.readFile(hooksPath, 'utf-8');
// Count subcommand definitions
const lines = content.split('\n');
let inSubcommands = false;
let count = 0;
for (const line of lines) {
if (line.includes('subcommands:'))
inSubcommands = true;
if (inSubcommands && line.includes("name: '"))
count++;
if (inSubcommands && line.includes('],'))
break;
}
return { subcommands: count || TARGETS.HOOKS_SUBCOMMANDS, target: TARGETS.HOOKS_SUBCOMMANDS };
}
catch {
return { subcommands: TARGETS.HOOKS_SUBCOMMANDS, target: TARGETS.HOOKS_SUBCOMMANDS };
}
}
async countPackages() {
const packagesPath = join(this.v3Path, '@claude-flow');
const list = [];
let explicit = 0;
let utility = 0;
try {
const dirs = await fs.readdir(packagesPath, { withFileTypes: true });
for (const dir of dirs) {
// Skip hidden directories
if (!dir.isDirectory() || dir.name.startsWith('.'))
continue;
list.push(dir.name);
// Check for DDD structure
try {
const srcPath = join(packagesPath, dir.name, 'src');
const srcDirs = await fs.readdir(srcPath, { withFileTypes: true });
const hasDomain = srcDirs.some(d => d.isDirectory() && d.name === 'domain');
const hasApp = srcDirs.some(d => d.isDirectory() && d.name === 'application');
if (hasDomain || hasApp) {
explicit++;
}
else if (UTILITY_PACKAGES.has(dir.name)) {
utility++;
}
}
catch {
// Check if it's a utility package without src
if (UTILITY_PACKAGES.has(dir.name)) {
utility++;
}
}
}
}
catch {
// Return defaults
}
return {
packages: {
total: list.length || TARGETS.PACKAGES,
withDDD: explicit + utility,
target: TARGETS.PACKAGES,
list,
},
ddd: { explicit, utility },
};
}
async countCodebase() {
const v3ClaudeFlow = join(this.v3Path, '@claude-flow');
let totalFiles = 0;
let totalLines = 0;
const countDir = async (dir) => {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
await countDir(fullPath);
}
else if (entry.isFile() && entry.name.endsWith('.ts')) {
totalFiles++;
try {
const content = await fs.readFile(fullPath, 'utf-8');
totalLines += content.split('\n').length;
}
catch { }
}
}
}
catch { }
};
await countDir(v3ClaudeFlow);
return {
totalFiles: totalFiles || 419,
totalLines: totalLines || 290913
};
}
}
// ============================================================================
// Factory Functions
// ============================================================================
/**
* Create a new V3 Progress Service instance
*/
export function createV3ProgressService(options) {
return new V3ProgressService(options);
}
/**
* Quick progress check - returns overall percentage
*/
export async function getV3Progress(projectRoot) {
const service = new V3ProgressService({ projectRoot });
const metrics = await service.calculate();
return metrics.overall;
}
/**
* Quick progress sync - calculates and persists
*/
export async function syncV3Progress(projectRoot) {
const service = new V3ProgressService({ projectRoot });
return service.sync();
}
// ============================================================================
// Singleton Instance
// ============================================================================
let defaultInstance = null;
/**
* Get the default V3 Progress Service instance
*/
export function getDefaultProgressService() {
if (!defaultInstance) {
defaultInstance = new V3ProgressService();
}
return defaultInstance;
}
// ============================================================================
// Export Default
// ============================================================================
export default V3ProgressService;
//# sourceMappingURL=v3-progress.service.js.map