Skip to content
Open
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
3 changes: 2 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"Bash(gh issue view :*)",
"Bash(bun test:*)",
"Bash(bun run lint:*)",
"Bash(bun run lint)"
"Bash(bun run lint)",
"WebFetch(domain:cli.github.com)"
],
"deny": [],
"ask": []
Expand Down
109 changes: 109 additions & 0 deletions src/commands/issue/comment-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as readline from 'node:readline'
import { Command } from 'commander'
import { deleteIssueCommentByNodeId } from '../../lib/github'
import { getRepoInfo } from '../../lib/github-api'
import { detectSystemLanguage, getCommentMessages } from '../../lib/i18n'
import { toIssueCommentNodeId, validateCommentIdentifier } from '../../lib/id-converter'

/**
* Prompt user for confirmation
* @param prompt - The prompt message to display
* @returns Promise that resolves to true if user confirms, false otherwise
*/
async function confirm(prompt: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close()
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes')
})
})
}

/**
* Creates a command to delete issue comments
* @returns Command object configured for deleting issue comments
*/
export function createIssueCommentDeleteCommand(): Command {
const command = new Command('delete')

command
.description('Delete an issue comment')
.argument('<comment-id>', 'Database ID (number) or Node ID (IC_...) of the issue comment')
.option('-R, --repo <owner/repo>', 'Repository in owner/repo format')
.option('--issue <number>', 'Issue number (required when using Database ID)')
.option('-y, --yes', 'Skip confirmation prompt')
.action(async (commentIdStr: string, options: { repo?: string, issue?: string, yes?: boolean }) => {
const lang = detectSystemLanguage()
const msg = getCommentMessages(lang)

try {
// Validate comment identifier (Database ID or Node ID)
const commentIdentifier = validateCommentIdentifier(commentIdStr)

// Get repository info
const { owner, repo } = await getRepoInfo(options.repo)

// Convert comment identifier to Node ID
let commentNodeId: string

if (commentIdentifier.startsWith('IC_')) {
// Already a Node ID, use directly
commentNodeId = commentIdentifier
console.log(`✓ Node ID detected, using directly`)
}
else {
// Database ID - need issue number to convert
if (!options.issue) {
throw new Error(
'Issue number is required when using Database ID. '
+ 'Use --issue <number> or provide Node ID instead.',
)
}

const issueNumber = Number.parseInt(options.issue, 10)
if (Number.isNaN(issueNumber)) {
throw new TypeError('Invalid issue number')
}

console.log(`🔄 Converting Database ID to Node ID...`)
Comment on lines +57 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There are several hardcoded English strings in the command's logic. To maintain consistency with the project's internationalization (i18n) approach, these should be moved to the src/lib/i18n.ts file.

Specifically, the following strings are hardcoded:

  • Informational messages for ID detection and conversion.
  • Error messages for missing --issue flag and invalid issue numbers.

I have added separate comments on src/lib/i18n.ts with suggestions to add the necessary keys. Once those are added, please update this file to use the new i18n messages. Remember to also use msg.invalidIssueNumber for the TypeError, as it's already available in CommentMessages.

          console.log(msg.nodeIdDetected)
        }
        else {
          // Database ID - need issue number to convert
          if (!options.issue) {
            throw new Error(msg.issueRequiredForDbId)
          }

          const issueNumber = Number.parseInt(options.issue, 10)
          if (Number.isNaN(issueNumber)) {
            throw new TypeError(msg.invalidIssueNumber)
          }

          console.log(msg.convertingId)

commentNodeId = await toIssueCommentNodeId(
commentIdentifier,
owner,
repo,
issueNumber,
)
}

// Prompt for confirmation unless --yes flag is provided
if (!options.yes) {
const confirmed = await confirm(msg.confirmDelete)
if (!confirmed) {
console.log(msg.deleteAborted)
process.exit(0)
}
}

// Delete comment using GraphQL
console.log(msg.deletingComment(commentIdentifier))
await deleteIssueCommentByNodeId(commentNodeId)

console.log(msg.commentDeleted)
}
catch (error) {
if (error instanceof Error) {
console.error(`${msg.errorPrefix}: ${error.message}`)
}
else {
console.error(msg.unknownError)
}
process.exit(1)
}
})

return command
}
2 changes: 2 additions & 0 deletions src/commands/issue/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command } from 'commander'
import { passThroughCommand } from '../../lib/gh-passthrough'
import { createCleanupCommand } from './cleanup'
import { createIssueCommentDeleteCommand } from './comment-delete'
import { createIssueCommentEditCommand } from './comment-edit'
import { createIssueCommentListCommand } from './comment-list'
import { createIssueCreateCommand } from './create'
Expand All @@ -24,6 +25,7 @@ export function createIssueCommand(): Command {
// Add comment subcommand group
const commentCommand = new Command('comment')
commentCommand.description('Manage issue comments')
commentCommand.addCommand(createIssueCommentDeleteCommand())
commentCommand.addCommand(createIssueCommentEditCommand())
commentCommand.addCommand(createIssueCommentListCommand())
command.addCommand(commentCommand)
Expand Down
1 change: 1 addition & 0 deletions src/lib/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export {
// Review operations
export {
createReviewCommentReply,
deleteIssueCommentByNodeId,
getThreadIdFromComment,
listReviewThreads,
resolveReviewThread,
Expand Down
20 changes: 20 additions & 0 deletions src/lib/github/review-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,23 @@ export async function updateIssueCommentByNodeId(
throw new Error('Failed to update issue comment')
}
}

/**
* Delete an issue comment using Node ID
*
* @param commentNodeId - Node ID of the comment to delete (IC_...)
* @throws Error if the mutation fails
*/
export async function deleteIssueCommentByNodeId(
commentNodeId: string,
): Promise<void> {
const mutation = `
mutation DeleteIssueComment($id: ID!) {
deleteIssueComment(input: { id: $id }) {
clientMutationId
}
}
`

await executeGraphQL(mutation, { id: commentNodeId }, undefined, 'DeleteIssueComment')
}
15 changes: 15 additions & 0 deletions src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export interface CommentMessages {
fetchingComment: (commentId: number) => string
updatingComment: (commentId: number) => string
commentUpdated: string
deletingComment: (commentId: number | string) => string
commentDeleted: string
confirmDelete: string
deleteAborted: string
usageDeleteIssue: string
Comment on lines +91 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To support full internationalization in the new issue comment delete command and remove hardcoded strings from its logic, some additional message keys are needed for informational logs and error handling.

Suggested change
deletingComment: (commentId: number | string) => string
commentDeleted: string
confirmDelete: string
deleteAborted: string
usageDeleteIssue: string
deletingComment: (commentId: number | string) => string
commentDeleted: string
confirmDelete: string
deleteAborted: string
usageDeleteIssue: string
nodeIdDetected: string
convertingId: string
issueRequiredForDbId: string

bodyRequired: string
bodyEmpty: string
usageIssue: string
Expand Down Expand Up @@ -299,6 +304,11 @@ export const commentMessages: Record<Language, CommentMessages> = {
fetchingComment: (commentId: number) => `🔍 댓글 ${commentId} 가져오는 중...`,
updatingComment: (commentId: number) => `📝 댓글 ${commentId} 업데이트 중...`,
commentUpdated: '✅ 댓글이 성공적으로 업데이트되었습니다!',
deletingComment: (commentId: number | string) => `🗑️ 댓글 ${commentId} 삭제 중...`,
commentDeleted: '✅ 댓글이 성공적으로 삭제되었습니다!',
confirmDelete: '정말로 이 댓글을 삭제하시겠습니까? (y/N): ',
deleteAborted: '삭제가 취소되었습니다.',
usageDeleteIssue: ' 사용법: gh please issue comment delete <comment-id> [--yes]',
Comment on lines +307 to +311
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Please add the Korean translations for the new i18n keys to ensure a consistent experience for Korean-speaking users.

Suggested change
deletingComment: (commentId: number | string) => `🗑️ 댓글 ${commentId} 삭제 중...`,
commentDeleted: '✅ 댓글이 성공적으로 삭제되었습니다!',
confirmDelete: '정말로 이 댓글을 삭제하시겠습니까? (y/N): ',
deleteAborted: '삭제가 취소되었습니다.',
usageDeleteIssue: ' 사용법: gh please issue comment delete <comment-id> [--yes]',
deletingComment: (commentId: number | string) => `🗑️ 댓글 ${commentId} 삭제 중...`,
commentDeleted: '✅ 댓글이 성공적으로 삭제되었습니다!',
confirmDelete: '정말로 이 댓글을 삭제하시겠습니까? (y/N): ',
deleteAborted: '삭제가 취소되었습니다.',
usageDeleteIssue: ' 사용법: gh please issue comment delete <comment-id> [--yes]',
nodeIdDetected: '✓ 노드 ID가 감지되어 직접 사용합니다',
convertingId: '🔄 데이터베이스 ID를 노드 ID로 변환 중...',
issueRequiredForDbId: '데이터베이스 ID를 사용할 때는 이슈 번호가 필요합니다. --issue <번호>를 사용하거나 노드 ID를 대신 제공하세요.',

bodyRequired: '❌ 오류: --body 또는 --body-file이 필요합니다',
bodyEmpty: '❌ 오류: 댓글 내용이 비어 있습니다',
usageIssue: ' 사용법: gh please issue comment edit <comment-id> --body \'내용\'',
Expand All @@ -317,6 +327,11 @@ export const commentMessages: Record<Language, CommentMessages> = {
fetchingComment: (commentId: number) => `🔍 Fetching comment ${commentId}...`,
updatingComment: (commentId: number) => `📝 Updating comment ${commentId}...`,
commentUpdated: '✅ Comment updated successfully!',
deletingComment: (commentId: number | string) => `🗑️ Deleting comment ${commentId}...`,
commentDeleted: '✅ Comment deleted successfully!',
confirmDelete: 'Are you sure you want to delete this comment? (y/N): ',
deleteAborted: 'Delete aborted.',
usageDeleteIssue: ' Usage: gh please issue comment delete <comment-id> [--yes]',
Comment on lines +330 to +334
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Please add the English translations for the new i18n keys. This completes the internationalization for the new command's logic.

Suggested change
deletingComment: (commentId: number | string) => `🗑️ Deleting comment ${commentId}...`,
commentDeleted: '✅ Comment deleted successfully!',
confirmDelete: 'Are you sure you want to delete this comment? (y/N): ',
deleteAborted: 'Delete aborted.',
usageDeleteIssue: ' Usage: gh please issue comment delete <comment-id> [--yes]',
deletingComment: (commentId: number | string) => `🗑️ Deleting comment ${commentId}...`,
commentDeleted: '✅ Comment deleted successfully!',
confirmDelete: 'Are you sure you want to delete this comment? (y/N): ',
deleteAborted: 'Delete aborted.',
usageDeleteIssue: ' Usage: gh please issue comment delete <comment-id> [--yes]',
nodeIdDetected: '✓ Node ID detected, using directly',
convertingId: '🔄 Converting Database ID to Node ID...',
issueRequiredForDbId: 'Issue number is required when using Database ID. Use --issue <number> or provide Node ID instead.',

bodyRequired: '❌ Error: --body or --body-file is required',
bodyEmpty: '❌ Error: Comment body cannot be empty',
usageIssue: ' Usage: gh please issue comment edit <comment-id> --body \'text\'',
Expand Down
Loading
Loading