Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- πŸ’¬ **Comment Management**: Add, list, edit, and delete comments on issues with file support
- πŸ“ **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
Expand Down Expand Up @@ -218,6 +219,44 @@ jira issue list --jql "project = PROJ AND status = 'In Progress'"
jira issue list --jql "bug" --limit 5
```

### Manage Comments

```bash
# Add a comment to an issue
jira issue comment add PROJ-123 "Review completed"

# Add multi-line comment
jira issue comment add PROJ-123 "Build status:
- Unit tests: βœ“
- Integration tests: βœ“
- Deployment: pending"

# Add comment from file
jira issue comment add PROJ-123 --file ./review-notes.md

# Add internal comment (visible only to team)
jira issue comment add PROJ-123 "Internal note" --internal

# List all comments on an issue
jira issue comment list PROJ-123

# List comments in JSON format
jira issue comment list PROJ-123 --format json

# Edit an existing comment
jira issue comment edit 12345 "Updated comment text"

# Edit comment from file
jira issue comment edit 12345 --file ./updated-notes.md

# Delete a comment (requires confirmation)
jira issue comment delete 12345 --force

# Using command alias
jira issue c add PROJ-123 "Quick comment"
jira issue c list PROJ-123
```

### Project Management

```bash
Expand Down Expand Up @@ -256,6 +295,10 @@ jira sprint list --board 123 --state active
| `issue create` | Create new issue | **Required:** `--project <key>`, `--type <type>`, `--summary <text>`<br>**Optional:** `--description <text>`, `--description-file <path>`, `--assignee <user>`, `--priority <level>` |
| `issue edit <key>` | Edit an existing issue (alias: update) | **At least one required:**<br>`--summary <text>`, `--description <text>`, `--description-file <path>`, `--assignee <user>`, `--priority <level>` |
| `issue delete <key>` | Delete issue | **Required:** `--force` |
| `issue comment add <key> [text]` | Add comment to issue (alias: c) | `[text]` or `--file <path>`<br>**Optional:** `--internal` |
| `issue comment list <key>` | List comments on issue | `--format <table\|json>` (default: table) |
| `issue comment edit <id> [text]` | Edit existing comment | `[text]` or `--file <path>` |
| `issue comment delete <id>` | Delete comment | **Required:** `--force` |
| `project list` | List all projects | `--type <type>`, `--category <category>` |
| `project view <key>` | View project details | - |
| `project components <key>` | List project components | - |
Expand Down Expand Up @@ -310,6 +353,21 @@ jira issue edit PROJ-123 --summary "Updated summary"
# Delete issue (requires --force)
jira issue delete PROJ-123 --force

# Add comment to an issue
jira issue comment add PROJ-123 "Review completed"

# Add comment from file
jira issue comment add PROJ-123 --file ./review-notes.md

# List all comments
jira issue comment list PROJ-123

# Edit a comment
jira issue comment edit 12345 "Updated comment"

# Delete a comment
jira issue comment delete 12345 --force

# List all projects
jira project list

Expand Down Expand Up @@ -467,6 +525,7 @@ This project is licensed under the ISC License - see the [LICENSE](https://githu
## Roadmap

- [x] Basic issue management (create, read, update, delete)
- [x] Comment management (add, list, edit, delete)
- [x] Project and sprint management
- [x] Configuration management
- [x] Non-interactive, automation-friendly CLI
Expand All @@ -477,7 +536,7 @@ This project is licensed under the ISC License - see the [LICENSE](https://githu
- [ ] Bulk operations
- [ ] Integration with other Atlassian tools
- [ ] Issue attachments management
- [ ] Comments and workflows
- [ ] Workflows and transitions
- [ ] Custom fields support
- [ ] Time tracking

Expand Down
199 changes: 198 additions & 1 deletion bin/commands/issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ const {
createIssuesTable,
displayIssueDetails,
formatIssueAsMarkdown,
buildJQL
buildJQL,
createCommentsTable
} = require('../../lib/utils');
const chalk = require('chalk');
const fs = require('fs');
Expand Down Expand Up @@ -141,6 +142,89 @@ function createIssueCommand(factory) {
}
});

// Comment subcommand
const commentCommand = command
.command('comment')
.description('Manage issue comments')
.alias('c');

commentCommand
.command('add <key> [text]')
.description('add a comment to an issue\n\n' +
'Examples:\n' +
' $ jira issue comment add PROJ-123 "Review completed"\n' +
' $ jira issue comment add PROJ-123 --file ./notes.md\n' +
' $ jira issue comment add PROJ-123 "Internal note" --internal')
.option('--file <path>', 'read comment body from file')
.option('--internal', 'mark comment as internal/private')
.action(async (key, text, options) => {
const io = factory.getIOStreams();
const client = await factory.getJiraClient();

try {
await addComment(client, io, key, text, options);
} catch (err) {
io.error(`Failed to add comment: ${err.message}`);
process.exit(1);
}
});

commentCommand
.command('list <key>')
.description('list comments on an issue\n\n' +
'Examples:\n' +
' $ jira issue comment list PROJ-123\n' +
' $ jira issue comment list PROJ-123 --format json')
.option('--format <format>', 'output format (table, json)', 'table')
.action(async (key, options) => {
const io = factory.getIOStreams();
const client = await factory.getJiraClient();

try {
await listComments(client, io, key, options);
} catch (err) {
io.error(`Failed to list comments: ${err.message}`);
process.exit(1);
}
});

commentCommand
.command('edit <commentId> [text]')
.description('edit an existing comment\n\n' +
'Examples:\n' +
' $ jira issue comment edit 12345 "Updated comment"\n' +
' $ jira issue comment edit 12345 --file ./updated-notes.md')
.option('--file <path>', 'read comment body from file')
.action(async (commentId, text, options) => {
const io = factory.getIOStreams();
const client = await factory.getJiraClient();

try {
await editComment(client, io, commentId, text, options);
} catch (err) {
io.error(`Failed to edit comment: ${err.message}`);
process.exit(1);
}
});

commentCommand
.command('delete <commentId>')
.description('delete a comment\n\n' +
'Examples:\n' +
' $ jira issue comment delete 12345 --force')
.option('-f, --force', 'force delete without confirmation')
.action(async (commentId, options) => {
const io = factory.getIOStreams();
const client = await factory.getJiraClient();

try {
await deleteComment(client, io, commentId, options);
} catch (err) {
io.error(`Failed to delete comment: ${err.message}`);
process.exit(1);
}
});

return command;
}

Expand Down Expand Up @@ -379,4 +463,117 @@ async function deleteIssue(client, io, issueKey, options = {}) {
io.success(`Issue ${issueKey} deleted successfully`);
}

async function addComment(client, io, issueKey, text, options = {}) {
// Validate that we have comment text from either argument or file
if (!text && !options.file) {
throw new Error(
'Comment text is required.\n\n' +
'Usage: jira issue comment add <KEY> [text] [options]\n\n' +
'Provide comment text either as:\n' +
' - Direct argument: jira issue comment add PROJ-123 "Comment text"\n' +
' - From file: jira issue comment add PROJ-123 --file ./comment.md\n\n' +
'Options:\n' +
' --file <path> Read comment body from file\n' +
' --internal Mark comment as internal/private'
);
}

if (text && options.file) {
throw new Error('Cannot use both text argument and --file option. Please use only one.');
}

let commentBody = text;
if (options.file) {
commentBody = readDescriptionFile(options.file);
}

const spinner = io.spinner(`Adding comment to ${issueKey}...`);
const comment = await client.addComment(issueKey, commentBody, {
internal: options.internal
});
spinner.stop();

io.success(`Comment added to ${issueKey}`);
io.out(`Comment ID: ${comment.id}`);

if (options.internal) {
io.info('Comment marked as internal');
}
}

async function listComments(client, io, issueKey, options = {}) {
const spinner = io.spinner(`Fetching comments for ${issueKey}...`);

try {
const result = await client.getComments(issueKey);
spinner.stop();

if (!result.comments || result.comments.length === 0) {
io.info(`No comments found for ${issueKey}`);
return;
}

io.out(chalk.bold(`\nComments for ${issueKey} (${result.comments.length} total):\n`));

if (options.format === 'json') {
io.out(JSON.stringify(result.comments, null, 2));
} else {
const table = createCommentsTable(result.comments);
io.out(table.toString());
}

} catch (err) {
spinner.stop();
throw err;
}
}

async function editComment(client, io, commentId, text, options = {}) {
// Validate that we have comment text from either argument or file
if (!text && !options.file) {
throw new Error(
'Comment text is required.\n\n' +
'Usage: jira issue comment edit <COMMENT-ID> [text] [options]\n\n' +
'Provide comment text either as:\n' +
' - Direct argument: jira issue comment edit 12345 "Updated text"\n' +
' - From file: jira issue comment edit 12345 --file ./updated.md\n\n' +
'Options:\n' +
' --file <path> Read comment body from file'
);
}

if (text && options.file) {
throw new Error('Cannot use both text argument and --file option. Please use only one.');
}

let commentBody = text;
if (options.file) {
commentBody = readDescriptionFile(options.file);
}

const spinner = io.spinner(`Updating comment ${commentId}...`);
await client.updateComment(commentId, commentBody);
spinner.stop();

io.success(`Comment ${commentId} updated successfully`);
}

async function deleteComment(client, io, commentId, options = {}) {
io.out(chalk.bold.red('\nWARNING: You are about to delete this comment:'));
io.out(` Comment ID: ${chalk.cyan(commentId)}\n`);

if (!options.force) {
throw new Error(
'Deletion requires --force flag to confirm.\n' +
`Use: jira issue comment delete ${commentId} --force`
);
}

const spinner = io.spinner('Deleting comment...');
await client.deleteComment(commentId);
spinner.stop();

io.success(`Comment ${commentId} deleted successfully`);
}

module.exports = createIssueCommand;
37 changes: 37 additions & 0 deletions lib/jira-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,43 @@ class JiraClient {
});
return response.data;
}

// Comments
async getComments(issueKey) {
const response = await this.client.get(`/issue/${issueKey}/comment`);
return response.data;
}

async addComment(issueKey, body, options = {}) {
const commentData = {
body: body
};

// Add visibility if internal flag is set
if (options.internal) {
commentData.visibility = {
type: 'role',
value: 'Administrators'
};
}

const response = await this.client.post(`/issue/${issueKey}/comment`, commentData);
return response.data;
}

async updateComment(commentId, body) {
const commentData = {
body: body
};

const response = await this.client.put(`/comment/${commentId}`, commentData);
return response.data;
}

async deleteComment(commentId) {
await this.client.delete(`/comment/${commentId}`);
return true;
}
}

module.exports = JiraClient;
Loading
Loading