/**
* pgh issue — repo and org issue tracker.
*
* Repo scope (positional):
* pgh issue <repo> list
* pgh issue <repo> <id> show
* pgh issue <repo> --create "title" [--body "..."]
* pgh issue <repo> <id> --close|--reopen|--delete
* pgh issue <repo> <id> --comment "text"
* pgh issue <repo> <id> --edit-title "..."
* pgh issue <repo> <id> --edit-body "..."
* pgh issue <repo> --counts
*
* Org scope (--org flag):
* pgh issue --org <name> list
* pgh issue --org <name> <id> show
* pgh issue --org <name> --create "title" [--body "..."]
* pgh issue --org <name> <id> --close|--reopen|--delete
* pgh issue --org <name> <id> --comment "text"
* pgh issue --org <name> <id> --edit-title "..."
* pgh issue --org <name> <id> --edit-body "..."
* pgh issue --org <name> --counts
*
* Notes:
* --delete is a soft delete: sets status='deleted' on the issue. The
* record stays in the autobase view (hard-delete would require new
* apply ops). Default `list` excludes deleted; pass --status deleted
* to see them.
*/
export const usage = `pgh issue — repo and org issue tracker
Repo scope (positional):
pgh issue <repo> List issues
pgh issue <repo> <id> Show one issue
pgh issue <repo> --create "title" [--body "..."]
pgh issue <repo> <id> --close|--reopen|--delete
pgh issue <repo> <id> --comment "text"
pgh issue <repo> <id> --edit-title "..." [--edit-body "..."]
pgh issue <repo> --counts
pgh issue <repo> --status open|closed|deleted|all
Org scope (--org flag, same actions):
pgh issue --org <name> List issues
pgh issue --org <name> --create "title" [--body "..."]
pgh issue --org <name> <id> --close|--comment "text"|...
Note: the action is a flag (--create), not a subcommand — the first
positional is the <repo> (or use --org <name>).`
// Action verbs people reach for by habit (git-issue style). They are flags
// here, not subcommands — if one shows up in the <repo> slot the user almost
// certainly mistyped, so surface usage instead of listing a repo by that name.
const MISTAKEN_VERBS = new Set([
'create', 'close', 'reopen', 'delete', 'comment', 'edit', 'view', 'show'
])
function flagValue (args, name) {
const idx = args.indexOf(name)
if (idx === -1) return undefined
return args[idx + 1]
}
function flagPresent (args, name) {
return args.includes(name)
}
function parseScope (args) {
// --org <name> wins over a positional repo arg.
const orgIdx = args.indexOf('--org')
if (orgIdx !== -1) {
const orgName = args[orgIdx + 1]
if (!orgName || orgName.startsWith('--')) {
throw new Error('--org requires an org name')
}
// Strip --org <name> from args so positional id parsing still works.
const rest = [...args.slice(0, orgIdx), ...args.slice(orgIdx + 2)]
return { scope: { org: orgName }, rest }
}
const repo = args[0]
if (!repo || repo.startsWith('--')) {
throw new Error(
'usage: pgh issue <repo> [...] | pgh issue --org <name> [...]'
)
}
if (MISTAKEN_VERBS.has(repo)) {
throw new Error(`'${repo}' is an action, not a repo.\n` + usage)
}
return { scope: { repo }, rest: args.slice(1) }
}
function parseId (rest) {
// First positional arg in rest (if any) is the issue id. Allow either
// a numeric string or a hash-prefixed form like "#5".
const raw = rest[0]
if (raw === undefined || raw.startsWith('--')) return null
const cleaned = raw.startsWith('#') ? raw.slice(1) : raw
const n = Number(cleaned)
if (!Number.isInteger(n) || n <= 0) {
throw new Error(`expected positive integer issue id, got '${raw}'`)
}
return n
}
export async function run (client, args) {
let scope, rest
try {
({ scope, rest } = parseScope(args))
} catch (err) {
throw err
}
// --counts is scope-only; no id required.
if (flagPresent(rest, '--counts')) {
return client.call('issue-counts', { ...scope })
}
// --create needs a title; no id; body is optional.
if (flagPresent(rest, '--create')) {
const title = flagValue(rest, '--create')
if (!title || title.startsWith('--')) {
throw new Error('--create requires a title')
}
const body = flagValue(rest, '--body')
return client.call('create-issue', { ...scope, title, body })
}
const id = parseId(rest)
// Mutating actions all need an id.
const needId = (flag) => {
if (id === null) throw new Error(`${flag} requires an issue id`)
}
if (flagPresent(rest, '--close')) {
needId('--close')
return client.call('update-issue', { ...scope, id, status: 'closed' })
}
if (flagPresent(rest, '--reopen')) {
needId('--reopen')
return client.call('update-issue', { ...scope, id, status: 'open' })
}
if (flagPresent(rest, '--delete')) {
needId('--delete')
return client.call('update-issue', { ...scope, id, status: 'deleted' })
}
if (flagPresent(rest, '--comment')) {
needId('--comment')
const text = flagValue(rest, '--comment')
if (!text) throw new Error('--comment requires text')
// The handler expects { body }, the v1 surface used { text }; accept
// either at the wire so existing scripts keep working.
return client.call('comment-issue', { ...scope, id, body: text })
}
// --edit-title and --edit-body can be combined in a single update.
const newTitle = flagValue(rest, '--edit-title')
const newBody = flagValue(rest, '--edit-body')
if (newTitle !== undefined || newBody !== undefined) {
needId('--edit-title/--edit-body')
const params = { ...scope, id }
if (newTitle !== undefined) {
if (!newTitle || newTitle.startsWith('--')) {
throw new Error('--edit-title requires a non-empty value')
}
params.title = newTitle
}
if (newBody !== undefined) {
params.body = newBody
}
return client.call('update-issue', params)
}
// No flags + id ⇒ show.
if (id !== null) return client.call('get-issue', { ...scope, id })
// No flags, no id ⇒ list. By default hide soft-deleted issues; pass
// --status deleted (or --status all) to override. The handler accepts
// a single status string filter — for everything-except-deleted we
// request all and filter client-side.
const status = flagValue(rest, '--status')
const params = { ...scope }
if (status) params.status = status
const result = await client.call('list-issues', params)
if (!status) {
result.issues = (result.issues || []).filter(i => i.status !== 'deleted')
}
return result
}