@fromsvenwithlove/devops-issues-cli
Version:
AI-powered CLI tool and library for Azure DevOps work item management with Claude agents
173 lines (138 loc) ⢠5.27 kB
JavaScript
import { readFileSync } from 'fs';
import chalk from 'chalk';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load schema
const schemaPath = join(__dirname, '..', 'schemas', 'work-item-hierarchy.json');
const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
// Configure AJV
const ajv = new Ajv({ allErrors: true, verbose: true });
addFormats(ajv);
const validate = ajv.compile(schema);
export default async function validateCommand(file, options = {}) {
try {
console.log(chalk.blue(`š Validating: ${file}`));
// Read and parse the file
const content = readFileSync(file, 'utf-8');
let workItemData;
try {
workItemData = JSON.parse(content);
} catch (parseError) {
console.log(chalk.red('ā Invalid JSON format'));
console.log(chalk.gray(`Parse error: ${parseError.message}`));
process.exit(1);
}
// Validate against schema
const isValid = validate(workItemData);
if (isValid) {
console.log(chalk.green('ā Validation passed'));
// Additional checks
const stats = analyzeWorkItems(workItemData);
console.log(chalk.blue('\nš Work Item Analysis:'));
console.log(` Total work items: ${chalk.yellow(stats.totalItems)}`);
console.log(` Work item types: ${chalk.yellow(Object.keys(stats.typeCount).join(', '))}`);
if (stats.typeCount) {
Object.entries(stats.typeCount).forEach(([type, count]) => {
console.log(` ${type}: ${chalk.yellow(count)}`);
});
}
if (stats.maxDepth > 0) {
console.log(` Maximum nesting depth: ${chalk.yellow(stats.maxDepth)}`);
}
if (stats.warnings.length > 0) {
console.log(chalk.yellow('\nā ļø Warnings:'));
stats.warnings.forEach(warning => {
console.log(chalk.yellow(` ⢠${warning}`));
});
}
if (options.verbose) {
console.log(chalk.blue('\nš Detailed Structure:'));
printStructure(workItemData.workItems, 0);
}
} else {
console.log(chalk.red('ā Validation failed'));
console.log(chalk.red('\nErrors:'));
validate.errors.forEach((error, index) => {
console.log(chalk.red(` ${index + 1}. ${error.instancePath || 'root'}: ${error.message}`));
if (error.data !== undefined) {
console.log(chalk.gray(` Current value: ${JSON.stringify(error.data)}`));
}
if (error.params) {
const params = Object.entries(error.params)
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
console.log(chalk.gray(` ${params}`));
}
});
process.exit(1);
}
} catch (error) {
console.log(chalk.red(`Error reading file: ${error.message}`));
process.exit(1);
}
}
function analyzeWorkItems(data) {
const stats = {
totalItems: 0,
typeCount: {},
maxDepth: 0,
warnings: []
};
function analyzeItems(items, depth = 0) {
if (!items || !Array.isArray(items)) return;
stats.maxDepth = Math.max(stats.maxDepth, depth);
items.forEach(item => {
stats.totalItems++;
// Count types
if (item.type) {
stats.typeCount[item.type] = (stats.typeCount[item.type] || 0) + 1;
}
// Check for potential issues
if (!item.title || item.title.trim().length === 0) {
stats.warnings.push(`Empty title found for ${item.type || 'unknown'} work item`);
}
if (item.title && item.title.length > 200) {
stats.warnings.push(`Very long title (${item.title.length} chars) for work item: "${item.title.substring(0, 50)}..."`);
}
if (item.type === 'User Story' && !item.acceptanceCriteria) {
stats.warnings.push(`User Story missing acceptance criteria: "${item.title}"`);
}
if (item.type === 'Bug' && !item.reproSteps) {
stats.warnings.push(`Bug missing reproduction steps: "${item.title}"`);
}
// Analyze children recursively
if (item.children) {
analyzeItems(item.children, depth + 1);
}
});
}
analyzeItems(data.workItems);
return stats;
}
function printStructure(items, depth = 0) {
if (!items || !Array.isArray(items)) return;
const indent = ' '.repeat(depth);
items.forEach((item, index) => {
const typeColor = getTypeColor(item.type);
const title = item.title ? item.title.substring(0, 60) : 'No title';
const truncated = item.title && item.title.length > 60 ? '...' : '';
console.log(`${indent}${chalk.gray(`${index + 1}.`)} ${typeColor(item.type)}: ${chalk.white(title)}${chalk.gray(truncated)}`);
if (item.children && item.children.length > 0) {
printStructure(item.children, depth + 1);
}
});
}
function getTypeColor(type) {
const colors = {
'Epic': chalk.magenta,
'Feature': chalk.blue,
'User Story': chalk.green,
'Task': chalk.yellow,
'Bug': chalk.red
};
return colors[type] || chalk.white;
}