#!/usr/bin/env npx tsx /** * Jira admin helper for migration (CF-762) * Usage: * npx tsx scripts/jira-admin.ts get-project CF * npx tsx scripts/jira-admin.ts delete-project CF * npx tsx scripts/jira-admin.ts create-project CF "Claude Framework" * npx tsx scripts/jira-admin.ts count-issues CF * npx tsx scripts/jira-admin.ts delete-all-issues CF */ import dotenv from 'dotenv'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: join(__dirname, '..', '.env'), override: true }); const JIRA_URL = process.env.JIRA_URL || 'https://agiliton.atlassian.net'; const JIRA_USER = process.env.JIRA_USERNAME || process.env.JIRA_EMAIL || ''; const JIRA_TOKEN = process.env.JIRA_API_TOKEN || ''; const JIRA_AUTH = Buffer.from(`${JIRA_USER}:${JIRA_TOKEN}`).toString('base64'); async function jiraFetch(path: string, options: RequestInit = {}): Promise { const url = path.startsWith('http') ? path : `${JIRA_URL}/rest/api/3${path}`; return fetch(url, { ...options, headers: { 'Authorization': `Basic ${JIRA_AUTH}`, 'Content-Type': 'application/json', 'Accept': 'application/json', ...options.headers, }, }); } function delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } const [command, ...cmdArgs] = process.argv.slice(2); async function main() { switch (command) { case 'get-project': { const key = cmdArgs[0]; const res = await jiraFetch(`/project/${key}`); if (!res.ok) { console.error(`Failed: ${res.status} ${await res.text()}`); return; } const data = await res.json() as Record; console.log(JSON.stringify(data, null, 2)); break; } case 'list-projects': { const res = await jiraFetch('/project'); if (!res.ok) { console.error(`Failed: ${res.status} ${await res.text()}`); return; } const projects = await res.json() as Array<{ key: string; name: string; id: string; projectTypeKey: string }>; console.log(`Total: ${projects.length} projects`); for (const p of projects) { console.log(` ${p.key}: ${p.name} (id=${p.id}, type=${p.projectTypeKey})`); } break; } case 'count-issues': { const key = cmdArgs[0]; const res = await jiraFetch(`/search/jql`, { method: 'POST', body: JSON.stringify({ jql: `project="${key}"`, maxResults: 1 }), }); if (!res.ok) { console.error(`Failed: ${res.status} ${await res.text()}`); return; } const data = await res.json() as { total: number }; console.log(`${key}: ${data.total} issues`); break; } case 'list-issues': { const key = cmdArgs[0]; const max = parseInt(cmdArgs[1] || '20'); const res = await jiraFetch(`/search/jql`, { method: 'POST', body: JSON.stringify({ jql: `project="${key}" ORDER BY key ASC`, maxResults: max, fields: ['key', 'summary', 'issuetype', 'status'] }), }); if (!res.ok) { console.error(`Failed: ${res.status} ${await res.text()}`); return; } const data = await res.json() as { total: number; issues: Array<{ key: string; fields: { summary: string; issuetype: { name: string }; status: { name: string } } }> }; console.log(`${key}: ${data.total} total issues (showing ${data.issues.length})`); for (const i of data.issues) { console.log(` ${i.key} [${i.fields.issuetype.name}] ${i.fields.status.name}: ${i.fields.summary.substring(0, 60)}`); } break; } case 'delete-all-issues': { const key = cmdArgs[0]; if (!key) { console.error('Usage: delete-all-issues '); return; } // Get all issues let startAt = 0; const allKeys: string[] = []; while (true) { const res = await jiraFetch(`/search/jql`, { method: 'POST', body: JSON.stringify({ jql: `project="${key}" ORDER BY key ASC`, maxResults: 100, startAt, fields: ['key'] }), }); if (!res.ok) { console.error(`Failed: ${res.status} ${await res.text()}`); return; } const data = await res.json() as { total: number; issues: Array<{ key: string }> }; if (data.issues.length === 0) break; allKeys.push(...data.issues.map(i => i.key)); startAt += data.issues.length; if (startAt >= data.total) break; } console.log(`Found ${allKeys.length} issues to delete in ${key}`); for (let i = 0; i < allKeys.length; i++) { await delay(300); const res = await jiraFetch(`/issue/${allKeys[i]}`, { method: 'DELETE' }); if (!res.ok) { console.error(` FAIL delete ${allKeys[i]}: ${res.status}`); } if (i % 10 === 0) console.log(` [${i + 1}/${allKeys.length}] Deleted ${allKeys[i]}`); } console.log(`Deleted ${allKeys.length} issues from ${key}`); break; } case 'delete-project': { const key = cmdArgs[0]; if (!key) { console.error('Usage: delete-project '); return; } // enableUndo=false for permanent deletion const res = await jiraFetch(`/project/${key}?enableUndo=false`, { method: 'DELETE' }); if (res.status === 204) { console.log(`Project ${key} deleted permanently`); } else { console.error(`Failed: ${res.status} ${await res.text()}`); } break; } case 'create-project': { const key = cmdArgs[0]; const name = cmdArgs[1] || key; if (!key) { console.error('Usage: create-project '); return; } // Get current user account ID for lead const meRes = await jiraFetch('/myself'); const me = await meRes.json() as { accountId: string }; const body = { key, name, projectTypeKey: 'business', leadAccountId: me.accountId, assigneeType: 'UNASSIGNED', }; const res = await jiraFetch('/project', { method: 'POST', body: JSON.stringify(body), }); if (res.ok || res.status === 201) { const data = await res.json() as { id: string; key: string }; console.log(`Project created: ${data.key} (id=${data.id})`); } else { console.error(`Failed: ${res.status} ${await res.text()}`); } break; } case 'get-schemes': { const key = cmdArgs[0]; // Get issue type scheme for project const res = await jiraFetch(`/project/${key}`); if (!res.ok) { console.error(`Failed: ${res.status} ${await res.text()}`); return; } const data = await res.json() as Record; console.log('Project type:', (data as any).projectTypeKey); console.log('Style:', (data as any).style); // Get issue types const itRes = await jiraFetch(`/project/${key}/statuses`); if (itRes.ok) { const itData = await itRes.json() as Array<{ name: string; id: string; statuses: Array<{ name: string }> }>; console.log('\nIssue types and statuses:'); for (const it of itData) { console.log(` ${it.name} (id=${it.id}): ${it.statuses.map(s => s.name).join(', ')}`); } } break; } default: console.log('Commands: list-projects, get-project, count-issues, list-issues, delete-all-issues, delete-project, create-project, get-schemes'); } } main().catch(err => { console.error(err); process.exit(1); });