md-linear-sync
Version:
Sync Linear tickets to local markdown files with status-based folder organization
208 lines ⢠8.27 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateCommand = validateCommand;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
async function validateCommand(filePath, options) {
try {
const result = await validateFile(filePath);
if (options.json) {
console.log(JSON.stringify(result, null, 2));
}
else {
outputHumanReadable(result, filePath);
}
// Exit with error code if validation failed
if (!result.valid) {
process.exit(1);
}
}
catch (error) {
const errorResult = {
valid: false,
errors: [error instanceof Error ? error.message : 'Unknown error'],
warnings: []
};
if (options.json) {
console.log(JSON.stringify(errorResult, null, 2));
}
else {
console.error('ā Validation failed:', errorResult.errors[0]);
}
process.exit(1);
}
}
async function validateFile(filePath) {
const errors = [];
const warnings = [];
// Check if file exists
try {
await promises_1.default.access(filePath);
}
catch {
throw new Error(`File not found: ${filePath}`);
}
// Check file extension
if (!filePath.endsWith('.md')) {
errors.push('File must have .md extension');
}
// Read file content
const content = await promises_1.default.readFile(filePath, 'utf-8');
// Load configuration for validation context
let config;
try {
config = await config_1.ConfigManager.loadConfig();
}
catch (error) {
throw new Error('No .linear-sync.json found. Run "md-linear-sync init" first.');
}
// Parse frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
errors.push('File must have YAML frontmatter section');
return { valid: false, errors, warnings };
}
const frontmatter = frontmatterMatch[1];
const metadata = {};
// Parse YAML-like frontmatter manually (simple parsing)
const lines = frontmatter.split('\n');
for (const line of lines) {
const match = line.match(/^(\w+):\s*(.+)$/);
if (match) {
const [, key, value] = match;
// Handle different value types
if (key === 'labels') {
// Parse array format: [label1, label2] or ['label1', 'label2']
const arrayMatch = value.match(/^\[(.*)\]$/);
if (arrayMatch) {
metadata[key] = arrayMatch[1]
.split(',')
.map(label => label.trim().replace(/['"`]/g, ''))
.filter(label => label.length > 0);
}
else {
metadata[key] = [];
}
}
else if (key === 'priority') {
metadata[key] = parseInt(value, 10);
}
else {
metadata[key] = value.trim().replace(/['"`]/g, '');
}
}
}
// Validate required fields
if (!metadata.title || metadata.title.trim() === '') {
errors.push('Title is required');
}
// Validate status against available workflow states
if (metadata.status) {
if (!config.statusMapping[metadata.status]) {
errors.push(`Invalid status "${metadata.status}". Available statuses: ${Object.keys(config.statusMapping).join(', ')}`);
}
}
else {
warnings.push('No status specified - ticket will be created with default status');
}
// Validate priority
if (metadata.priority !== undefined) {
if (!Number.isInteger(metadata.priority) || metadata.priority < 0 || metadata.priority > 4) {
errors.push('Priority must be an integer between 0-4 (0=No priority, 1=Urgent, 2=High, 3=Normal, 4=Low)');
}
}
// Validate labels against team labels
if (metadata.labels && Array.isArray(metadata.labels)) {
const availableLabels = Object.keys(config.labelMapping);
const invalidLabels = metadata.labels.filter((label) => !availableLabels.includes(label));
if (invalidLabels.length > 0) {
errors.push(`Invalid labels: ${invalidLabels.join(', ')}. Available labels: ${availableLabels.join(', ')}`);
}
}
// Validate parent_id format (if provided)
if (metadata.parent_id) {
// Should be either a Linear ticket ID (like PAP-123) or a file path
const linearIdPattern = /^[A-Z]+-\d+$/;
const isLinearId = linearIdPattern.test(metadata.parent_id);
const isFilePath = metadata.parent_id.includes('/') || metadata.parent_id.endsWith('.md');
if (!isLinearId && !isFilePath) {
errors.push('parent_id must be either a Linear ticket ID (e.g., PAP-123) or a file path (e.g., path/to/file.md)');
}
// If it's a file path, check if file exists
if (isFilePath) {
try {
const parentPath = path_1.default.resolve(path_1.default.dirname(filePath), metadata.parent_id);
await promises_1.default.access(parentPath);
}
catch {
warnings.push(`Parent file "${metadata.parent_id}" not found - ticket will be created without parent`);
}
}
}
// Check for body content
const bodyContent = content.replace(/^---[\s\S]*?---\n/, '').trim();
if (!bodyContent) {
warnings.push('No body content found - ticket will be created with minimal description');
}
else {
// Check for ticket ID pattern in H1 heading at the start
const firstLineMatch = bodyContent.match(/^#\s+(.+)/);
if (firstLineMatch) {
const heading = firstLineMatch[1];
// Look for patterns like -XXX, .123, -ABC, .789 etc. that suggest a ticket ID
const ticketIdPattern = /[-.]([A-Z]{2,}|\d{2,})/;
if (ticketIdPattern.test(heading)) {
errors.push('H1 heading contains what looks like a ticket ID pattern (e.g., -XXX, .123). ' +
'Do not include ticket IDs in headings at the start of the description.\n' +
'\n' +
'ā Wrong: # PAP-515.XXX: Troubleshoot Issues\n' +
'ā
Correct: ## Summary\n' +
'\n' +
'Just start with your content directly without an H1 heading that duplicates the ticket title.');
}
}
}
return {
valid: errors.length === 0,
errors,
warnings,
metadata
};
}
function outputHumanReadable(result, filePath) {
console.log(`\nš Validation Results for: ${filePath}`);
if (result.valid) {
console.log('ā
File is valid for ticket creation');
}
else {
console.log('ā File has validation errors');
}
if (result.errors.length > 0) {
console.log('\nšØ Errors:');
result.errors.forEach(error => console.log(` ⢠${error}`));
}
if (result.warnings.length > 0) {
console.log('\nā ļø Warnings:');
result.warnings.forEach(warning => console.log(` ⢠${warning}`));
}
if (result.metadata) {
console.log('\nš Parsed Metadata:');
if (result.metadata.title)
console.log(` Title: ${result.metadata.title}`);
if (result.metadata.status)
console.log(` Status: ${result.metadata.status}`);
if (result.metadata.priority !== undefined)
console.log(` Priority: ${result.metadata.priority}`);
if (result.metadata.labels && result.metadata.labels.length > 0) {
console.log(` Labels: ${result.metadata.labels.join(', ')}`);
}
if (result.metadata.parent_id)
console.log(` Parent: ${result.metadata.parent_id}`);
}
console.log('');
}
//# sourceMappingURL=validate.js.map