@fromsvenwithlove/devops-issues-cli
Version:
AI-powered CLI tool and library for Azure DevOps work item management with Claude agents
334 lines (294 loc) ⢠9.96 kB
JavaScript
import chalk from 'chalk';
import inquirer from 'inquirer';
import { writeFileSync } from 'fs';
import { templates, getTemplateList, getTemplatesByCategory, createFromTemplate, categories } from '../../templates/index.js';
export default async function templatesCommand(action, options = {}) {
switch (action) {
case 'list':
await listTemplates(options);
break;
case 'show':
await showTemplate(options);
break;
case 'generate':
await generateFromTemplate(options);
break;
case undefined:
await interactiveTemplateMenu();
break;
default:
console.log(chalk.red(`Unknown action: ${action}`));
console.log(chalk.blue('Available actions: list, show, generate'));
process.exit(1);
}
}
async function listTemplates(options) {
console.log(chalk.blue('š Available Templates:\n'));
const allTemplates = getTemplateList();
if (options.category) {
const filtered = getTemplatesByCategory(options.category);
if (filtered.length === 0) {
console.log(chalk.yellow(`No templates found for category: ${options.category}`));
console.log(chalk.blue(`Available categories: ${Object.keys(categories).join(', ')}`));
return;
}
displayTemplates(filtered);
} else {
// Group by category
const grouped = {};
allTemplates.forEach(template => {
const category = template.category || 'other';
if (!grouped[category]) grouped[category] = [];
grouped[category].push(template);
});
Object.entries(grouped).forEach(([category, templates]) => {
const categoryName = categories[category] || category;
console.log(chalk.cyan(`${categoryName}:`));
displayTemplates(templates, ' ');
console.log('');
});
}
}
function displayTemplates(templates, indent = '') {
templates.forEach(template => {
console.log(`${indent}${chalk.green(template.key)} - ${template.name}`);
console.log(`${indent} ${chalk.gray(template.description)}`);
console.log(`${indent} ${chalk.blue(`File: ${template.file}`)}`);
});
}
async function showTemplate(options) {
let templateKey = options.template;
if (!templateKey) {
const templates = getTemplateList();
const { selected } = await inquirer.prompt([{
type: 'list',
name: 'selected',
message: 'Select a template to view:',
choices: templates.map(t => ({
name: `${t.name} - ${t.description}`,
value: t.key
}))
}]);
templateKey = selected;
}
const template = templates[templateKey];
if (!template) {
console.log(chalk.red(`Template '${templateKey}' not found`));
process.exit(1);
}
console.log(chalk.blue(`š Template: ${template.name}`));
console.log(chalk.gray(`Description: ${template.description}`));
console.log(chalk.gray(`Category: ${categories[template.category] || template.category}`));
console.log('');
try {
const content = template.content;
if (options.json) {
console.log(JSON.stringify(content, null, 2));
} else {
console.log(chalk.blue('Structure:'));
printTemplateStructure(content);
if (options.full) {
console.log(chalk.blue('\nFull JSON:'));
console.log(JSON.stringify(content, null, 2));
}
}
} catch (error) {
console.log(chalk.red(`Error loading template: ${error.message}`));
}
}
async function generateFromTemplate(options) {
let templateKey = options.template;
if (!templateKey) {
const templates = getTemplateList();
const { selected } = await inquirer.prompt([{
type: 'list',
name: 'selected',
message: 'Select a template to generate from:',
choices: templates.map(t => ({
name: `${t.name} - ${t.description}`,
value: t.key
}))
}]);
templateKey = selected;
}
const template = templates[templateKey];
if (!template) {
console.log(chalk.red(`Template '${templateKey}' not found`));
process.exit(1);
}
console.log(chalk.blue(`šÆ Generating from template: ${template.name}`));
// Get customization options
const overrides = await getTemplateOverrides(template);
// Generate the work items
let workItems;
try {
workItems = createFromTemplate(templateKey, overrides);
} catch (error) {
console.log(chalk.red(`Error generating template: ${error.message}`));
process.exit(1);
}
// Get output filename
let outputFile = options.output;
if (!outputFile) {
const { filename } = await inquirer.prompt([{
type: 'input',
name: 'filename',
message: 'Output filename:',
default: `${templateKey}-workitems.json`,
validate: input => input.trim().length > 0 || 'Filename is required'
}]);
outputFile = filename;
}
// Write to file
try {
writeFileSync(outputFile, JSON.stringify(workItems, null, 2), 'utf-8');
console.log(chalk.green(`ā Generated work items saved to: ${outputFile}`));
// Show next steps
console.log(chalk.blue('\nNext steps:'));
console.log(` 1. Review: ${chalk.yellow(`cat ${outputFile}`)}`);
console.log(` 2. Validate: ${chalk.yellow(`npx devops-issues validate ${outputFile}`)}`);
console.log(` 3. Preview: ${chalk.yellow(`npx devops-issues bulk-create ${outputFile} --preview`)}`);
console.log(` 4. Create: ${chalk.yellow(`npx devops-issues bulk-create ${outputFile}`)}`);
} catch (error) {
console.log(chalk.red(`Error writing file: ${error.message}`));
process.exit(1);
}
}
async function getTemplateOverrides(template) {
console.log(chalk.blue('\nš§ Customize template settings:'));
const overrides = { metadata: { defaults: {} } };
// Get basic overrides
const answers = await inquirer.prompt([
{
type: 'input',
name: 'parentId',
message: 'Parent Work Item ID (required - Epic, Feature, or other parent):',
validate: input => {
if (!input || input.trim().length === 0) {
return 'Parent ID is required for all work items';
}
const trimmed = input.trim();
if (!/^\d+$/.test(trimmed) && trimmed !== 'PARENT_WORK_ITEM_ID') {
return 'Parent ID must be a number (e.g., 12345) or placeholder text';
}
return true;
},
filter: input => input.trim()
},
{
type: 'input',
name: 'areaPath',
message: 'Area Path (e.g., YourProject\\YourArea):',
default: 'YourProject\\YourArea'
},
{
type: 'input',
name: 'iterationPath',
message: 'Iteration Path (e.g., YourProject\\Sprint 1):',
default: 'YourProject\\Sprint 1'
},
{
type: 'input',
name: 'assignedTo',
message: 'Default assignee (email):',
default: 'user@company.com'
},
{
type: 'list',
name: 'priority',
message: 'Default priority:',
choices: [
{ name: '1 - Highest', value: 1 },
{ name: '2 - High', value: 2 },
{ name: '3 - Medium', value: 3 },
{ name: '4 - Low', value: 4 }
],
default: 2
},
{
type: 'input',
name: 'tags',
message: 'Default tags (semicolon separated):',
default: 'generated;template'
}
]);
overrides.metadata.parentId = answers.parentId;
overrides.metadata.areaPath = answers.areaPath;
overrides.metadata.iterationPath = answers.iterationPath;
overrides.metadata.defaults.assignedTo = answers.assignedTo;
overrides.metadata.defaults.priority = answers.priority;
overrides.metadata.defaults.tags = answers.tags;
return overrides;
}
async function interactiveTemplateMenu() {
const choices = [
{ name: 'š List all templates', value: 'list' },
{ name: 'šļø Show template details', value: 'show' },
{ name: 'šÆ Generate from template', value: 'generate' },
{ name: 'ā Exit', value: 'exit' }
];
const { action } = await inquirer.prompt([{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices
}]);
if (action === 'exit') {
return;
}
switch (action) {
case 'list':
await listTemplates({});
break;
case 'show':
await showTemplate({});
break;
case 'generate':
await generateFromTemplate({});
break;
}
}
function printTemplateStructure(template) {
if (template.metadata) {
console.log(chalk.cyan(' Metadata:'));
if (template.metadata.areaPath) {
console.log(` Area Path: ${chalk.yellow(template.metadata.areaPath)}`);
}
if (template.metadata.iterationPath) {
console.log(` Iteration: ${chalk.yellow(template.metadata.iterationPath)}`);
}
if (template.metadata.defaults) {
const defaults = Object.entries(template.metadata.defaults)
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
console.log(` Defaults: ${chalk.yellow(defaults)}`);
}
}
if (template.workItems) {
console.log(chalk.cyan(' Work Items:'));
printWorkItemStructure(template.workItems, 2);
}
}
function printWorkItemStructure(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, 50) : 'No title';
const truncated = item.title && item.title.length > 50 ? '...' : '';
console.log(`${indent}${chalk.gray(`${index + 1}.`)} ${typeColor(item.type)}: ${chalk.white(title)}${chalk.gray(truncated)}`);
if (item.children && item.children.length > 0) {
printWorkItemStructure(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;
}