@ondrej-svec/hog 1.24.2 → 1.25.1

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.
@@ -74,51 +74,22 @@ async function fetchRepoIssuesAsync(repo, options2 = {}) {
74
74
  }
75
75
  return runGhJsonAsync(args);
76
76
  }
77
+ function accumulateEnrichment(nodes, targetRepo, enrichMap) {
78
+ for (const item of nodes) {
79
+ if (!item?.content?.number) continue;
80
+ const itemRepo = item.content.repository?.nameWithOwner;
81
+ if (itemRepo && itemRepo !== targetRepo) continue;
82
+ const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], "projectStatus");
83
+ enrichMap.set(item.content.number, enrichment);
84
+ }
85
+ }
77
86
  async function fetchProjectEnrichmentAsync(repo, projectNumber) {
78
87
  const [owner] = repo.split("/");
79
88
  if (!owner) return /* @__PURE__ */ new Map();
80
- const projectItemsFragment = `
81
- projectV2(number: $projectNumber) {
82
- items(first: 100, after: $cursor) {
83
- pageInfo { hasNextPage endCursor }
84
- nodes {
85
- content {
86
- ... on Issue {
87
- number
88
- }
89
- }
90
- fieldValues(first: 20) {
91
- nodes {
92
- ... on ProjectV2ItemFieldDateValue {
93
- field { ... on ProjectV2Field { name } }
94
- date
95
- }
96
- ... on ProjectV2ItemFieldSingleSelectValue {
97
- field { ... on ProjectV2SingleSelectField { name } }
98
- name
99
- }
100
- ... on ProjectV2ItemFieldTextValue {
101
- field { ... on ProjectV2Field { name } }
102
- text
103
- }
104
- ... on ProjectV2ItemFieldNumberValue {
105
- field { ... on ProjectV2Field { name } }
106
- number
107
- }
108
- ... on ProjectV2ItemFieldIterationValue {
109
- field { ... on ProjectV2IterationField { name } }
110
- title
111
- }
112
- }
113
- }
114
- }
115
- }
116
- }
117
- `;
118
89
  const query = `
119
90
  query($owner: String!, $projectNumber: Int!, $cursor: String) {
120
- organization(login: $owner) { ${projectItemsFragment} }
121
- user(login: $owner) { ${projectItemsFragment} }
91
+ organization(login: $owner) { ${PROJECT_ITEMS_FRAGMENT} }
92
+ user(login: $owner) { ${PROJECT_ITEMS_FRAGMENT} }
122
93
  }
123
94
  `;
124
95
  try {
@@ -139,12 +110,7 @@ async function fetchProjectEnrichmentAsync(repo, projectNumber) {
139
110
  const result = await runGhGraphQLAsync(args);
140
111
  const ownerNode = result?.data?.organization ?? result?.data?.user;
141
112
  const page = ownerNode?.projectV2?.items;
142
- const nodes = page?.nodes ?? [];
143
- for (const item of nodes) {
144
- if (!item?.content?.number) continue;
145
- const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], "projectStatus");
146
- enrichMap.set(item.content.number, enrichment);
147
- }
113
+ accumulateEnrichment(page?.nodes ?? [], repo, enrichMap);
148
114
  if (!page?.pageInfo?.hasNextPage) break;
149
115
  cursor = page.pageInfo.endCursor ?? null;
150
116
  } while (cursor);
@@ -191,12 +157,51 @@ async function fetchProjectStatusOptionsAsync(repo, projectNumber, _statusFieldI
191
157
  return [];
192
158
  }
193
159
  }
194
- var execFileAsync, DATE_FIELD_NAME_RE;
160
+ var execFileAsync, DATE_FIELD_NAME_RE, PROJECT_ITEMS_FRAGMENT;
195
161
  var init_github = __esm({
196
162
  "src/github.ts"() {
197
163
  "use strict";
198
164
  execFileAsync = promisify(execFile);
199
165
  DATE_FIELD_NAME_RE = /^(target\s*date|due\s*date|due|deadline)$/i;
166
+ PROJECT_ITEMS_FRAGMENT = `
167
+ projectV2(number: $projectNumber) {
168
+ items(first: 100, after: $cursor) {
169
+ pageInfo { hasNextPage endCursor }
170
+ nodes {
171
+ content {
172
+ ... on Issue {
173
+ number
174
+ repository { nameWithOwner }
175
+ }
176
+ }
177
+ fieldValues(first: 20) {
178
+ nodes {
179
+ ... on ProjectV2ItemFieldDateValue {
180
+ field { ... on ProjectV2Field { name } }
181
+ date
182
+ }
183
+ ... on ProjectV2ItemFieldSingleSelectValue {
184
+ field { ... on ProjectV2SingleSelectField { name } }
185
+ name
186
+ }
187
+ ... on ProjectV2ItemFieldTextValue {
188
+ field { ... on ProjectV2Field { name } }
189
+ text
190
+ }
191
+ ... on ProjectV2ItemFieldNumberValue {
192
+ field { ... on ProjectV2Field { name } }
193
+ number
194
+ }
195
+ ... on ProjectV2ItemFieldIterationValue {
196
+ field { ... on ProjectV2IterationField { name } }
197
+ title
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ `;
200
205
  }
201
206
  });
202
207
 
@@ -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\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":";;;;;;;;;;;AAAA,SAAS,UAAU,oBAAoB;AACvC,SAAS,iBAAiB;AAiD1B,eAAe,WAAW,MAAiC;AACzD,QAAM,EAAE,OAAO,IAAI,MAAM,cAAc,MAAM,MAAM,EAAE,UAAU,SAAS,SAAS,IAAO,CAAC;AACzF,SAAO,OAAO,KAAK;AACrB;AAEA,eAAe,eAAkB,MAA4B;AAC3D,QAAM,SAAS,MAAM,WAAW,IAAI;AACpC,SAAO,KAAK,MAAM,MAAM;AAC1B;AA2BA,eAAe,kBAAqB,MAA4B;AAC9D,MAAI;AACF,WAAO,MAAM,eAAkB,IAAI;AAAA,EACrC,SAAS,KAAc;AACrB,QAAI,OAAO,OAAO,QAAQ,YAAY,YAAY,KAAK;AACrD,YAAM,SAAU,IAAoC;AACpD,YAAM,SAAS,OAAO,WAAW,WAAW,SAAS,QAAQ,SAAS,OAAO;AAC7E,UAAI,QAAQ;AACV,YAAI;AACF,iBAAO,KAAK,MAAM,OAAO,KAAK,CAAC;AAAA,QACjC,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAiKA,SAAS,iBACP,aACA,WACwC;AACxC,QAAM,SAAiD,CAAC;AACxD,aAAW,MAAM,aAAa;AAC5B,QAAI,CAAC,GAAI;AACT,UAAM,YAAY,GAAG,OAAO,QAAQ;AACpC,QAAI,UAAU,MAAM,GAAG,QAAQ,mBAAmB,KAAK,SAAS,GAAG;AACjE,aAAO,aAAa,GAAG;AAAA,IACzB,WAAW,UAAU,MAAM,cAAc,YAAY,GAAG,MAAM;AAC5D,MAAC,OAAmC,SAAS,IAAI,GAAG;AAAA,IACtD,WAAW,WAAW;AACpB,YAAM,QACJ,UAAU,MAAM,GAAG,QAAQ,OACvB,GAAG,OACH,YAAY,MAAM,GAAG,UAAU,OAC7B,OAAO,GAAG,MAAM,IAChB,UAAU,MAAM,GAAG,QAAQ,OACzB,GAAG,OACH,WAAW,MAAM,GAAG,SAAS,OAC3B,GAAG,QACH;AACZ,UAAI,SAAS,MAAM;AACjB,YAAI,CAAC,OAAO,aAAc,QAAO,eAAe,CAAC;AACjD,eAAO,aAAa,SAAS,IAAI;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAsHA,eAAsB,qBACpB,MACAA,WAA8B,CAAC,GACP;AACxB,QAAM,EAAE,QAAQ,QAAQ,QAAQ,IAAI,IAAIA;AACxC,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,KAAK;AAAA,EACd;AACA,MAAIA,SAAQ,UAAU;AACpB,SAAK,KAAK,cAAcA,SAAQ,QAAQ;AAAA,EAC1C;AACA,SAAO,eAA8B,IAAI;AAC3C;AAsQA,eAAsB,4BACpB,MACA,eACyC;AACzC,QAAM,CAAC,KAAK,IAAI,KAAK,MAAM,GAAG;AAC9B,MAAI,CAAC,MAAO,QAAO,oBAAI,IAAI;AAE3B,QAAM,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuC7B,QAAM,QAAQ;AAAA;AAAA,sCAEsB,oBAAoB;AAAA,8BAC5B,oBAAoB;AAAA;AAAA;AAIhD,MAAI;AACF,UAAM,YAAY,oBAAI,IAA+B;AACrD,QAAI,SAAwB;AAE5B,OAAG;AACD,YAAM,OAAO;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,KAAK;AAAA,QACd;AAAA,QACA,SAAS,KAAK;AAAA,QACd;AAAA,QACA,iBAAiB,OAAO,aAAa,CAAC;AAAA,MACxC;AACA,UAAI,OAAQ,MAAK,KAAK,MAAM,UAAU,MAAM,EAAE;AAC9C,YAAM,SAAS,MAAM,kBAAsC,IAAI;AAC/D,YAAM,YAAY,QAAQ,MAAM,gBAAgB,QAAQ,MAAM;AAC9D,YAAM,OAAO,WAAW,WAAW;AACnC,YAAM,QAAQ,MAAM,SAAS,CAAC;AAE9B,iBAAW,QAAQ,OAAO;AACxB,YAAI,CAAC,MAAM,SAAS,OAAQ;AAC5B,cAAM,aAAa,iBAAiB,KAAK,aAAa,SAAS,CAAC,GAAG,eAAe;AAClF,kBAAU,IAAI,KAAK,QAAQ,QAAQ,UAAU;AAAA,MAC/C;AAEA,UAAI,CAAC,MAAM,UAAU,YAAa;AAClC,eAAS,KAAK,SAAS,aAAa;AAAA,IACtC,SAAS;AAET,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO,oBAAI,IAAI;AAAA,EACjB;AACF;AAqEA,eAAsB,+BACpB,MACA,eACA,gBACyB;AACzB,QAAM,CAAC,KAAK,IAAI,KAAK,MAAM,GAAG;AAC9B,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,QAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAavB,QAAM,QAAQ;AAAA;AAAA,sCAEsB,cAAc;AAAA,8BACtB,cAAc;AAAA;AAAA;AAI1C,MAAI;AACF,UAAM,SAAS,MAAM,kBAAuC;AAAA,MAC1D;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,KAAK;AAAA,MACd;AAAA,MACA,SAAS,KAAK;AAAA,MACd;AAAA,MACA,iBAAiB,OAAO,aAAa,CAAC;AAAA,IACxC,CAAC;AAED,UAAM,YAAY,QAAQ,MAAM,gBAAgB,QAAQ,MAAM;AAC9D,WAAO,WAAW,WAAW,OAAO,WAAW,CAAC;AAAA,EAClD,QAAQ;AAEN,WAAO,CAAC;AAAA,EACV;AACF;AAl4BA,IAGM,eAoCA;AAvCN;AAAA;AAAA;AAGA,IAAM,gBAAgB,UAAU,QAAQ;AAoCxC,IAAM,qBAAqB;AAAA;AAAA;;;ACtCpB,SAAS,YAAY,KAAsB;AAChD,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAHA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,YAAAC,WAAU,gBAAAC,qBAAoB;AACvC,SAAS,aAAAC,kBAAiB;AAwDnB,SAAS,gBAAgB,MAA8C;AAC5E,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,KAAK,MAAM,YAAY;AACrC,SAAO,QAAQ,CAAC;AAClB;AAGO,SAAS,8BAA8B,YAA8B;AAE1E,QAAM,UAAU,WAAW,MAAM,gBAAgB;AACjD,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,SAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;AAC9E;AAGO,SAAS,0BAA0B,OAAsB,MAA+B;AAC7F,QAAM,OAAO,GAAG,SAAS,EAAE,IAAI,QAAQ,EAAE;AACzC,QAAM,UAAU,KAAK,MAAM,eAAe;AAC1C,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,SAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,SAAS,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;AACvF;AAIA,SAAS,oBAAoB,QAAgB,WAAoC;AAC/E,QAAM,SAAS,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK;AAC3C,QAAM,SAA0B,CAAC;AAEjC,aAAW,QAAQ,OAAO,KAAK,EAAE,MAAM,IAAI,GAAG;AAC5C,QAAI,CAAC,KAAK,KAAK,EAAG;AAClB,QAAI;AACF,YAAM,KAAK,KAAK,MAAM,IAAI;AAa1B,YAAM,YAAY,IAAI,KAAK,GAAG,UAAU;AACxC,UAAI,UAAU,QAAQ,IAAI,OAAQ;AAElC,UAAI,GAAG,SAAS,eAAe;AAC7B,YAAI,GAAG,aAAa,YAAY,CAAC,GAAG,IAAK;AACzC,cAAM,eAAe,8BAA8B,GAAG,GAAG;AACzD,mBAAW,OAAO,cAAc;AAC9B,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN,eAAe;AAAA,YACf,aAAa;AAAA,YACb,OAAO,GAAG;AAAA,YACV,SAAS,kBAAkB,GAAG,GAAG;AAAA,YACjC;AAAA,YACA,YAAY,GAAG;AAAA,UACjB,CAAC;AAAA,QACH;AACA;AAAA,MACF;AAEA,UAAI,CAAC,GAAG,OAAQ;AAEhB,UAAI;AACJ,UAAI;AACJ,UAAI,SAAmD,CAAC;AAExD,UAAI,GAAG,SAAS,qBAAqB;AACnC,oBAAY;AACZ,cAAM,UAAU,GAAG,OAAO,GAAG,KAAK,MAAM,GAAG,EAAE,EAAE,QAAQ,OAAO,GAAG,IAAI;AACrE,kBAAU,iBAAiB,GAAG,MAAM,GAAG,UAAU,YAAO,OAAO,IAAI,GAAG,MAAM,UAAU,KAAK,KAAK,QAAQ,EAAE,MAAM,EAAE;AAAA,MACpH,WAAW,GAAG,SAAS,eAAe;AACpC,gBAAQ,GAAG,QAAQ;AAAA,UACjB,KAAK;AACH,wBAAY;AACZ,sBAAU,WAAW,GAAG,MAAM,KAAK,GAAG,SAAS,EAAE;AACjD;AAAA,UACF,KAAK;AACH,wBAAY;AACZ,sBAAU,WAAW,GAAG,MAAM;AAC9B;AAAA,UACF,KAAK;AACH,wBAAY;AACZ,sBAAU,aAAa,GAAG,MAAM;AAChC;AAAA,UACF,KAAK;AACH,wBAAY;AACZ,sBAAU,YAAY,GAAG,MAAM;AAC/B;AAAA,UACF;AACE;AAAA,QACJ;AAAA,MACF,WAAW,GAAG,SAAS,oBAAoB;AACzC,cAAM,WAAW,GAAG;AACpB,iBAAS,EAAE,SAAS;AACpB,YAAI,GAAG,WAAW,UAAU;AAC1B,sBAAY;AACZ,oBAAU,cAAc,QAAQ,KAAK,GAAG,SAAS,EAAE;AAAA,QACrD,WAAW,GAAG,WAAW,YAAY,GAAG,QAAQ;AAC9C,sBAAY;AACZ,oBAAU,cAAc,QAAQ,KAAK,GAAG,SAAS,EAAE;AAAA,QACrD,WAAW,GAAG,WAAW,UAAU;AACjC,sBAAY;AACZ,oBAAU,cAAc,QAAQ;AAAA,QAClC,OAAO;AACL;AAAA,QACF;AAEA,cAAM,eAAe,0BAA0B,GAAG,OAAO,GAAG,IAAI;AAChE,mBAAW,YAAY,cAAc;AACnC,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN,eAAe;AAAA,YACf,aAAa;AAAA,YACb,OAAO,GAAG;AAAA,YACV;AAAA,YACA;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH;AACA,YAAI,aAAa,WAAW,GAAG;AAC7B,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN,eAAe;AAAA,YACf,aAAa;AAAA,YACb,OAAO,GAAG;AAAA,YACV;AAAA,YACA;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH;AACA;AAAA,MACF,OAAO;AACL;AAAA,MACF;AAEA,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,eAAe;AAAA,QACf,aAAa,GAAG;AAAA,QAChB,OAAO,GAAG;AAAA,QACV;AAAA,QACA;AAAA,QACA,GAAG;AAAA,MACL,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,OAAO,MAAM,GAAG,EAAE;AAC3B;AAGO,SAAS,oBAAoB,UAAkB,WAAoC;AACxF,MAAI;AACF,UAAM,SAASD;AAAA,MACb;AAAA,MACA;AAAA,QACE;AAAA,QACA,SAAS,QAAQ;AAAA,QACjB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,EAAE,UAAU,SAAS,SAAS,MAAQ,OAAO,OAAO;AAAA,IACtD;AACA,WAAO,oBAAoB,QAAQ,SAAS;AAAA,EAC9C,QAAQ;AAEN,WAAO,CAAC;AAAA,EACV;AACF;AAGA,eAAe,mBACb,MACA,UACmB;AACnB,MAAI;AACF,UAAM,YAAmC,CAAC;AAC1C,QAAI,SAAU,WAAU,WAAW;AACnC,UAAM,SAAS,MAAM,qBAAqB,KAAK,MAAM,SAAS;AAG9D,QAAI,iBAAiB;AACrB,QAAI,gBAAgC,CAAC;AACrC,QAAI;AACF,YAAM,CAAC,WAAW,IAAI,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC1C,4BAA4B,KAAK,MAAM,KAAK,aAAa;AAAA,QACzD,+BAA+B,KAAK,MAAM,KAAK,eAAe,KAAK,aAAa;AAAA,MAClF,CAAC;AACD,uBAAiB,OAAO,IAAI,CAAC,UAAuB;AAClD,cAAM,IAAI,UAAU,IAAI,MAAM,MAAM;AACpC,cAAM,WAAW,gBAAgB,MAAM,QAAQ,EAAE;AACjD,eAAO;AAAA,UACL,GAAG;AAAA,UACH,GAAI,GAAG,eAAe,SAAY,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,UAClE,GAAI,GAAG,kBAAkB,SAAY,EAAE,eAAe,EAAE,cAAc,IAAI,CAAC;AAAA,UAC3E,GAAI,GAAG,iBAAiB,SAAY,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;AAAA,UACxE,GAAI,WAAW,EAAE,gBAAgB,SAAS,IAAI,CAAC;AAAA,QACjD;AAAA,MACF,CAAC;AACD,sBAAgB;AAAA,IAClB,QAAQ;AAEN,uBAAiB,OAAO,IAAI,CAAC,UAAuB;AAClD,cAAM,WAAW,gBAAgB,MAAM,QAAQ,EAAE;AACjD,eAAO,WAAW,EAAE,GAAG,OAAO,gBAAgB,SAAS,IAAI;AAAA,MAC7D,CAAC;AAAA,IACH;AAEA,WAAO,EAAE,MAAM,QAAQ,gBAAgB,eAAe,OAAO,KAAK;AAAA,EACpE,SAAS,KAAK;AACZ,WAAO,EAAE,MAAM,QAAQ,CAAC,GAAG,eAAe,CAAC,GAAG,OAAO,YAAY,GAAG,EAAE;AAAA,EACxE;AACF;AAGA,eAAe,yBACb,UACA,WAC0B;AAC1B,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAME;AAAA,MACvB;AAAA,MACA;AAAA,QACE;AAAA,QACA,SAAS,QAAQ;AAAA,QACjB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,EAAE,UAAU,SAAS,SAAS,KAAO;AAAA,IACvC;AACA,WAAO,oBAAoB,QAAQ,SAAS;AAAA,EAC9C,QAAQ;AAEN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,eACpBC,SACAC,WAAwB,CAAC,GACD;AACxB,QAAM,QAAQA,SAAQ,aAClBD,QAAO,MAAM;AAAA,IACX,CAAC,MAAM,EAAE,cAAcC,SAAQ,cAAc,EAAE,SAASA,SAAQ;AAAA,EAClE,IACAD,QAAO;AAEX,QAAM,WAAWC,SAAQ,WAAWD,QAAO,MAAM,WAAW;AAG5D,QAAM,CAAC,UAAU,GAAG,eAAe,IAAI,MAAM,QAAQ,IAAI;AAAA,IACvD,QAAQ,IAAI,MAAM,IAAI,CAAC,SAAS,mBAAmB,MAAM,QAAQ,CAAC,CAAC;AAAA,IACnE,GAAG,MAAM,IAAI,CAAC,SAAS,yBAAyB,KAAK,MAAM,KAAK,SAAS,CAAC;AAAA,EAC5E,CAAC;AAGD,QAAM,WAAW,gBACd,KAAK,EACL,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ,CAAC;AAE/D,SAAO;AAAA,IACL,OAAO;AAAA,IACP,UAAU,SAAS,MAAM,GAAG,EAAE;AAAA,IAC9B,WAAW,oBAAI,KAAK;AAAA,EACtB;AACF;AA5UA,IAWMD,gBA4CO;AAvDb;AAAA;AAAA;AAIA;AAKA;AAEA,IAAMA,iBAAgBD,WAAUF,SAAQ;AA4CjC,IAAM,eAAe;AAAA;AAAA;;;ACvD5B,SAAS,YAAY,kBAAkB;AAIvC,IAAM,EAAE,QAAQ,QAAQ,IAAI;AAE5B,IAAM,EAAE,gBAAAM,gBAAe,IAAI,MAAM;AAEjC,IAAI,CAAC,WAAY,OAAM,IAAI,MAAM,0CAA0C;AAE3E,IAAI;AACF,QAAM,OAAO,MAAMA,gBAAe,QAAQ,OAAO;AACjD,aAAW,YAAY,EAAE,MAAM,WAAW,KAAK,CAAC;AAClD,SAAS,KAAK;AACZ,aAAW,YAAY;AAAA,IACrB,MAAM;AAAA,IACN,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,EACxD,CAAC;AACH;","names":["options","execFile","execFileSync","promisify","execFileAsync","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; repository?: { nameWithOwner?: string } };\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/** Shared GraphQL fragment for fetching project items with repo info. */\nconst PROJECT_ITEMS_FRAGMENT = `\n projectV2(number: $projectNumber) {\n items(first: 100, after: $cursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n content {\n ... on Issue {\n number\n repository { nameWithOwner }\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/** Accumulate enrichment data from a page of project items, filtering by target repo. */\nfunction accumulateEnrichment(\n nodes: (ProjectItemNode | null)[],\n targetRepo: string,\n enrichMap: Map<number, ProjectEnrichment>,\n): void {\n for (const item of nodes) {\n if (!item?.content?.number) continue;\n // Skip items from other repos to avoid issue number collisions\n const itemRepo = item.content.repository?.nameWithOwner;\n if (itemRepo && itemRepo !== targetRepo) continue;\n const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], \"projectStatus\");\n enrichMap.set(item.content.number, enrichment);\n }\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 * Projects can contain items from multiple repos, so we filter by the target repo\n * to avoid cross-repo issue number collisions overwriting statuses.\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 query = `\n query($owner: String!, $projectNumber: Int!, $cursor: String) {\n organization(login: $owner) { ${PROJECT_ITEMS_FRAGMENT} }\n user(login: $owner) { ${PROJECT_ITEMS_FRAGMENT} }\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 accumulateEnrichment(page?.nodes ?? [], repo, enrichMap);\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 query = `\n query($owner: String!, $projectNumber: Int!, $cursor: String) {\n organization(login: $owner) { ${PROJECT_ITEMS_FRAGMENT} }\n user(login: $owner) { ${PROJECT_ITEMS_FRAGMENT} }\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 accumulateEnrichment(page?.nodes ?? [], repo, enrichMap);\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.24.2",
3
+ "version": "1.25.1",
4
4
  "description": "Personal command deck — unified task dashboard for GitHub Projects + TickTick",
5
5
  "author": "Ondrej Svec",
6
6
  "repository": {