@ondrej-svec/hog 1.23.1 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/github.ts","../src/utils.ts","../src/board/fetch.ts","../src/board/fetch-worker.ts"],"sourcesContent":["import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface GitHubIssue {\n readonly number: number;\n readonly title: string;\n readonly url: string;\n readonly state: string;\n readonly updatedAt: string;\n readonly labels: { name: string }[];\n readonly assignees?: { login: string }[];\n readonly targetDate?: string;\n readonly body?: string;\n readonly projectStatus?: string;\n readonly slackThreadUrl?: string;\n /**\n * All other GitHub Project custom field values keyed by field name.\n * Includes single-select, text, number, and iteration fields — excluding\n * Status (→ projectStatus) and date fields (→ targetDate).\n * Example: { Workstream: \"Platform\", Size: \"M\", Priority: \"High\" }\n */\n readonly customFields?: Record<string, string>;\n}\n\nexport interface ProjectFieldValues {\n targetDate?: string;\n status?: string;\n customFields?: Record<string, string>;\n}\n\nexport interface RepoProjectConfig {\n projectNumber: number;\n statusFieldId: string;\n optionId: string;\n}\n\n/** Matches common date field names used in GitHub Projects v2 (case-insensitive). */\nconst DATE_FIELD_NAME_RE = /^(target\\s*date|due\\s*date|due|deadline)$/i;\n\nfunction runGh(args: string[]): string {\n return execFileSync(\"gh\", args, { encoding: \"utf-8\", timeout: 30_000, stdio: \"pipe\" }).trim();\n}\n\nfunction runGhJson<T>(args: string[]): T {\n const output = runGh(args);\n return JSON.parse(output) as T;\n}\n\nasync function runGhAsync(args: string[]): Promise<string> {\n const { stdout } = await execFileAsync(\"gh\", args, { encoding: \"utf-8\", timeout: 30_000 });\n return stdout.trim();\n}\n\nasync function runGhJsonAsync<T>(args: string[]): Promise<T> {\n const output = await runGhAsync(args);\n return JSON.parse(output) as T;\n}\n\n/**\n * Run a GraphQL query via `gh api graphql`. Handles partial errors: when the\n * query returns data for one alias but NOT_FOUND for another (e.g. org vs user\n * owner), `gh` exits with code 1 but still emits valid JSON on stdout. This\n * helper recovers that JSON from the error object instead of throwing.\n */\nfunction runGhGraphQL<T>(args: string[]): T {\n try {\n return runGhJson<T>(args);\n } catch (err: unknown) {\n if (err && typeof err === \"object\" && \"stdout\" in err) {\n const stdout = (err as { stdout: string | Buffer }).stdout;\n const output = typeof stdout === \"string\" ? stdout : stdout?.toString(\"utf-8\");\n if (output) {\n try {\n return JSON.parse(output.trim()) as T;\n } catch {\n // stdout wasn't valid JSON — rethrow original\n }\n }\n }\n throw err;\n }\n}\n\nasync function runGhGraphQLAsync<T>(args: string[]): Promise<T> {\n try {\n return await runGhJsonAsync<T>(args);\n } catch (err: unknown) {\n if (err && typeof err === \"object\" && \"stdout\" in err) {\n const stdout = (err as { stdout: string | Buffer }).stdout;\n const output = typeof stdout === \"string\" ? stdout : stdout?.toString(\"utf-8\");\n if (output) {\n try {\n return JSON.parse(output.trim()) as T;\n } catch {\n // stdout wasn't valid JSON — rethrow original\n }\n }\n }\n throw err;\n }\n}\n\nexport function fetchAssignedIssues(repo: string, assignee: string): GitHubIssue[] {\n return runGhJson<GitHubIssue[]>([\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--assignee\",\n assignee,\n \"--state\",\n \"open\",\n \"--json\",\n \"number,title,url,state,updatedAt,labels\",\n \"--limit\",\n \"100\",\n ]);\n}\n\nexport interface FetchIssuesOptions {\n assignee?: string | undefined;\n state?: \"open\" | \"closed\" | \"all\" | undefined;\n limit?: number | undefined;\n}\n\nexport function fetchRepoIssues(repo: string, options: FetchIssuesOptions = {}): GitHubIssue[] {\n const { state = \"open\", limit = 100 } = options;\n const args = [\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--state\",\n state,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body\",\n \"--limit\",\n String(limit),\n ];\n if (options.assignee) {\n args.push(\"--assignee\", options.assignee);\n }\n return runGhJson<GitHubIssue[]>(args);\n}\n\nexport function assignIssue(repo: string, issueNumber: number): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueToAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", user]);\n}\n\nexport async function unassignIssueAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\n \"issue\",\n \"edit\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--remove-assignee\",\n user,\n ]);\n}\n\nexport async function fetchIssueAsync(repo: string, issueNumber: number): Promise<GitHubIssue> {\n return runGhJsonAsync<GitHubIssue>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body,projectStatus\",\n ]);\n}\n\nexport async function closeIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"close\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function reopenIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"reopen\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function createIssueAsync(\n repo: string,\n title: string,\n body: string,\n labels?: string[],\n): Promise<string> {\n const args = [\"issue\", \"create\", \"--repo\", repo, \"--title\", title, \"--body\", body];\n if (labels && labels.length > 0) {\n for (const label of labels) {\n args.push(\"--label\", label);\n }\n }\n return runGhAsync(args);\n}\n\nexport async function editIssueTitleAsync(\n repo: string,\n issueNumber: number,\n title: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--title\", title]);\n}\n\nexport async function editIssueBodyAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addCommentAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"comment\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport async function removeLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--remove-label\", label]);\n}\n\nexport async function updateLabelsAsync(\n repo: string,\n issueNumber: number,\n addLabels: string[],\n removeLabels: string[],\n): Promise<void> {\n const args = [\"issue\", \"edit\", String(issueNumber), \"--repo\", repo];\n for (const label of addLabels) args.push(\"--add-label\", label);\n for (const label of removeLabels) args.push(\"--remove-label\", label);\n await runGhAsync(args);\n}\n\nexport interface IssueComment {\n readonly body: string;\n readonly author: { readonly login: string };\n readonly createdAt: string;\n}\n\nexport async function fetchIssueCommentsAsync(\n repo: string,\n issueNumber: number,\n): Promise<IssueComment[]> {\n const result = await runGhJsonAsync<{ comments: IssueComment[] }>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"comments\",\n ]);\n return result.comments ?? [];\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub Project field types\nexport function fetchProjectFields(\n repo: string,\n issueNumber: number,\n projectNumber: number,\n): ProjectFieldValues {\n // GraphQL query to get project item fields for this issue\n const query = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n project { number }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n }\n }\n `;\n\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return {};\n\n try {\n const result = runGhJson<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem) return {};\n\n const fields: ProjectFieldValues = {};\n const fieldValues = projectItem.fieldValues?.nodes ?? [];\n\n for (const fv of fieldValues) {\n if (!fv) continue;\n const fieldName = fv.field?.name ?? \"\";\n if (\"date\" in fv && DATE_FIELD_NAME_RE.test(fieldName)) {\n fields.targetDate = fv.date;\n } else if (\"name\" in fv && fieldName === \"Status\") {\n fields.status = fv.name;\n } else if (fieldName) {\n const value =\n \"text\" in fv && fv.text != null\n ? fv.text\n : \"number\" in fv && fv.number != null\n ? String(fv.number)\n : \"name\" in fv && fv.name != null\n ? fv.name\n : \"title\" in fv && fv.title != null\n ? fv.title\n : null;\n if (value != null) {\n if (!fields.customFields) fields.customFields = {};\n fields.customFields[fieldName] = value;\n }\n }\n }\n\n return fields;\n } catch {\n return {};\n }\n}\n\nexport interface ProjectEnrichment {\n targetDate?: string;\n projectStatus?: string;\n customFields?: Record<string, string>;\n}\n\n/**\n * Fetch target dates and project statuses for all issues in a project in one GraphQL call.\n * Returns a Map from issue number to enrichment data.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub Project field types across all items\nexport function fetchProjectEnrichment(\n repo: string,\n projectNumber: number,\n): Map<number, ProjectEnrichment> {\n const [owner] = repo.split(\"/\");\n if (!owner) return new Map();\n\n const projectItemsFragment = `\n projectV2(number: $projectNumber) {\n items(first: 100, after: $cursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n content {\n ... on Issue {\n number\n }\n }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!, $cursor: String) {\n organization(login: $owner) { ${projectItemsFragment} }\n user(login: $owner) { ${projectItemsFragment} }\n }\n `;\n\n try {\n const enrichMap = new Map<number, ProjectEnrichment>();\n let cursor: string | null = null;\n\n do {\n const args = [\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ];\n if (cursor) args.push(\"-f\", `cursor=${cursor}`);\n const result = runGhGraphQL<ProjectItemsResult>(args);\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n const page = ownerNode?.projectV2?.items;\n const nodes = page?.nodes ?? [];\n\n for (const item of nodes) {\n if (!item?.content?.number) continue;\n const enrichment: ProjectEnrichment = {};\n const fieldValues = item.fieldValues?.nodes ?? [];\n for (const fv of fieldValues) {\n if (!fv) continue;\n const fieldName = fv.field?.name ?? \"\";\n if (\"date\" in fv && fv.date && DATE_FIELD_NAME_RE.test(fieldName)) {\n enrichment.targetDate = fv.date;\n } else if (\"name\" in fv && fieldName === \"Status\" && fv.name) {\n enrichment.projectStatus = fv.name;\n } else if (fieldName) {\n const value =\n \"text\" in fv && fv.text != null\n ? fv.text\n : \"number\" in fv && fv.number != null\n ? String(fv.number)\n : \"name\" in fv && fv.name != null\n ? fv.name\n : \"title\" in fv && fv.title != null\n ? fv.title\n : null;\n if (value != null) {\n if (!enrichment.customFields) enrichment.customFields = {};\n enrichment.customFields[fieldName] = value;\n }\n }\n }\n enrichMap.set(item.content.number, enrichment);\n }\n\n if (!page?.pageInfo?.hasNextPage) break;\n cursor = page.pageInfo.endCursor ?? null;\n } while (cursor);\n\n return enrichMap;\n } catch {\n return new Map();\n }\n}\n\n/** Backwards-compatible wrapper for fetchProjectEnrichment. */\nexport function fetchProjectTargetDates(repo: string, projectNumber: number): Map<number, string> {\n const enrichMap = fetchProjectEnrichment(repo, projectNumber);\n const dateMap = new Map<number, string>();\n for (const [num, e] of enrichMap) {\n if (e.targetDate) dateMap.set(num, e.targetDate);\n }\n return dateMap;\n}\n\nexport interface StatusOption {\n id: string;\n name: string;\n}\n\n/**\n * Fetch available project status options (the SingleSelectField values).\n * Returns options in the order defined on the project board.\n */\nexport function fetchProjectStatusOptions(\n repo: string,\n projectNumber: number,\n _statusFieldId: string,\n): StatusOption[] {\n const [owner] = repo.split(\"/\");\n if (!owner) return [];\n\n const statusFragment = `\n projectV2(number: $projectNumber) {\n field(name: \"Status\") {\n ... on ProjectV2SingleSelectField {\n options {\n id\n name\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${statusFragment} }\n user(login: $owner) { ${statusFragment} }\n }\n `;\n\n try {\n const result = runGhGraphQL<ProjectStatusResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n return ownerNode?.projectV2?.field?.options ?? [];\n } catch {\n return [];\n }\n}\n\nexport function addLabel(repo: string, issueNumber: number, label: string): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport interface LabelOption {\n name: string;\n color: string;\n}\n\n/**\n * Fetch all labels defined in the repo asynchronously.\n * Uses execFileAsync (not execFileSync) to avoid blocking the React render thread.\n */\nexport async function fetchRepoLabelsAsync(repo: string): Promise<LabelOption[]> {\n try {\n const result = await runGhJsonAsync<LabelOption[]>([\n \"label\",\n \"list\",\n \"--repo\",\n repo,\n \"--json\",\n \"name,color\",\n ]);\n return Array.isArray(result) ? result : [];\n } catch {\n return [];\n }\n}\n\n/** Cache for GitHub Projects node IDs — these are immutable per project number. */\nconst projectNodeIdCache = new Map<string, string>();\n\n/** Clears the project node ID cache. Intended for use in tests only. */\nexport function clearProjectNodeIdCache(): void {\n projectNodeIdCache.clear();\n}\n\nasync function getProjectNodeId(owner: string, projectNumber: number): Promise<string | null> {\n const key = `${owner}/${String(projectNumber)}`;\n const cached = projectNodeIdCache.get(key);\n if (cached !== undefined) return cached;\n\n const idFragment = `projectV2(number: $projectNumber) { id }`;\n\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${idFragment} }\n user(login: $owner) { ${idFragment} }\n }\n `;\n\n const projectResult = await runGhGraphQLAsync<GraphQLProjectResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${projectQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = projectResult?.data?.organization ?? projectResult?.data?.user;\n const projectId = ownerNode?.projectV2?.id;\n if (!projectId) return null;\n projectNodeIdCache.set(key, projectId);\n return projectId;\n}\n\nexport function updateProjectItemStatus(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): void {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n // First get the project item ID\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = runGhJson<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectNumber = projectConfig.projectNumber;\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem?.id) return;\n\n // Get the project ID\n const idFragment = `projectV2(number: $projectNumber) { id }`;\n\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${idFragment} }\n user(login: $owner) { ${idFragment} }\n }\n `;\n\n const projectResult = runGhGraphQL<GraphQLProjectResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${projectQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const projectOwner = projectResult?.data?.organization ?? projectResult?.data?.user;\n const projectId = projectOwner?.projectV2?.id;\n if (!projectId) return;\n\n const statusFieldId = projectConfig.statusFieldId;\n const optionId = projectConfig.optionId;\n\n // Mutation to update the status\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n runGh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${statusFieldId}`,\n \"-F\",\n `optionId=${optionId}`,\n ]);\n}\n\nexport async function updateProjectItemStatusAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = await runGhJsonAsync<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectNumber = projectConfig.projectNumber;\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectNumber);\n if (!projectId) return;\n\n const statusFieldId = projectConfig.statusFieldId;\n const optionId = projectConfig.optionId;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${statusFieldId}`,\n \"-F\",\n `optionId=${optionId}`,\n ]);\n}\n\nexport interface RepoDueDateConfig {\n projectNumber: number;\n dueDateFieldId: string;\n}\n\n/**\n * Set a date field value on a GitHub Projects v2 item for the given issue.\n * Uses the same 3-step pattern as updateProjectItemStatusAsync.\n */\nexport async function updateProjectItemDateAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoDueDateConfig,\n dueDate: string,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = await runGhJsonAsync<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectItem = items.find((item) => item?.project?.number === projectConfig.projectNumber);\n\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);\n if (!projectId) return;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { date: $date }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${projectConfig.dueDateFieldId}`,\n \"-F\",\n `date=${dueDate}`,\n ]);\n}\n\n// Internal GraphQL response types\n\ninterface FieldValue {\n field?: { name?: string };\n date?: string;\n name?: string;\n text?: string;\n number?: number;\n title?: string; // iteration field title\n}\n\ninterface ProjectItem {\n id?: string;\n project?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface GraphQLResult {\n data?: {\n repository?: {\n issue?: {\n projectItems?: {\n nodes?: (ProjectItem | null)[];\n };\n };\n };\n };\n}\n\ninterface ProjectV2IdNode {\n projectV2?: {\n id?: string;\n };\n}\n\ninterface GraphQLProjectResult {\n data?: {\n organization?: ProjectV2IdNode;\n user?: ProjectV2IdNode;\n };\n}\n\ninterface ProjectItemNode {\n content?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface ProjectV2ItemsNode {\n projectV2?: {\n items?: {\n pageInfo?: { hasNextPage: boolean; endCursor?: string };\n nodes?: (ProjectItemNode | null)[];\n };\n };\n}\n\ninterface ProjectItemsResult {\n data?: {\n organization?: ProjectV2ItemsNode;\n user?: ProjectV2ItemsNode;\n };\n}\n\ninterface ProjectV2StatusNode {\n projectV2?: {\n field?: {\n options?: StatusOption[];\n };\n };\n}\n\ninterface ProjectStatusResult {\n data?: {\n organization?: ProjectV2StatusNode;\n user?: ProjectV2StatusNode;\n };\n}\n","/** Formats an unknown error as a string. */\nexport function formatError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { HogConfig, RepoConfig } from \"../config.js\";\nimport type { GitHubIssue, StatusOption } from \"../github.js\";\nimport { fetchProjectEnrichment, fetchProjectStatusOptions, fetchRepoIssues } from \"../github.js\";\nimport { formatError } from \"../utils.js\";\n\nexport interface RepoData {\n repo: RepoConfig;\n issues: GitHubIssue[];\n statusOptions: StatusOption[];\n error: string | null;\n}\n\nexport interface ActivityEvent {\n type:\n | \"comment\"\n | \"status\"\n | \"assignment\"\n | \"opened\"\n | \"closed\"\n | \"labeled\"\n | \"branch_created\"\n | \"pr_opened\"\n | \"pr_merged\"\n | \"pr_closed\";\n repoShortName: string;\n issueNumber: number;\n actor: string;\n summary: string;\n timestamp: Date;\n /** For branch_created events: the full branch name */\n branchName?: string | undefined;\n /** For pr_* events: the PR number (distinct from linked issueNumber) */\n prNumber?: number | undefined;\n}\n\nexport interface DashboardData {\n repos: RepoData[];\n activity: ActivityEvent[];\n fetchedAt: Date;\n}\n\nexport interface FetchOptions {\n repoFilter?: string | undefined;\n mineOnly?: boolean | undefined;\n backlogOnly?: boolean | undefined;\n}\n\nexport const SLACK_URL_RE = /https:\\/\\/[^/]+\\.slack\\.com\\/archives\\/[A-Z0-9]+\\/p[0-9]+/i;\n\nexport function extractSlackUrl(body: string | undefined): string | undefined {\n if (!body) return undefined;\n const match = body.match(SLACK_URL_RE);\n return match?.[0];\n}\n\n/** Extract issue numbers from a branch name (e.g. \"feat/42-add-auth\" → [42]) */\nexport function extractIssueNumbersFromBranch(branchName: string): number[] {\n // Find numbers that look like issue numbers (1-5 digits, word boundary)\n const matches = branchName.match(/\\b(\\d{1,5})\\b/g);\n if (!matches) return [];\n return [...new Set(matches.map((m) => parseInt(m, 10)).filter((n) => n > 0))];\n}\n\n/** Extract issue numbers linked in PR title/body (e.g. \"Fixes #42\" → [42]) */\nexport function extractLinkedIssueNumbers(title: string | null, body: string | null): number[] {\n const text = `${title ?? \"\"} ${body ?? \"\"}`;\n const matches = text.match(/#(\\d{1,5})\\b/g);\n if (!matches) return [];\n return [...new Set(matches.map((m) => parseInt(m.slice(1), 10)).filter((n) => n > 0))];\n}\n\n/** Fetch recent activity events for a repo (last 24h, max 30 events) */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub event types\nexport function fetchRecentActivity(repoName: string, shortName: string): ActivityEvent[] {\n try {\n const output = execFileSync(\n \"gh\",\n [\n \"api\",\n `repos/${repoName}/events`,\n \"-f\",\n \"per_page=30\",\n \"-q\",\n '.[] | select(.type == \"IssuesEvent\" or .type == \"IssueCommentEvent\" or .type == \"PullRequestEvent\" or .type == \"CreateEvent\") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: (.payload.comment.body // .payload.pull_request.body), created_at: .created_at, ref: .payload.ref, ref_type: .payload.ref_type, merged: .payload.pull_request.merged}',\n ],\n { encoding: \"utf-8\", timeout: 15_000, stdio: \"pipe\" },\n );\n\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n const events: ActivityEvent[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const ev = JSON.parse(line) as {\n type: string;\n actor: string;\n action: string;\n number: number | null;\n title: string | null;\n body: string | null;\n created_at: string;\n ref: string | null;\n ref_type: string | null;\n merged: boolean | null;\n };\n\n const timestamp = new Date(ev.created_at);\n if (timestamp.getTime() < cutoff) continue;\n\n // CreateEvent (branch creation) has no issue/PR number — handled separately\n if (ev.type === \"CreateEvent\") {\n if (ev.ref_type !== \"branch\" || !ev.ref) continue;\n // Extract issue numbers from branch name (e.g. \"feat/42-add-auth\" → 42)\n const issueNumbers = extractIssueNumbersFromBranch(ev.ref);\n for (const num of issueNumbers) {\n events.push({\n type: \"branch_created\",\n repoShortName: shortName,\n issueNumber: num,\n actor: ev.actor,\n summary: `created branch ${ev.ref}`,\n timestamp,\n branchName: ev.ref,\n });\n }\n continue;\n }\n\n if (!ev.number) continue;\n\n let eventType: ActivityEvent[\"type\"];\n let summary: string;\n let extras: Partial<Pick<ActivityEvent, \"prNumber\">> = {};\n\n if (ev.type === \"IssueCommentEvent\") {\n eventType = \"comment\";\n const preview = ev.body ? ev.body.slice(0, 60).replace(/\\n/g, \" \") : \"\";\n summary = `commented on #${ev.number}${preview ? ` — \"${preview}${(ev.body?.length ?? 0) > 60 ? \"...\" : \"\"}\"` : \"\"}`;\n } else if (ev.type === \"IssuesEvent\") {\n switch (ev.action) {\n case \"opened\":\n eventType = \"opened\";\n summary = `opened #${ev.number}: ${ev.title ?? \"\"}`;\n break;\n case \"closed\":\n eventType = \"closed\";\n summary = `closed #${ev.number}`;\n break;\n case \"assigned\":\n eventType = \"assignment\";\n summary = `assigned #${ev.number}`;\n break;\n case \"labeled\":\n eventType = \"labeled\";\n summary = `labeled #${ev.number}`;\n break;\n default:\n continue;\n }\n } else if (ev.type === \"PullRequestEvent\") {\n const prNumber = ev.number;\n extras = { prNumber };\n if (ev.action === \"opened\") {\n eventType = \"pr_opened\";\n summary = `opened PR #${prNumber}: ${ev.title ?? \"\"}`;\n } else if (ev.action === \"closed\" && ev.merged) {\n eventType = \"pr_merged\";\n summary = `merged PR #${prNumber}: ${ev.title ?? \"\"}`;\n } else if (ev.action === \"closed\") {\n eventType = \"pr_closed\";\n summary = `closed PR #${prNumber}`;\n } else {\n continue;\n }\n\n // For PR events, also create events for each linked issue number\n const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);\n for (const issueNum of linkedIssues) {\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: issueNum,\n actor: ev.actor,\n summary,\n timestamp,\n prNumber,\n });\n }\n // If no linked issues, use the PR number as issueNumber\n if (linkedIssues.length === 0) {\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: prNumber,\n actor: ev.actor,\n summary,\n timestamp,\n prNumber,\n });\n }\n continue;\n } else {\n continue;\n }\n\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: ev.number,\n actor: ev.actor,\n summary,\n timestamp,\n ...extras,\n });\n } catch {\n // Skip malformed event\n }\n }\n\n return events.slice(0, 15);\n } catch {\n return [];\n }\n}\n\nexport async function fetchDashboard(\n config: HogConfig,\n options: FetchOptions = {},\n): Promise<DashboardData> {\n const repos = options.repoFilter\n ? config.repos.filter(\n (r) => r.shortName === options.repoFilter || r.name === options.repoFilter,\n )\n : config.repos;\n\n // GitHub: synchronous (uses gh CLI via execFileSync)\n const repoData: RepoData[] = repos.map((repo) => {\n try {\n const fetchOpts: { assignee?: string } = {};\n if (options.mineOnly) {\n fetchOpts.assignee = config.board.assignee;\n }\n const issues = fetchRepoIssues(repo.name, fetchOpts);\n\n // Enrich issues with target dates + statuses from GitHub Projects (batched)\n let enrichedIssues = issues;\n let statusOptions: StatusOption[] = [];\n try {\n const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const e = enrichMap.get(issue.number);\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return {\n ...issue,\n ...(e?.targetDate !== undefined ? { targetDate: e.targetDate } : {}),\n ...(e?.projectStatus !== undefined ? { projectStatus: e.projectStatus } : {}),\n ...(e?.customFields !== undefined ? { customFields: e.customFields } : {}),\n ...(slackUrl ? { slackThreadUrl: slackUrl } : {}),\n };\n });\n statusOptions = fetchProjectStatusOptions(\n repo.name,\n repo.projectNumber,\n repo.statusFieldId,\n );\n } catch {\n // Non-critical: silently skip if project fields fail\n // Compute Slack thread URLs from original issue bodies\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;\n });\n }\n\n return { repo, issues: enrichedIssues, statusOptions, error: null };\n } catch (err) {\n return { repo, issues: [], statusOptions: [], error: formatError(err) };\n }\n });\n\n // Activity: fetch recent events from all repos (non-blocking, best-effort)\n const activity: ActivityEvent[] = [];\n for (const repo of repos) {\n const events = fetchRecentActivity(repo.name, repo.shortName);\n activity.push(...events);\n }\n // Sort by timestamp descending\n activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());\n\n return {\n repos: repoData,\n activity: activity.slice(0, 15),\n fetchedAt: new Date(),\n };\n}\n","import { parentPort, workerData } from \"node:worker_threads\";\nimport type { HogConfig } from \"../config.js\";\nimport type { FetchOptions } from \"./fetch.js\";\n\nconst { config, options } = workerData as { config: HogConfig; options: FetchOptions };\n\nconst { fetchDashboard } = await import(\"./fetch.js\");\n\nif (!parentPort) throw new Error(\"fetch-worker must run in a worker thread\");\n\ntry {\n const data = await fetchDashboard(config, options);\n parentPort.postMessage({ type: \"success\", data });\n} catch (err) {\n parentPort.postMessage({\n type: \"error\",\n error: err instanceof Error ? err.message : String(err),\n });\n}\n"],"mappings":"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","names":["options","execFileSync","config","options","fetchDashboard"]}
1
+ {"version":3,"sources":["../src/github.ts","../src/utils.ts","../src/board/fetch.ts","../src/board/fetch-worker.ts"],"sourcesContent":["import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface GitHubIssue {\n readonly number: number;\n readonly title: string;\n readonly url: string;\n readonly state: string;\n readonly updatedAt: string;\n readonly labels: { name: string }[];\n readonly assignees?: { login: string }[];\n readonly targetDate?: string;\n readonly body?: string;\n readonly projectStatus?: string;\n readonly slackThreadUrl?: string;\n /**\n * All other GitHub Project custom field values keyed by field name.\n * Includes single-select, text, number, and iteration fields — excluding\n * Status (→ projectStatus) and date fields (→ targetDate).\n * Example: { Workstream: \"Platform\", Size: \"M\", Priority: \"High\" }\n */\n readonly customFields?: Record<string, string>;\n}\n\nexport interface ProjectFieldValues {\n targetDate?: string;\n status?: string;\n customFields?: Record<string, string>;\n}\n\nexport interface RepoProjectConfig {\n projectNumber: number;\n statusFieldId: string;\n optionId: string;\n}\n\n/** Matches common date field names used in GitHub Projects v2 (case-insensitive). */\nconst DATE_FIELD_NAME_RE = /^(target\\s*date|due\\s*date|due|deadline)$/i;\n\nfunction runGh(args: string[]): string {\n return execFileSync(\"gh\", args, { encoding: \"utf-8\", timeout: 30_000, stdio: \"pipe\" }).trim();\n}\n\nfunction runGhJson<T>(args: string[]): T {\n const output = runGh(args);\n return JSON.parse(output) as T;\n}\n\nasync function runGhAsync(args: string[]): Promise<string> {\n const { stdout } = await execFileAsync(\"gh\", args, { encoding: \"utf-8\", timeout: 30_000 });\n return stdout.trim();\n}\n\nasync function runGhJsonAsync<T>(args: string[]): Promise<T> {\n const output = await runGhAsync(args);\n return JSON.parse(output) as T;\n}\n\n/**\n * Run a GraphQL query via `gh api graphql`. Handles partial errors: when the\n * query returns data for one alias but NOT_FOUND for another (e.g. org vs user\n * owner), `gh` exits with code 1 but still emits valid JSON on stdout. This\n * helper recovers that JSON from the error object instead of throwing.\n */\nfunction runGhGraphQL<T>(args: string[]): T {\n try {\n return runGhJson<T>(args);\n } catch (err: unknown) {\n if (err && typeof err === \"object\" && \"stdout\" in err) {\n const stdout = (err as { stdout: string | Buffer }).stdout;\n const output = typeof stdout === \"string\" ? stdout : stdout?.toString(\"utf-8\");\n if (output) {\n try {\n return JSON.parse(output.trim()) as T;\n } catch {\n // stdout wasn't valid JSON — rethrow original\n }\n }\n }\n throw err;\n }\n}\n\nasync function runGhGraphQLAsync<T>(args: string[]): Promise<T> {\n try {\n return await runGhJsonAsync<T>(args);\n } catch (err: unknown) {\n if (err && typeof err === \"object\" && \"stdout\" in err) {\n const stdout = (err as { stdout: string | Buffer }).stdout;\n const output = typeof stdout === \"string\" ? stdout : stdout?.toString(\"utf-8\");\n if (output) {\n try {\n return JSON.parse(output.trim()) as T;\n } catch {\n // stdout wasn't valid JSON — rethrow original\n }\n }\n }\n throw err;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal GraphQL response types (hoisted for use by shared helpers)\n// ---------------------------------------------------------------------------\n\ninterface FieldValue {\n field?: { name?: string };\n date?: string;\n name?: string;\n text?: string;\n number?: number;\n title?: string; // iteration field title\n}\n\ninterface ProjectItem {\n id?: string;\n project?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface GraphQLResult {\n data?: {\n repository?: {\n issue?: {\n projectItems?: {\n nodes?: (ProjectItem | null)[];\n };\n };\n };\n };\n}\n\ninterface ProjectV2IdNode {\n projectV2?: {\n id?: string;\n };\n}\n\ninterface GraphQLProjectResult {\n data?: {\n organization?: ProjectV2IdNode;\n user?: ProjectV2IdNode;\n };\n}\n\ninterface ProjectItemNode {\n content?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface ProjectV2ItemsNode {\n projectV2?: {\n items?: {\n pageInfo?: { hasNextPage: boolean; endCursor?: string };\n nodes?: (ProjectItemNode | null)[];\n };\n };\n}\n\ninterface ProjectItemsResult {\n data?: {\n organization?: ProjectV2ItemsNode;\n user?: ProjectV2ItemsNode;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Shared GraphQL queries & helpers\n// ---------------------------------------------------------------------------\n\n/** GraphQL query to find a project item by issue number. */\nconst FIND_PROJECT_ITEM_QUERY = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/** Build the `gh api graphql` args for {@link FIND_PROJECT_ITEM_QUERY}. */\nfunction findProjectItemArgs(owner: string, repoName: string, issueNumber: number): string[] {\n return [\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${FIND_PROJECT_ITEM_QUERY}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ];\n}\n\n/** Find a project item synchronously. Returns the matching node or `null`. */\nfunction findProjectItemSync(\n owner: string,\n repoName: string,\n issueNumber: number,\n projectNumber: number,\n): ProjectItem | null {\n const result = runGhJson<GraphQLResult>(findProjectItemArgs(owner, repoName, issueNumber));\n const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];\n return items.find((item) => item?.project?.number === projectNumber) ?? null;\n}\n\n/** Find a project item asynchronously. Returns the matching node or `null`. */\nasync function findProjectItemAsync(\n owner: string,\n repoName: string,\n issueNumber: number,\n projectNumber: number,\n): Promise<ProjectItem | null> {\n const result = await runGhJsonAsync<GraphQLResult>(\n findProjectItemArgs(owner, repoName, issueNumber),\n );\n const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];\n return items.find((item) => item?.project?.number === projectNumber) ?? null;\n}\n\n/**\n * Parse field value nodes from a GitHub Projects v2 item into structured data.\n *\n * The `statusKey` parameter controls which key receives the Status value\n * (`\"status\"` for {@link ProjectFieldValues}, `\"projectStatus\"` for\n * {@link ProjectEnrichment}).\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub Project field types\nfunction parseFieldValues(\n fieldValues: (FieldValue | null)[],\n statusKey: \"status\" | \"projectStatus\",\n): ProjectFieldValues & ProjectEnrichment {\n const result: ProjectFieldValues & ProjectEnrichment = {};\n for (const fv of fieldValues) {\n if (!fv) continue;\n const fieldName = fv.field?.name ?? \"\";\n if (\"date\" in fv && fv.date && DATE_FIELD_NAME_RE.test(fieldName)) {\n result.targetDate = fv.date;\n } else if (\"name\" in fv && fieldName === \"Status\" && fv.name) {\n (result as Record<string, unknown>)[statusKey] = fv.name;\n } else if (fieldName) {\n const value =\n \"text\" in fv && fv.text != null\n ? fv.text\n : \"number\" in fv && fv.number != null\n ? String(fv.number)\n : \"name\" in fv && fv.name != null\n ? fv.name\n : \"title\" in fv && fv.title != null\n ? fv.title\n : null;\n if (value != null) {\n if (!result.customFields) result.customFields = {};\n result.customFields[fieldName] = value;\n }\n }\n }\n return result;\n}\n\n/** Cache for GitHub Projects node IDs — these are immutable per project number. */\nconst projectNodeIdCache = new Map<string, string>();\n\n/** Resolve a GitHub Projects v2 node ID synchronously (with caching). */\nfunction getProjectNodeIdSync(owner: string, projectNumber: number): string | null {\n const key = `${owner}/${String(projectNumber)}`;\n const cached = projectNodeIdCache.get(key);\n if (cached !== undefined) return cached;\n\n const idFragment = `projectV2(number: $projectNumber) { id }`;\n\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${idFragment} }\n user(login: $owner) { ${idFragment} }\n }\n `;\n\n const projectResult = runGhGraphQL<GraphQLProjectResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${projectQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = projectResult?.data?.organization ?? projectResult?.data?.user;\n const projectId = ownerNode?.projectV2?.id;\n if (!projectId) return null;\n projectNodeIdCache.set(key, projectId);\n return projectId;\n}\n\n/** Resolve a GitHub Projects v2 node ID asynchronously (with caching). */\nasync function getProjectNodeId(owner: string, projectNumber: number): Promise<string | null> {\n const key = `${owner}/${String(projectNumber)}`;\n const cached = projectNodeIdCache.get(key);\n if (cached !== undefined) return cached;\n\n const idFragment = `projectV2(number: $projectNumber) { id }`;\n\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${idFragment} }\n user(login: $owner) { ${idFragment} }\n }\n `;\n\n const projectResult = await runGhGraphQLAsync<GraphQLProjectResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${projectQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = projectResult?.data?.organization ?? projectResult?.data?.user;\n const projectId = ownerNode?.projectV2?.id;\n if (!projectId) return null;\n projectNodeIdCache.set(key, projectId);\n return projectId;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport function fetchAssignedIssues(repo: string, assignee: string): GitHubIssue[] {\n return runGhJson<GitHubIssue[]>([\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--assignee\",\n assignee,\n \"--state\",\n \"open\",\n \"--json\",\n \"number,title,url,state,updatedAt,labels\",\n \"--limit\",\n \"100\",\n ]);\n}\n\nexport interface FetchIssuesOptions {\n assignee?: string | undefined;\n state?: \"open\" | \"closed\" | \"all\" | undefined;\n limit?: number | undefined;\n}\n\nexport function fetchRepoIssues(repo: string, options: FetchIssuesOptions = {}): GitHubIssue[] {\n const { state = \"open\", limit = 100 } = options;\n const args = [\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--state\",\n state,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body\",\n \"--limit\",\n String(limit),\n ];\n if (options.assignee) {\n args.push(\"--assignee\", options.assignee);\n }\n return runGhJson<GitHubIssue[]>(args);\n}\n\nexport async function fetchRepoIssuesAsync(\n repo: string,\n options: FetchIssuesOptions = {},\n): Promise<GitHubIssue[]> {\n const { state = \"open\", limit = 100 } = options;\n const args = [\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--state\",\n state,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body\",\n \"--limit\",\n String(limit),\n ];\n if (options.assignee) {\n args.push(\"--assignee\", options.assignee);\n }\n return runGhJsonAsync<GitHubIssue[]>(args);\n}\n\nexport function assignIssue(repo: string, issueNumber: number): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueToAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", user]);\n}\n\nexport async function unassignIssueAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\n \"issue\",\n \"edit\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--remove-assignee\",\n user,\n ]);\n}\n\nexport async function fetchIssueAsync(repo: string, issueNumber: number): Promise<GitHubIssue> {\n return runGhJsonAsync<GitHubIssue>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body,projectStatus\",\n ]);\n}\n\nexport async function closeIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"close\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function reopenIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"reopen\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function createIssueAsync(\n repo: string,\n title: string,\n body: string,\n labels?: string[],\n): Promise<string> {\n const args = [\"issue\", \"create\", \"--repo\", repo, \"--title\", title, \"--body\", body];\n if (labels && labels.length > 0) {\n for (const label of labels) {\n args.push(\"--label\", label);\n }\n }\n return runGhAsync(args);\n}\n\nexport async function editIssueTitleAsync(\n repo: string,\n issueNumber: number,\n title: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--title\", title]);\n}\n\nexport async function editIssueBodyAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addCommentAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"comment\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport async function removeLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--remove-label\", label]);\n}\n\nexport async function updateLabelsAsync(\n repo: string,\n issueNumber: number,\n addLabels: string[],\n removeLabels: string[],\n): Promise<void> {\n const args = [\"issue\", \"edit\", String(issueNumber), \"--repo\", repo];\n for (const label of addLabels) args.push(\"--add-label\", label);\n for (const label of removeLabels) args.push(\"--remove-label\", label);\n await runGhAsync(args);\n}\n\nexport interface IssueComment {\n readonly body: string;\n readonly author: { readonly login: string };\n readonly createdAt: string;\n}\n\nexport async function fetchIssueCommentsAsync(\n repo: string,\n issueNumber: number,\n): Promise<IssueComment[]> {\n const result = await runGhJsonAsync<{ comments: IssueComment[] }>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"comments\",\n ]);\n return result.comments ?? [];\n}\n\nexport function fetchProjectFields(\n repo: string,\n issueNumber: number,\n projectNumber: number,\n): ProjectFieldValues {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return {};\n\n try {\n const projectItem = findProjectItemSync(owner, repoName, issueNumber, projectNumber);\n if (!projectItem) return {};\n\n return parseFieldValues(projectItem.fieldValues?.nodes ?? [], \"status\");\n } catch {\n return {};\n }\n}\n\nexport interface ProjectEnrichment {\n targetDate?: string;\n projectStatus?: string;\n customFields?: Record<string, string>;\n}\n\n/**\n * Fetch target dates and project statuses for all issues in a project in one GraphQL call.\n * Returns a Map from issue number to enrichment data.\n */\nexport function fetchProjectEnrichment(\n repo: string,\n projectNumber: number,\n): Map<number, ProjectEnrichment> {\n const [owner] = repo.split(\"/\");\n if (!owner) return new Map();\n\n const projectItemsFragment = `\n projectV2(number: $projectNumber) {\n items(first: 100, after: $cursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n content {\n ... on Issue {\n number\n }\n }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!, $cursor: String) {\n organization(login: $owner) { ${projectItemsFragment} }\n user(login: $owner) { ${projectItemsFragment} }\n }\n `;\n\n try {\n const enrichMap = new Map<number, ProjectEnrichment>();\n let cursor: string | null = null;\n\n do {\n const args = [\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ];\n if (cursor) args.push(\"-f\", `cursor=${cursor}`);\n const result = runGhGraphQL<ProjectItemsResult>(args);\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n const page = ownerNode?.projectV2?.items;\n const nodes = page?.nodes ?? [];\n\n for (const item of nodes) {\n if (!item?.content?.number) continue;\n const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], \"projectStatus\");\n enrichMap.set(item.content.number, enrichment);\n }\n\n if (!page?.pageInfo?.hasNextPage) break;\n cursor = page.pageInfo.endCursor ?? null;\n } while (cursor);\n\n return enrichMap;\n } catch {\n return new Map();\n }\n}\n\n/** Async version of fetchProjectEnrichment for parallel fetching. */\nexport async function fetchProjectEnrichmentAsync(\n repo: string,\n projectNumber: number,\n): Promise<Map<number, ProjectEnrichment>> {\n const [owner] = repo.split(\"/\");\n if (!owner) return new Map();\n\n const projectItemsFragment = `\n projectV2(number: $projectNumber) {\n items(first: 100, after: $cursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n content {\n ... on Issue {\n number\n }\n }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!, $cursor: String) {\n organization(login: $owner) { ${projectItemsFragment} }\n user(login: $owner) { ${projectItemsFragment} }\n }\n `;\n\n try {\n const enrichMap = new Map<number, ProjectEnrichment>();\n let cursor: string | null = null;\n\n do {\n const args = [\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ];\n if (cursor) args.push(\"-f\", `cursor=${cursor}`);\n const result = await runGhGraphQLAsync<ProjectItemsResult>(args);\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n const page = ownerNode?.projectV2?.items;\n const nodes = page?.nodes ?? [];\n\n for (const item of nodes) {\n if (!item?.content?.number) continue;\n const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], \"projectStatus\");\n enrichMap.set(item.content.number, enrichment);\n }\n\n if (!page?.pageInfo?.hasNextPage) break;\n cursor = page.pageInfo.endCursor ?? null;\n } while (cursor);\n\n return enrichMap;\n } catch {\n // Non-critical: return empty map if project fields fail\n return new Map();\n }\n}\n\n/** Backwards-compatible wrapper for fetchProjectEnrichment. */\nexport function fetchProjectTargetDates(repo: string, projectNumber: number): Map<number, string> {\n const enrichMap = fetchProjectEnrichment(repo, projectNumber);\n const dateMap = new Map<number, string>();\n for (const [num, e] of enrichMap) {\n if (e.targetDate) dateMap.set(num, e.targetDate);\n }\n return dateMap;\n}\n\nexport interface StatusOption {\n id: string;\n name: string;\n}\n\n/**\n * Fetch available project status options (the SingleSelectField values).\n * Returns options in the order defined on the project board.\n */\nexport function fetchProjectStatusOptions(\n repo: string,\n projectNumber: number,\n _statusFieldId: string,\n): StatusOption[] {\n const [owner] = repo.split(\"/\");\n if (!owner) return [];\n\n const statusFragment = `\n projectV2(number: $projectNumber) {\n field(name: \"Status\") {\n ... on ProjectV2SingleSelectField {\n options {\n id\n name\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${statusFragment} }\n user(login: $owner) { ${statusFragment} }\n }\n `;\n\n try {\n const result = runGhGraphQL<ProjectStatusResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n return ownerNode?.projectV2?.field?.options ?? [];\n } catch {\n return [];\n }\n}\n\n/** Async version of fetchProjectStatusOptions for parallel fetching. */\nexport async function fetchProjectStatusOptionsAsync(\n repo: string,\n projectNumber: number,\n _statusFieldId: string,\n): Promise<StatusOption[]> {\n const [owner] = repo.split(\"/\");\n if (!owner) return [];\n\n const statusFragment = `\n projectV2(number: $projectNumber) {\n field(name: \"Status\") {\n ... on ProjectV2SingleSelectField {\n options {\n id\n name\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${statusFragment} }\n user(login: $owner) { ${statusFragment} }\n }\n `;\n\n try {\n const result = await runGhGraphQLAsync<ProjectStatusResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n return ownerNode?.projectV2?.field?.options ?? [];\n } catch {\n // Non-critical: return empty options if project fields fail\n return [];\n }\n}\n\nexport function addLabel(repo: string, issueNumber: number, label: string): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport interface LabelOption {\n name: string;\n color: string;\n}\n\n/**\n * Fetch all labels defined in the repo asynchronously.\n * Uses execFileAsync (not execFileSync) to avoid blocking the React render thread.\n */\nexport async function fetchRepoLabelsAsync(repo: string): Promise<LabelOption[]> {\n try {\n const result = await runGhJsonAsync<LabelOption[]>([\n \"label\",\n \"list\",\n \"--repo\",\n repo,\n \"--json\",\n \"name,color\",\n ]);\n return Array.isArray(result) ? result : [];\n } catch {\n return [];\n }\n}\n\n/** Clears the project node ID cache. Intended for use in tests only. */\nexport function clearProjectNodeIdCache(): void {\n projectNodeIdCache.clear();\n}\n\nexport function updateProjectItemStatus(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): void {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const projectItem = findProjectItemSync(\n owner,\n repoName,\n issueNumber,\n projectConfig.projectNumber,\n );\n if (!projectItem?.id) return;\n\n const projectId = getProjectNodeIdSync(owner, projectConfig.projectNumber);\n if (!projectId) return;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n runGh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${projectConfig.statusFieldId}`,\n \"-F\",\n `optionId=${projectConfig.optionId}`,\n ]);\n}\n\nexport async function updateProjectItemStatusAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const projectItem = await findProjectItemAsync(\n owner,\n repoName,\n issueNumber,\n projectConfig.projectNumber,\n );\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);\n if (!projectId) return;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${projectConfig.statusFieldId}`,\n \"-F\",\n `optionId=${projectConfig.optionId}`,\n ]);\n}\n\nexport interface RepoDueDateConfig {\n projectNumber: number;\n dueDateFieldId: string;\n}\n\n/**\n * Set a date field value on a GitHub Projects v2 item for the given issue.\n * Uses the same 3-step pattern as updateProjectItemStatusAsync.\n */\nexport async function updateProjectItemDateAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoDueDateConfig,\n dueDate: string,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const projectItem = await findProjectItemAsync(\n owner,\n repoName,\n issueNumber,\n projectConfig.projectNumber,\n );\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);\n if (!projectId) return;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { date: $date }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${projectConfig.dueDateFieldId}`,\n \"-F\",\n `date=${dueDate}`,\n ]);\n}\n\ninterface ProjectV2StatusNode {\n projectV2?: {\n field?: {\n options?: StatusOption[];\n };\n };\n}\n\ninterface ProjectStatusResult {\n data?: {\n organization?: ProjectV2StatusNode;\n user?: ProjectV2StatusNode;\n };\n}\n","/** Formats an unknown error as a string. */\nexport function formatError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { HogConfig, RepoConfig } from \"../config.js\";\nimport type { GitHubIssue, StatusOption } from \"../github.js\";\nimport {\n fetchProjectEnrichmentAsync,\n fetchProjectStatusOptionsAsync,\n fetchRepoIssuesAsync,\n} from \"../github.js\";\nimport { formatError } from \"../utils.js\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface RepoData {\n repo: RepoConfig;\n issues: GitHubIssue[];\n statusOptions: StatusOption[];\n error: string | null;\n}\n\nexport interface ActivityEvent {\n type:\n | \"comment\"\n | \"status\"\n | \"assignment\"\n | \"opened\"\n | \"closed\"\n | \"labeled\"\n | \"branch_created\"\n | \"pr_opened\"\n | \"pr_merged\"\n | \"pr_closed\";\n repoShortName: string;\n issueNumber: number;\n actor: string;\n summary: string;\n timestamp: Date;\n /** For branch_created events: the full branch name */\n branchName?: string | undefined;\n /** For pr_* events: the PR number (distinct from linked issueNumber) */\n prNumber?: number | undefined;\n}\n\nexport interface DashboardData {\n repos: RepoData[];\n activity: ActivityEvent[];\n fetchedAt: Date;\n}\n\nexport interface FetchOptions {\n repoFilter?: string | undefined;\n mineOnly?: boolean | undefined;\n backlogOnly?: boolean | undefined;\n}\n\nexport const SLACK_URL_RE = /https:\\/\\/[^/]+\\.slack\\.com\\/archives\\/[A-Z0-9]+\\/p[0-9]+/i;\n\nexport function extractSlackUrl(body: string | undefined): string | undefined {\n if (!body) return undefined;\n const match = body.match(SLACK_URL_RE);\n return match?.[0];\n}\n\n/** Extract issue numbers from a branch name (e.g. \"feat/42-add-auth\" → [42]) */\nexport function extractIssueNumbersFromBranch(branchName: string): number[] {\n // Find numbers that look like issue numbers (1-5 digits, word boundary)\n const matches = branchName.match(/\\b(\\d{1,5})\\b/g);\n if (!matches) return [];\n return [...new Set(matches.map((m) => parseInt(m, 10)).filter((n) => n > 0))];\n}\n\n/** Extract issue numbers linked in PR title/body (e.g. \"Fixes #42\" → [42]) */\nexport function extractLinkedIssueNumbers(title: string | null, body: string | null): number[] {\n const text = `${title ?? \"\"} ${body ?? \"\"}`;\n const matches = text.match(/#(\\d{1,5})\\b/g);\n if (!matches) return [];\n return [...new Set(matches.map((m) => parseInt(m.slice(1), 10)).filter((n) => n > 0))];\n}\n\n/** Parse raw gh CLI activity output into ActivityEvent[]. Shared by sync and async fetchers. */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub event types\nfunction parseActivityOutput(output: string, shortName: string): ActivityEvent[] {\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n const events: ActivityEvent[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const ev = JSON.parse(line) as {\n type: string;\n actor: string;\n action: string;\n number: number | null;\n title: string | null;\n body: string | null;\n created_at: string;\n ref: string | null;\n ref_type: string | null;\n merged: boolean | null;\n };\n\n const timestamp = new Date(ev.created_at);\n if (timestamp.getTime() < cutoff) continue;\n\n if (ev.type === \"CreateEvent\") {\n if (ev.ref_type !== \"branch\" || !ev.ref) continue;\n const issueNumbers = extractIssueNumbersFromBranch(ev.ref);\n for (const num of issueNumbers) {\n events.push({\n type: \"branch_created\",\n repoShortName: shortName,\n issueNumber: num,\n actor: ev.actor,\n summary: `created branch ${ev.ref}`,\n timestamp,\n branchName: ev.ref,\n });\n }\n continue;\n }\n\n if (!ev.number) continue;\n\n let eventType: ActivityEvent[\"type\"];\n let summary: string;\n let extras: Partial<Pick<ActivityEvent, \"prNumber\">> = {};\n\n if (ev.type === \"IssueCommentEvent\") {\n eventType = \"comment\";\n const preview = ev.body ? ev.body.slice(0, 60).replace(/\\n/g, \" \") : \"\";\n summary = `commented on #${ev.number}${preview ? ` — \"${preview}${(ev.body?.length ?? 0) > 60 ? \"...\" : \"\"}\"` : \"\"}`;\n } else if (ev.type === \"IssuesEvent\") {\n switch (ev.action) {\n case \"opened\":\n eventType = \"opened\";\n summary = `opened #${ev.number}: ${ev.title ?? \"\"}`;\n break;\n case \"closed\":\n eventType = \"closed\";\n summary = `closed #${ev.number}`;\n break;\n case \"assigned\":\n eventType = \"assignment\";\n summary = `assigned #${ev.number}`;\n break;\n case \"labeled\":\n eventType = \"labeled\";\n summary = `labeled #${ev.number}`;\n break;\n default:\n continue;\n }\n } else if (ev.type === \"PullRequestEvent\") {\n const prNumber = ev.number;\n extras = { prNumber };\n if (ev.action === \"opened\") {\n eventType = \"pr_opened\";\n summary = `opened PR #${prNumber}: ${ev.title ?? \"\"}`;\n } else if (ev.action === \"closed\" && ev.merged) {\n eventType = \"pr_merged\";\n summary = `merged PR #${prNumber}: ${ev.title ?? \"\"}`;\n } else if (ev.action === \"closed\") {\n eventType = \"pr_closed\";\n summary = `closed PR #${prNumber}`;\n } else {\n continue;\n }\n\n const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);\n for (const issueNum of linkedIssues) {\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: issueNum,\n actor: ev.actor,\n summary,\n timestamp,\n prNumber,\n });\n }\n if (linkedIssues.length === 0) {\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: prNumber,\n actor: ev.actor,\n summary,\n timestamp,\n prNumber,\n });\n }\n continue;\n } else {\n continue;\n }\n\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: ev.number,\n actor: ev.actor,\n summary,\n timestamp,\n ...extras,\n });\n } catch {\n // Skip malformed event JSON\n }\n }\n\n return events.slice(0, 15);\n}\n\n/** Fetch recent activity events for a repo (last 24h, max 30 events) */\nexport function fetchRecentActivity(repoName: string, shortName: string): ActivityEvent[] {\n try {\n const output = execFileSync(\n \"gh\",\n [\n \"api\",\n `repos/${repoName}/events`,\n \"-f\",\n \"per_page=30\",\n \"-q\",\n '.[] | select(.type == \"IssuesEvent\" or .type == \"IssueCommentEvent\" or .type == \"PullRequestEvent\" or .type == \"CreateEvent\") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: (.payload.comment.body // .payload.pull_request.body), created_at: .created_at, ref: .payload.ref, ref_type: .payload.ref_type, merged: .payload.pull_request.merged}',\n ],\n { encoding: \"utf-8\", timeout: 15_000, stdio: \"pipe\" },\n );\n return parseActivityOutput(output, shortName);\n } catch {\n // Best-effort: activity fetch is non-critical\n return [];\n }\n}\n\n/** Fetch a single repo's data asynchronously. */\nasync function fetchRepoDataAsync(\n repo: RepoConfig,\n assignee?: string | undefined,\n): Promise<RepoData> {\n try {\n const fetchOpts: { assignee?: string } = {};\n if (assignee) fetchOpts.assignee = assignee;\n const issues = await fetchRepoIssuesAsync(repo.name, fetchOpts);\n\n // Enrich issues with target dates + statuses from GitHub Projects (parallel)\n let enrichedIssues = issues;\n let statusOptions: StatusOption[] = [];\n try {\n const [enrichMap, opts] = await Promise.all([\n fetchProjectEnrichmentAsync(repo.name, repo.projectNumber),\n fetchProjectStatusOptionsAsync(repo.name, repo.projectNumber, repo.statusFieldId),\n ]);\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const e = enrichMap.get(issue.number);\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return {\n ...issue,\n ...(e?.targetDate !== undefined ? { targetDate: e.targetDate } : {}),\n ...(e?.projectStatus !== undefined ? { projectStatus: e.projectStatus } : {}),\n ...(e?.customFields !== undefined ? { customFields: e.customFields } : {}),\n ...(slackUrl ? { slackThreadUrl: slackUrl } : {}),\n };\n });\n statusOptions = opts;\n } catch {\n // Non-critical: silently skip if project fields fail\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;\n });\n }\n\n return { repo, issues: enrichedIssues, statusOptions, error: null };\n } catch (err) {\n return { repo, issues: [], statusOptions: [], error: formatError(err) };\n }\n}\n\n/** Fetch recent activity for a repo asynchronously. */\nasync function fetchRecentActivityAsync(\n repoName: string,\n shortName: string,\n): Promise<ActivityEvent[]> {\n try {\n const { stdout } = await execFileAsync(\n \"gh\",\n [\n \"api\",\n `repos/${repoName}/events`,\n \"-f\",\n \"per_page=30\",\n \"-q\",\n '.[] | select(.type == \"IssuesEvent\" or .type == \"IssueCommentEvent\" or .type == \"PullRequestEvent\" or .type == \"CreateEvent\") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: (.payload.comment.body // .payload.pull_request.body), created_at: .created_at, ref: .payload.ref, ref_type: .payload.ref_type, merged: .payload.pull_request.merged}',\n ],\n { encoding: \"utf-8\", timeout: 15_000 },\n );\n return parseActivityOutput(stdout, shortName);\n } catch {\n // Best-effort: activity fetch is non-critical\n return [];\n }\n}\n\nexport async function fetchDashboard(\n config: HogConfig,\n options: FetchOptions = {},\n): Promise<DashboardData> {\n const repos = options.repoFilter\n ? config.repos.filter(\n (r) => r.shortName === options.repoFilter || r.name === options.repoFilter,\n )\n : config.repos;\n\n const assignee = options.mineOnly ? config.board.assignee : undefined;\n\n // Fetch all repos and activity in parallel\n const [repoData, ...activityResults] = await Promise.all([\n Promise.all(repos.map((repo) => fetchRepoDataAsync(repo, assignee))),\n ...repos.map((repo) => fetchRecentActivityAsync(repo.name, repo.shortName)),\n ]);\n\n // Merge and sort activity by timestamp descending\n const activity = activityResults\n .flat()\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());\n\n return {\n repos: repoData,\n activity: activity.slice(0, 15),\n fetchedAt: new Date(),\n };\n}\n","import { parentPort, workerData } from \"node:worker_threads\";\nimport type { HogConfig } from \"../config.js\";\nimport type { FetchOptions } from \"./fetch.js\";\n\nconst { config, options } = workerData as { config: HogConfig; options: FetchOptions };\n\nconst { fetchDashboard } = await import(\"./fetch.js\");\n\nif (!parentPort) throw new Error(\"fetch-worker must run in a worker thread\");\n\ntry {\n const data = await fetchDashboard(config, options);\n parentPort.postMessage({ type: \"success\", data });\n} catch (err) {\n parentPort.postMessage({\n type: \"error\",\n error: err instanceof Error ? err.message : String(err),\n });\n}\n"],"mappings":"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","names":["options","execFile","execFileSync","promisify","execFileAsync","config","options","fetchDashboard"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondrej-svec/hog",
3
- "version": "1.23.1",
3
+ "version": "1.24.0",
4
4
  "description": "Personal command deck — unified task dashboard for GitHub Projects + TickTick",
5
5
  "author": "Ondrej Svec",
6
6
  "repository": {