// Version management operations for task-mcp import { query, queryOne, execute, getProjectKey } from '../db.js'; import type { Version, Task } from '../types.js'; interface VersionAddArgs { project: string; version: string; build_number?: number; status?: string; release_notes?: string; } interface VersionListArgs { project?: string; status?: string; limit?: number; } interface VersionUpdateArgs { id: string; status?: string; git_tag?: string; git_sha?: string; release_notes?: string; release_date?: string; } /** * Generate version ID from project and version number */ function generateVersionId(projectKey: string, version: string): string { return `${projectKey}-v${version.replace(/^v/, '')}`; } /** * Create a new version */ export async function versionAdd(args: VersionAddArgs): Promise { const { project, version, build_number, status = 'planned', release_notes } = args; // Get project key const projectKey = await getProjectKey(project); // Generate version ID const versionId = generateVersionId(projectKey, version); // Check if version already exists const existing = await queryOne<{ id: string }>(`SELECT id FROM versions WHERE id = $1`, [versionId]); if (existing) { return `Version already exists: ${versionId}`; } // Insert version await execute( `INSERT INTO versions (id, project, version, build_number, status, release_notes) VALUES ($1, $2, $3, $4, $5, $6)`, [versionId, projectKey, version, build_number || null, status, release_notes || null] ); return `Created version: ${versionId}\n Version: ${version}\n Project: ${projectKey}\n Status: ${status}${build_number ? `\n Build: ${build_number}` : ''}`; } /** * List versions with filters */ export async function versionList(args: VersionListArgs): Promise { const { project, status, limit = 20 } = args; let whereClause = 'WHERE 1=1'; const params: unknown[] = []; let paramIndex = 1; if (project) { const projectKey = await getProjectKey(project); whereClause += ` AND v.project = $${paramIndex++}`; params.push(projectKey); } if (status) { whereClause += ` AND v.status = $${paramIndex++}`; params.push(status); } params.push(limit); const versions = await query( `SELECT v.id, v.version, v.status, v.project, v.build_number, v.git_tag, to_char(v.release_date, 'YYYY-MM-DD') as release_date, COUNT(t.id) as task_count, COUNT(t.id) FILTER (WHERE t.status != 'completed') as open_count FROM versions v LEFT JOIN tasks t ON t.version_id = v.id ${whereClause} GROUP BY v.id, v.version, v.status, v.project, v.build_number, v.git_tag, v.release_date, v.created_at ORDER BY CASE v.status WHEN 'in_progress' THEN 0 WHEN 'planned' THEN 1 WHEN 'released' THEN 2 ELSE 3 END, v.created_at DESC LIMIT $${paramIndex}`, params ); if (versions.length === 0) { return `No versions found${project ? ` for project ${project}` : ''}`; } const lines = versions.map(v => { const statusIcon = v.status === 'released' ? '[R]' : v.status === 'in_progress' ? '[>]' : v.status === 'archived' ? '[A]' : '[ ]'; const progress = v.task_count > 0 ? ` (${v.task_count - v.open_count}/${v.task_count} tasks)` : ''; const tag = v.git_tag ? ` [${v.git_tag}]` : ''; const date = (v as unknown as { release_date: string }).release_date ? ` - ${(v as unknown as { release_date: string }).release_date}` : ''; return `${statusIcon} ${v.id}: ${v.version}${tag}${progress}${date}`; }); return `Versions${project ? ` (${project})` : ''}:\n\n${lines.join('\n')}`; } /** * Show version details with assigned tasks */ export async function versionShow(id: string): Promise { const version = await queryOne( `SELECT id, project, version, build_number, status, release_notes, git_tag, git_sha, to_char(created_at, 'YYYY-MM-DD HH24:MI') as created, to_char(release_date, 'YYYY-MM-DD') as released FROM versions WHERE id = $1`, [id] ); if (!version) { return `Version not found: ${id}`; } let output = `# ${version.id}\n\n`; output += `**Version:** ${version.version}\n`; output += `**Project:** ${version.project}\n`; output += `**Status:** ${version.status}\n`; if (version.build_number) { output += `**Build:** ${version.build_number}\n`; } if ((version as unknown as { git_tag: string }).git_tag) { output += `**Git Tag:** ${(version as unknown as { git_tag: string }).git_tag}\n`; } if ((version as unknown as { git_sha: string }).git_sha) { output += `**Git SHA:** ${(version as unknown as { git_sha: string }).git_sha}\n`; } output += `**Created:** ${version.created}\n`; if (version.released) { output += `**Released:** ${version.released}\n`; } if (version.release_notes) { output += `\n**Release Notes:**\n${version.release_notes}\n`; } // Get tasks assigned to this version const tasks = await query( `SELECT id, title, status, priority, type FROM tasks WHERE version_id = $1 ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'open' THEN 1 WHEN 'blocked' THEN 2 ELSE 3 END, CASE priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END`, [id] ); if (tasks.length > 0) { const done = tasks.filter(t => t.status === 'completed').length; output += `\n**Tasks:** (${done}/${tasks.length} done)\n`; for (const t of tasks) { const statusIcon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[>]' : t.status === 'blocked' ? '[!]' : '[ ]'; output += ` ${statusIcon} ${t.priority} ${t.id}: ${t.title}\n`; } } else { output += `\n**Tasks:** None assigned\n`; } // Get epics targeting this version const epics = await query<{ id: string; title: string; status: string }>( `SELECT id, title, status FROM epics WHERE target_version_id = $1`, [id] ); if (epics.length > 0) { output += `\n**Epics:**\n`; for (const e of epics) { const statusIcon = e.status === 'completed' ? '[x]' : e.status === 'in_progress' ? '[>]' : '[ ]'; output += ` ${statusIcon} ${e.id}: ${e.title}\n`; } } return output; } /** * Update a version */ export async function versionUpdate(args: VersionUpdateArgs): Promise { const { id, status, git_tag, git_sha, release_notes, release_date } = args; const updates: string[] = []; const params: unknown[] = []; let paramIndex = 1; if (status) { updates.push(`status = $${paramIndex++}`); params.push(status); } if (git_tag !== undefined) { updates.push(`git_tag = $${paramIndex++}`); params.push(git_tag); } if (git_sha !== undefined) { updates.push(`git_sha = $${paramIndex++}`); params.push(git_sha); } if (release_notes !== undefined) { updates.push(`release_notes = $${paramIndex++}`); params.push(release_notes); } if (release_date) { updates.push(`release_date = $${paramIndex++}`); params.push(release_date); } if (updates.length === 0) { return 'No updates specified'; } params.push(id); const result = await execute( `UPDATE versions SET ${updates.join(', ')} WHERE id = $${paramIndex}`, params ); if (result === 0) { return `Version not found: ${id}`; } return `Updated: ${id}`; } /** * Mark a version as released */ export async function versionRelease(args: { id: string; git_tag?: string }): Promise { const { id, git_tag } = args; // Verify version exists const version = await queryOne<{ id: string; status: string; version: string }>( `SELECT id, status, version FROM versions WHERE id = $1`, [id] ); if (!version) { return `Version not found: ${id}`; } if (version.status === 'released') { return `Version already released: ${id}`; } // Update version status const updates = ['status = $1', 'release_date = NOW()']; const params: unknown[] = ['released']; let paramIndex = 2; if (git_tag) { updates.push(`git_tag = $${paramIndex++}`); params.push(git_tag); } params.push(id); await execute( `UPDATE versions SET ${updates.join(', ')} WHERE id = $${paramIndex}`, params ); return `Released: ${id} (${version.version})${git_tag ? ` tagged as ${git_tag}` : ''}`; } /** * Assign a task to a version */ export async function versionAssignTask(args: { task_id: string; version_id: string }): Promise { const { task_id, version_id } = args; // Verify version exists const version = await queryOne<{ id: string }>(`SELECT id FROM versions WHERE id = $1`, [version_id]); if (!version) { return `Version not found: ${version_id}`; } // Update task const result = await execute( `UPDATE tasks SET version_id = $1, updated_at = NOW() WHERE id = $2`, [version_id, task_id] ); if (result === 0) { return `Task not found: ${task_id}`; } return `Assigned ${task_id} to version ${version_id}`; }