/**
* pgh issue commands
*
* Subcommands: list, create, view, edit, close, reopen, comment, label
*/
import { table, jsonOut, log, error } from '../output.js'
import { getRepo, getNamespace } from '../config.js'
function resolveRepo (flags) {
const repo = getRepo(flags)
if (!repo) error('Repository required. Use --repo <name> or set defaultRepo in config.')
return repo
}
function resolveNs (flags) {
return getNamespace(flags) || '_'
}
export async function issueCommand (client, sub, args, flags) {
switch (sub) {
case 'list':
case 'ls':
return issueList(client, flags)
case 'create':
case 'new':
return issueCreate(client, args.join(' '), flags)
case 'view':
case 'show':
return issueView(client, args[0], flags)
case 'edit':
return issueEdit(client, args[0], flags)
case 'close':
return issueUpdate(client, args[0], 'closed', flags)
case 'reopen':
return issueUpdate(client, args[0], 'open', flags)
case 'comment':
return issueComment(client, args[0], flags)
case 'label':
return issueLabel(client, args, flags)
default:
issueHelp()
}
}
async function issueList (client, flags) {
const repo = resolveRepo(flags)
const ns = resolveNs(flags)
const params = new URLSearchParams()
if (flags.status) params.set('status', flags.status)
if (flags.label) params.set('label', flags.label)
if (flags.author) params.set('author', flags.author)
const qs = params.toString()
const data = await client.get(`/${ns}/${repo}/issues${qs ? '?' + qs : ''}`, {
Accept: 'application/json'
})
if (flags.json) {
jsonOut(data)
return
}
const issues = data.issues || []
if (issues.length === 0) {
log('No issues.')
if (data.counts) {
log(` Open: ${data.counts.open} Closed: ${data.counts.closed} Total: ${data.counts.total}`)
}
return
}
table(issues, [
{ key: 'number', label: '#', width: 5 },
{ key: 'title', label: 'TITLE', width: 50 },
{ key: 'status', label: 'STATUS', width: 8 },
{ key: 'labels', label: 'LABELS', width: 20 }
])
if (data.counts) {
log(`\nOpen: ${data.counts.open} Closed: ${data.counts.closed} Total: ${data.counts.total}`)
}
}
async function issueCreate (client, title, flags) {
if (!title && !flags.title) error('Usage: pgh issue create <title> [-d description]')
const repo = resolveRepo(flags)
const ns = resolveNs(flags)
const body = {
title: flags.title || title,
description: flags.description || flags.d || ''
}
const data = await client.post(`/${ns}/${repo}/issues`, body)
if (flags.json) {
jsonOut(data)
return
}
if (data.issue) {
log(`Issue #${data.issue.number} created: ${data.issue.title}`)
}
}
async function issueView (client, number, flags) {
if (!number) error('Usage: pgh issue view <number>')
const repo = resolveRepo(flags)
const ns = resolveNs(flags)
const data = await client.get(`/${ns}/${repo}/issues/${number}`, {
Accept: 'application/json'
})
if (flags.json) {
jsonOut(data)
return
}
const issue = data.issue || data
log(`#${issue.number}: ${issue.title}`)
log(`Status: ${issue.status}`)
if (issue.labels && issue.labels.length > 0) {
log(`Labels: ${issue.labels.join(', ')}`)
}
log(`Created: ${new Date(issue.createdAt).toLocaleString()}`)
if (issue.createdBy) log(`Author: ${issue.createdBy}`)
log('')
if (issue.description) {
log(issue.description)
} else {
log('No description.')
}
if (issue.comments && issue.comments.length > 0) {
log(`\n--- Comments (${issue.comments.length}) ---\n`)
for (const c of issue.comments) {
const author = c.author || c.userId || 'Anonymous'
const date = new Date(c.createdAt).toLocaleString()
log(`#${c.id} ${author} (${date}):`)
log(` ${c.text}`)
log(` Link: issues/${issue.number}#comment-${c.id}`)
log('')
}
}
}
async function issueUpdate (client, number, status, flags) {
if (!number) error(`Usage: pgh issue ${status === 'closed' ? 'close' : 'reopen'} <number>`)
const repo = resolveRepo(flags)
const ns = resolveNs(flags)
const data = await client.patch(`/${ns}/${repo}/issues/${number}`, { status })
if (flags.json) {
jsonOut(data)
return
}
log(`Issue #${number} ${status === 'closed' ? 'closed' : 'reopened'}`)
}
async function issueEdit (client, number, flags) {
if (!number) error('Usage: pgh issue edit <number> --title "..." --description "..."')
const title = flags.title || flags.t
const description = flags.description || flags.d
if (!title && !description) error('Provide --title and/or --description to edit')
const repo = resolveRepo(flags)
const ns = resolveNs(flags)
const body = {}
if (title) body.title = title
if (description) body.description = description
const data = await client.patch(`/${ns}/${repo}/issues/${number}`, body)
if (flags.json) {
jsonOut(data)
return
}
const parts = []
if (title) parts.push('title')
if (description) parts.push('description')
log(`Issue #${number} updated (${parts.join(', ')})`)
}
async function issueComment (client, number, flags) {
if (!number) error('Usage: pgh issue comment <number> -m <message>')
const message = flags.m || flags.message
if (!message) error('Comment message required. Use -m <message>')
const repo = resolveRepo(flags)
const ns = resolveNs(flags)
const data = await client.post(`/${ns}/${repo}/issues/${number}/comments`, {
text: message
})
if (flags.json) {
jsonOut(data)
return
}
const commentId = data.comment && data.comment.id
log(`Comment added to issue #${number}`)
if (commentId) {
log(`Link: issues/${number}#comment-${commentId}`)
}
}
async function issueLabel (client, args, flags) {
const action = args[0]
const number = args[1]
const labelName = args[2]
if (!action || !number || !labelName) {
error('Usage: pgh issue label <add|rm> <number> <label>')
}
const repo = resolveRepo(flags)
const ns = resolveNs(flags)
if (action === 'add') {
await client.post(`/${ns}/${repo}/issues/${number}/labels`, {
labels: [labelName]
})
log(`Label "${labelName}" added to issue #${number}`)
} else if (action === 'rm' || action === 'remove') {
await client.del(`/${ns}/${repo}/issues/${number}/labels/${encodeURIComponent(labelName)}`)
log(`Label "${labelName}" removed from issue #${number}`)
} else {
error('Unknown label action. Use "add" or "rm".')
}
}
export function issueHelp () {
log(`Usage: pgh issue <subcommand> [args] [flags]
Subcommands:
list List issues
create <title> Create a new issue
view <number> View issue details
close <number> Close an issue
reopen <number> Reopen an issue
comment <number> -m <msg> Add comment to issue
label add <number> <name> Add label to issue
label rm <number> <name> Remove label from issue
Flags:
--repo <name> Repository name
--namespace <ns> Namespace (default: _)
--status <open|closed|all> Filter by status
--label <name> Filter by label
--author <id> Filter by author
-d, --description <text> Issue description (for create)
-m, --message <text> Comment text
--title <text> Issue title (for create)
--json Output as JSON
Comment linking:
Each comment has a unique ID shown as #N in view output.
Direct link: /issues/<number>#comment-<id>`)
}