From a8a986a3fb6aef5c18484bc6c6bd1befa8ba692a Mon Sep 17 00:00:00 2001 From: "heecheol.park" Date: Thu, 11 Dec 2025 14:15:50 +0900 Subject: [PATCH 1/2] feat: add markdown export support for issues - Add formatIssueAsMarkdown() function to lib/utils.js - Add --format and --output options to issue view command - Support viewing issues as markdown in terminal with --format markdown - Support exporting issues to markdown files with --output - Update README with markdown export examples and features - Update roadmap to reflect completed markdown support --- README.md | 31 +++++++++++++++++------------- bin/commands/issue.js | 35 ++++++++++++++++++++++++++-------- lib/utils.js | 44 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 5231e61..ea4513b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A modern, extensible command-line interface for Atlassian JIRA built with Factor ## ✨ Features - 📋 **Issue Management**: Create, read, update, and delete JIRA issues with full CRUD operations +- 📝 **Markdown Support**: Export issues to markdown files and create/update issues from markdown - 📊 **Project Information**: View project details, statistics, and team insights - 🏃 **Sprint Management**: Monitor sprint progress, burndown charts, and team velocity - ⚙️ **Smart Configuration**: Environment variables and CLI options for flexible setup @@ -139,14 +140,17 @@ export JIRA_API_TOKEN="your-api-token" ### Read an Issue ```bash -# Read by issue key -jira issue --get PROJ-123 +# View in terminal +jira issue view PROJ-123 + +# View as markdown in terminal +jira issue view PROJ-123 --format markdown -# Get issue with full details -jira issue --get PROJ-123 --verbose +# Export to markdown file +jira issue view PROJ-123 --output ./issue.md -# Get issue in JSON format -jira issue --get PROJ-123 --format json +# Export with explicit markdown format +jira issue view PROJ-123 --format markdown --output ./issue.md ``` ### List Issues @@ -247,7 +251,7 @@ jira sprint list --board 123 --state active | `config --server --token ` | Configure CLI (Bearer auth) | Username optional; use `--username ` for Basic auth | | `config --show` | Show current configuration | - | | `config set ` | Set individual config value | - | -| `issue get ` | Get issue details | `--format `, `--verbose` | +| `issue view ` | View issue details (alias: show) | `--format `, `--output ` | | `issue list` | List issues | `--project `, `--assignee `, `--status `, `--jql `, `--limit ` | | `issue create` | Create new issue | **Required:** `--project `, `--type `, `--summary `
**Optional:** `--description `, `--description-file `, `--assignee `, `--priority ` | | `issue edit ` | Edit an existing issue (alias: update) | **At least one required:**
`--summary `, `--description `, `--description-file `, `--assignee `, `--priority ` | @@ -280,14 +284,14 @@ jira config --server https://jira.company.com \ --username user@company.com \ --token your-api-token -# Read an issue +# View an issue in terminal jira issue view PROJ-123 -# Read an issue with full details -jira issue view PROJ-123 --verbose +# View as markdown in terminal +jira issue view PROJ-123 --format markdown -# Get issue in JSON format -jira issue view PROJ-123 --format json +# Export to markdown file +jira issue view PROJ-123 --output ./issue.md # List issues with filters jira issue list --project PROJ --status "In Progress" --limit 10 @@ -467,9 +471,10 @@ This project is licensed under the ISC License - see the [LICENSE](https://githu - [x] Configuration management - [x] Non-interactive, automation-friendly CLI - [x] Analytics and reporting +- [x] Export issues to markdown format +- [x] Create/update issues from markdown files - [ ] Issue templates - [ ] Bulk operations -- [ ] Export issues to different formats - [ ] Integration with other Atlassian tools - [ ] Issue attachments management - [ ] Comments and workflows diff --git a/bin/commands/issue.js b/bin/commands/issue.js index 63b92f6..62a51d5 100644 --- a/bin/commands/issue.js +++ b/bin/commands/issue.js @@ -2,6 +2,7 @@ const { Command } = require('commander'); const { createIssuesTable, displayIssueDetails, + formatIssueAsMarkdown, buildJQL } = require('../../lib/utils'); const chalk = require('chalk'); @@ -47,14 +48,21 @@ function createIssueCommand(factory) { command .command('view ') - .description('view issue details') + .description('view issue details\n\n' + + 'Examples:\n' + + ' $ jira issue view PROJ-123 # View in terminal\n' + + ' $ jira issue view PROJ-123 --format markdown # View as markdown\n' + + ' $ jira issue view PROJ-123 --output ./issue.md # Save to file\n' + + ' $ jira issue view PROJ-123 --format markdown --output ./issue.md') .alias('show') - .action(async (key) => { + .option('--format ', 'output format (terminal, markdown)', 'terminal') + .option('--output ', 'save to file instead of displaying') + .action(async (key, options) => { const io = factory.getIOStreams(); const client = await factory.getJiraClient(); - + try { - await getIssue(client, io, key); + await getIssue(client, io, key, options); } catch (err) { io.error(`Failed to get issue: ${err.message}`); process.exit(1); @@ -188,14 +196,25 @@ async function listIssues(client, io, options) { } } -async function getIssue(client, io, issueKey) { +async function getIssue(client, io, issueKey, options = {}) { const spinner = io.spinner(`Fetching issue ${issueKey}...`); - + try { const issue = await client.getIssue(issueKey); spinner.stop(); - - displayIssueDetails(issue); + + if (options.output) { + const outputPath = path.resolve(options.output); + const content = formatIssueAsMarkdown(issue); + + fs.writeFileSync(outputPath, content, 'utf8'); + io.success(`Issue ${issueKey} saved to ${outputPath}`); + } else if (options.format === 'markdown') { + const markdown = formatIssueAsMarkdown(issue); + io.out('\n' + markdown); + } else { + displayIssueDetails(issue); + } } catch (err) { spinner.stop(); diff --git a/lib/utils.js b/lib/utils.js index 3c3abda..bb09a01 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -107,11 +107,46 @@ function createSprintsTable(sprints) { return table; } +// Format issue as markdown +function formatIssueAsMarkdown(issue) { + const lines = []; + + lines.push(`# ${issue.key}: ${issue.fields.summary}`); + lines.push(''); + + lines.push('## Metadata'); + lines.push(''); + lines.push(`- **Status**: ${issue.fields.status.name}`); + lines.push(`- **Type**: ${issue.fields.issuetype.name}`); + lines.push(`- **Priority**: ${issue.fields.priority ? issue.fields.priority.name : 'N/A'}`); + lines.push(`- **Assignee**: ${issue.fields.assignee ? issue.fields.assignee.displayName : 'Unassigned'}`); + lines.push(`- **Reporter**: ${issue.fields.reporter ? issue.fields.reporter.displayName : 'N/A'}`); + lines.push(`- **Created**: ${formatDate(issue.fields.created)}`); + lines.push(`- **Updated**: ${formatDate(issue.fields.updated)}`); + + if (issue.fields.labels && issue.fields.labels.length > 0) { + lines.push(`- **Labels**: ${issue.fields.labels.join(', ')}`); + } + + const url = issue.self.replace('/rest/api/2/issue/' + issue.id, '/browse/' + issue.key); + lines.push(`- **URL**: ${url}`); + lines.push(''); + + if (issue.fields.description) { + lines.push('## Description'); + lines.push(''); + lines.push(issue.fields.description); + lines.push(''); + } + + return lines.join('\n'); +} + // Display issue details function displayIssueDetails(issue) { console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`)); console.log(chalk.gray('─'.repeat(60))); - + console.log(`${chalk.bold('Status:')} ${chalk.yellow(issue.fields.status.name)}`); console.log(`${chalk.bold('Type:')} ${issue.fields.issuetype.name}`); console.log(`${chalk.bold('Priority:')} ${issue.fields.priority ? issue.fields.priority.name : 'N/A'}`); @@ -119,16 +154,16 @@ function displayIssueDetails(issue) { console.log(`${chalk.bold('Reporter:')} ${issue.fields.reporter ? issue.fields.reporter.displayName : 'N/A'}`); console.log(`${chalk.bold('Created:')} ${formatDate(issue.fields.created)}`); console.log(`${chalk.bold('Updated:')} ${formatDate(issue.fields.updated)}`); - + if (issue.fields.description) { console.log(`\n${chalk.bold('Description:')}`); console.log(issue.fields.description); } - + if (issue.fields.labels && issue.fields.labels.length > 0) { console.log(`\n${chalk.bold('Labels:')} ${issue.fields.labels.join(', ')}`); } - + console.log(`\n${chalk.bold('URL:')} ${issue.self.replace('/rest/api/2/issue/' + issue.id, '/browse/' + issue.key)}`); } @@ -181,6 +216,7 @@ module.exports = { createProjectsTable, createSprintsTable, displayIssueDetails, + formatIssueAsMarkdown, buildJQL, success, error, From 91445de3c1735436cd5dfc6239c4b284fcaede58 Mon Sep 17 00:00:00 2001 From: "heecheol.park" Date: Thu, 11 Dec 2025 14:20:05 +0900 Subject: [PATCH 2/2] refactor: use template literals for URL construction Replace string concatenation with template literals for better readability --- lib/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils.js b/lib/utils.js index bb09a01..7262cab 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -128,7 +128,7 @@ function formatIssueAsMarkdown(issue) { lines.push(`- **Labels**: ${issue.fields.labels.join(', ')}`); } - const url = issue.self.replace('/rest/api/2/issue/' + issue.id, '/browse/' + issue.key); + const url = issue.self.replace(`/rest/api/2/issue/${issue.id}`, `/browse/${issue.key}`); lines.push(`- **URL**: ${url}`); lines.push('');