@ondrej-svec/hog 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api.ts","../src/config.ts","../src/github.ts","../src/types.ts","../src/utils.ts","../src/board/fetch.ts","../src/board/fetch-worker.ts"],"sourcesContent":["import type { CreateTaskInput, Project, ProjectData, Task, UpdateTaskInput } from \"./types.js\";\n\nconst BASE_URL = \"https://api.ticktick.com/open/v1\";\n\nexport class TickTickClient {\n private token: string;\n\n constructor(token: string) {\n this.token = token;\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T | null> {\n const url = `${BASE_URL}${path}`;\n\n const init: RequestInit = {\n method,\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/json\",\n },\n };\n\n if (body !== undefined) {\n init.body = JSON.stringify(body);\n }\n\n const res = await fetch(url, init);\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`TickTick API error ${res.status}: ${text}`);\n }\n\n const text = await res.text();\n if (!text) return null;\n return JSON.parse(text) as T;\n }\n\n async listProjects(): Promise<Project[]> {\n return (await this.request<Project[]>(\"GET\", \"/project\")) ?? [];\n }\n\n async getProject(projectId: string): Promise<Project> {\n const result = await this.request<Project>(\"GET\", `/project/${projectId}`);\n if (!result) throw new Error(`TickTick API returned empty response for project ${projectId}`);\n return result;\n }\n\n async getProjectData(projectId: string): Promise<ProjectData> {\n const result = await this.request<ProjectData>(\"GET\", `/project/${projectId}/data`);\n if (!result)\n throw new Error(`TickTick API returned empty response for project data ${projectId}`);\n return result;\n }\n\n async listTasks(projectId: string): Promise<Task[]> {\n const data = await this.getProjectData(projectId);\n return data.tasks ?? [];\n }\n\n async getTask(projectId: string, taskId: string): Promise<Task> {\n const result = await this.request<Task>(\"GET\", `/project/${projectId}/task/${taskId}`);\n if (!result) throw new Error(`TickTick API returned empty response for task ${taskId}`);\n return result;\n }\n\n async createTask(input: CreateTaskInput): Promise<Task> {\n const result = await this.request<Task>(\"POST\", \"/task\", input);\n if (!result) throw new Error(\"TickTick API returned empty response for createTask\");\n return result;\n }\n\n async updateTask(input: UpdateTaskInput): Promise<Task> {\n const result = await this.request<Task>(\"POST\", `/task/${input.id}`, input);\n if (!result) throw new Error(`TickTick API returned empty response for updateTask ${input.id}`);\n return result;\n }\n\n async completeTask(projectId: string, taskId: string): Promise<void> {\n await this.request<void>(\"POST\", `/project/${projectId}/task/${taskId}/complete`);\n }\n\n async deleteTask(projectId: string, taskId: string): Promise<void> {\n await this.request<void>(\"DELETE\", `/project/${projectId}/task/${taskId}`);\n }\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join, normalize } from \"node:path\";\nimport { z } from \"zod\";\n\nexport const CONFIG_DIR = join(homedir(), \".config\", \"hog\");\nconst AUTH_FILE = join(CONFIG_DIR, \"auth.json\");\nconst CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\nconst AUTH_SCHEMA = z.object({\n accessToken: z.string(),\n clientId: z.string(),\n clientSecret: z.string(),\n openrouterApiKey: z.string().optional(),\n});\n\ntype AuthData = z.infer<typeof AUTH_SCHEMA>;\n\n// ── Config Schema (Zod) ──\n\nconst COMPLETION_ACTION_SCHEMA = z.discriminatedUnion(\"type\", [\n z.object({ type: z.literal(\"updateProjectStatus\"), optionId: z.string() }),\n z.object({ type: z.literal(\"closeIssue\") }),\n z.object({ type: z.literal(\"addLabel\"), label: z.string() }),\n]);\n\nconst REPO_NAME_PATTERN = /^[\\w.-]+\\/[\\w.-]+$/;\n\nconst CLAUDE_START_COMMAND_SCHEMA = z.object({\n command: z.string().min(1),\n extraArgs: z.array(z.string()),\n});\n\nconst REPO_CONFIG_SCHEMA = z.object({\n name: z.string().regex(REPO_NAME_PATTERN, \"Must be owner/repo format\"),\n shortName: z.string().min(1),\n projectNumber: z.number().int().positive(),\n statusFieldId: z.string().min(1),\n dueDateFieldId: z.string().optional(),\n completionAction: COMPLETION_ACTION_SCHEMA,\n statusGroups: z.array(z.string()).optional(),\n localPath: z\n .string()\n .refine((p) => isAbsolute(p), { message: \"localPath must be an absolute path\" })\n .refine((p) => normalize(p) === p, {\n message: \"localPath must be normalized (no .. segments)\",\n })\n .refine((p) => !p.includes(\"\\0\"), { message: \"localPath must not contain null bytes\" })\n .optional(),\n claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional(),\n claudePrompt: z.string().optional(),\n});\n\nconst BOARD_CONFIG_SCHEMA = z.object({\n refreshInterval: z.number().int().min(10).default(60),\n backlogLimit: z.number().int().min(1).default(20),\n assignee: z.string().min(1),\n focusDuration: z.number().int().min(60).default(1500),\n claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional(),\n claudePrompt: z.string().optional(),\n claudeLaunchMode: z.enum([\"auto\", \"tmux\", \"terminal\"]).optional(),\n claudeTerminalApp: z\n .enum([\"Terminal\", \"iTerm\", \"Ghostty\", \"WezTerm\", \"Kitty\", \"Alacritty\"])\n .optional(),\n});\n\nconst TICKTICK_CONFIG_SCHEMA = z.object({\n enabled: z.boolean().default(true),\n});\n\nconst PROFILE_SCHEMA = z.object({\n repos: z.array(REPO_CONFIG_SCHEMA).default([]),\n board: BOARD_CONFIG_SCHEMA,\n ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true }),\n});\n\nconst HOG_CONFIG_SCHEMA = z.object({\n version: z.number().int().default(3),\n defaultProjectId: z.string().optional(),\n defaultProjectName: z.string().optional(),\n repos: z.array(REPO_CONFIG_SCHEMA).default([]),\n board: BOARD_CONFIG_SCHEMA,\n ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true }),\n profiles: z.record(z.string(), PROFILE_SCHEMA).default({}),\n defaultProfile: z.string().optional(),\n});\n\nexport type CompletionAction = z.infer<typeof COMPLETION_ACTION_SCHEMA>;\nexport type RepoConfig = z.infer<typeof REPO_CONFIG_SCHEMA>;\nexport type BoardConfig = z.infer<typeof BOARD_CONFIG_SCHEMA>;\nexport type ProfileConfig = z.infer<typeof PROFILE_SCHEMA>;\nexport type HogConfig = z.infer<typeof HOG_CONFIG_SCHEMA>;\n\n// ── Config Migration ──\n\nfunction migrateConfig(raw: Record<string, unknown>): HogConfig {\n const version = typeof raw[\"version\"] === \"number\" ? raw[\"version\"] : 1;\n\n if (version < 2) {\n // v1 → v2: Add repos and board config from legacy defaults\n raw = {\n ...raw,\n version: 2,\n repos: [],\n board: {\n refreshInterval: 60,\n backlogLimit: 20,\n assignee: \"unknown\",\n },\n };\n }\n\n const currentVersion = typeof raw[\"version\"] === \"number\" ? raw[\"version\"] : 2;\n if (currentVersion < 3) {\n // v2 → v3: Add ticktick config, infer enabled from auth.json presence\n raw = {\n ...raw,\n version: 3,\n ticktick: { enabled: existsSync(AUTH_FILE) },\n };\n }\n\n return HOG_CONFIG_SCHEMA.parse(raw);\n}\n\n// ── Config Access ──\n\nexport function loadFullConfig(): HogConfig {\n const raw = loadRawConfig();\n\n if (Object.keys(raw).length === 0) {\n // No config exists — create from legacy defaults\n const config = migrateConfig({});\n saveFullConfig(config);\n return config;\n }\n\n const version = typeof raw[\"version\"] === \"number\" ? raw[\"version\"] : 1;\n if (version < 3) {\n const migrated = migrateConfig(raw);\n saveFullConfig(migrated);\n return migrated;\n }\n\n return HOG_CONFIG_SCHEMA.parse(raw);\n}\n\nexport function saveFullConfig(config: HogConfig): void {\n ensureDir();\n writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\\n`, { mode: 0o600 });\n}\n\nfunction loadRawConfig(): Record<string, unknown> {\n if (!existsSync(CONFIG_FILE)) return {};\n try {\n return JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\")) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\n/**\n * Resolve a profile from the config.\n * Priority: explicit profileName > config.defaultProfile > top-level config.\n * Returns a HogConfig with the resolved profile's repos/board/ticktick.\n */\nexport function resolveProfile(\n config: HogConfig,\n profileName?: string | undefined,\n): { resolved: HogConfig; activeProfile: string | null } {\n const name = profileName ?? config.defaultProfile;\n\n if (!name) {\n return { resolved: config, activeProfile: null };\n }\n\n const profile = config.profiles[name];\n if (!profile) {\n console.error(\n `Profile \"${name}\" not found. Available: ${Object.keys(config.profiles).join(\", \") || \"(none)\"}`,\n );\n process.exit(1);\n }\n\n return {\n resolved: { ...config, repos: profile.repos, board: profile.board, ticktick: profile.ticktick },\n activeProfile: name,\n };\n}\n\nexport function findRepo(config: HogConfig, shortNameOrFull: string): RepoConfig | undefined {\n return config.repos.find((r) => r.shortName === shortNameOrFull || r.name === shortNameOrFull);\n}\n\nexport function validateRepoName(name: string): boolean {\n return REPO_NAME_PATTERN.test(name);\n}\n\n// ── Legacy Config Access (backward compat) ──\n\ninterface ConfigData {\n defaultProjectId?: string;\n defaultProjectName?: string;\n}\n\nfunction ensureDir(): void {\n mkdirSync(CONFIG_DIR, { recursive: true });\n}\n\nexport function getAuth(): AuthData | null {\n if (!existsSync(AUTH_FILE)) return null;\n try {\n const raw: unknown = JSON.parse(readFileSync(AUTH_FILE, \"utf-8\"));\n const result = AUTH_SCHEMA.safeParse(raw);\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nexport function saveAuth(data: AuthData): void {\n ensureDir();\n writeFileSync(AUTH_FILE, `${JSON.stringify(data, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\nexport function getLlmAuth(): { provider: \"openrouter\"; apiKey: string } | null {\n const auth = getAuth();\n if (auth?.openrouterApiKey) return { provider: \"openrouter\", apiKey: auth.openrouterApiKey };\n return null;\n}\n\nexport function saveLlmAuth(openrouterApiKey: string): void {\n const existing = getAuth();\n const updated: AuthData = existing\n ? { ...existing, openrouterApiKey }\n : { accessToken: \"\", clientId: \"\", clientSecret: \"\", openrouterApiKey };\n saveAuth(updated);\n}\n\nexport function clearLlmAuth(): void {\n const existing = getAuth();\n if (!existing) return;\n const { openrouterApiKey: _, ...rest } = existing;\n saveAuth(rest as AuthData);\n}\n\nexport function getConfig(): ConfigData {\n if (!existsSync(CONFIG_FILE)) return {};\n try {\n return JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\"));\n } catch {\n return {};\n }\n}\n\nexport function saveConfig(data: ConfigData): void {\n ensureDir();\n const existing = getConfig();\n writeFileSync(CONFIG_FILE, `${JSON.stringify({ ...existing, ...data }, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\nexport function requireAuth(): AuthData {\n const auth = getAuth();\n if (!auth) {\n console.error(\"Not authenticated. Run `hog init` first.\");\n process.exit(1);\n }\n return auth;\n}\n","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 }).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\nexport function fetchAssignedIssues(repo: string, assignee: string): GitHubIssue[] {\n return runGhJson<GitHubIssue[]>([\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--assignee\",\n assignee,\n \"--state\",\n \"open\",\n \"--json\",\n \"number,title,url,state,updatedAt,labels\",\n \"--limit\",\n \"100\",\n ]);\n}\n\nexport interface FetchIssuesOptions {\n assignee?: string | undefined;\n state?: \"open\" | \"closed\" | \"all\" | undefined;\n limit?: number | undefined;\n}\n\nexport function fetchRepoIssues(repo: string, options: FetchIssuesOptions = {}): GitHubIssue[] {\n const { state = \"open\", limit = 100 } = options;\n const args = [\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--state\",\n state,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body\",\n \"--limit\",\n String(limit),\n ];\n if (options.assignee) {\n args.push(\"--assignee\", options.assignee);\n }\n return runGhJson<GitHubIssue[]>(args);\n}\n\nexport function assignIssue(repo: string, issueNumber: number): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueToAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", user]);\n}\n\nexport async function unassignIssueAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\n \"issue\",\n \"edit\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--remove-assignee\",\n user,\n ]);\n}\n\nexport async function fetchIssueAsync(repo: string, issueNumber: number): Promise<GitHubIssue> {\n return runGhJsonAsync<GitHubIssue>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body,projectStatus\",\n ]);\n}\n\nexport async function closeIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"close\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function reopenIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"reopen\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function createIssueAsync(\n repo: string,\n title: string,\n body: string,\n labels?: string[],\n): Promise<string> {\n const args = [\"issue\", \"create\", \"--repo\", repo, \"--title\", title, \"--body\", body];\n if (labels && labels.length > 0) {\n for (const label of labels) {\n args.push(\"--label\", label);\n }\n }\n return runGhAsync(args);\n}\n\nexport async function editIssueTitleAsync(\n repo: string,\n issueNumber: number,\n title: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--title\", title]);\n}\n\nexport async function editIssueBodyAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addCommentAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"comment\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport async function removeLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--remove-label\", label]);\n}\n\nexport async function updateLabelsAsync(\n repo: string,\n issueNumber: number,\n addLabels: string[],\n removeLabels: string[],\n): Promise<void> {\n const args = [\"issue\", \"edit\", String(issueNumber), \"--repo\", repo];\n for (const label of addLabels) args.push(\"--add-label\", label);\n for (const label of removeLabels) args.push(\"--remove-label\", label);\n await runGhAsync(args);\n}\n\nexport interface IssueComment {\n readonly body: string;\n readonly author: { readonly login: string };\n readonly createdAt: string;\n}\n\nexport async function fetchIssueCommentsAsync(\n repo: string,\n issueNumber: number,\n): Promise<IssueComment[]> {\n const result = await runGhJsonAsync<{ comments: IssueComment[] }>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"comments\",\n ]);\n return result.comments ?? [];\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub Project field types\nexport function fetchProjectFields(\n repo: string,\n issueNumber: number,\n projectNumber: number,\n): ProjectFieldValues {\n // GraphQL query to get project item fields for this issue\n const query = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n project { number }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n }\n }\n `;\n\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return {};\n\n try {\n const result = runGhJson<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem) return {};\n\n const fields: ProjectFieldValues = {};\n const fieldValues = projectItem.fieldValues?.nodes ?? [];\n\n for (const fv of fieldValues) {\n if (!fv) continue;\n const fieldName = fv.field?.name ?? \"\";\n if (\"date\" in fv && DATE_FIELD_NAME_RE.test(fieldName)) {\n fields.targetDate = fv.date;\n } else if (\"name\" in fv && fieldName === \"Status\") {\n fields.status = fv.name;\n } else if (fieldName) {\n const value =\n \"text\" in fv && fv.text != null\n ? fv.text\n : \"number\" in fv && fv.number != null\n ? String(fv.number)\n : \"name\" in fv && fv.name != null\n ? fv.name\n : \"title\" in fv && fv.title != null\n ? fv.title\n : null;\n if (value != null) {\n if (!fields.customFields) fields.customFields = {};\n fields.customFields[fieldName] = value;\n }\n }\n }\n\n return fields;\n } catch {\n return {};\n }\n}\n\nexport interface ProjectEnrichment {\n targetDate?: string;\n projectStatus?: string;\n customFields?: Record<string, string>;\n}\n\n/**\n * Fetch target dates and project statuses for all issues in a project in one GraphQL call.\n * Returns a Map from issue number to enrichment data.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub Project field types across all items\nexport function fetchProjectEnrichment(\n repo: string,\n projectNumber: number,\n): Map<number, ProjectEnrichment> {\n const [owner] = repo.split(\"/\");\n if (!owner) return new Map();\n\n const query = `\n query($owner: String!, $projectNumber: Int!, $cursor: String) {\n organization(login: $owner) {\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 `;\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 = runGhJson<ProjectItemsResult>(args);\n const page = result?.data?.organization?.projectV2?.items;\n const nodes = page?.nodes ?? [];\n\n for (const item of nodes) {\n if (!item?.content?.number) continue;\n const enrichment: ProjectEnrichment = {};\n const fieldValues = item.fieldValues?.nodes ?? [];\n for (const fv of fieldValues) {\n if (!fv) continue;\n const fieldName = fv.field?.name ?? \"\";\n if (\"date\" in fv && fv.date && DATE_FIELD_NAME_RE.test(fieldName)) {\n enrichment.targetDate = fv.date;\n } else if (\"name\" in fv && fieldName === \"Status\" && fv.name) {\n enrichment.projectStatus = fv.name;\n } else if (fieldName) {\n const value =\n \"text\" in fv && fv.text != null\n ? fv.text\n : \"number\" in fv && fv.number != null\n ? String(fv.number)\n : \"name\" in fv && fv.name != null\n ? fv.name\n : \"title\" in fv && fv.title != null\n ? fv.title\n : null;\n if (value != null) {\n if (!enrichment.customFields) enrichment.customFields = {};\n enrichment.customFields[fieldName] = value;\n }\n }\n }\n enrichMap.set(item.content.number, enrichment);\n }\n\n if (!page?.pageInfo?.hasNextPage) break;\n cursor = page.pageInfo.endCursor ?? null;\n } while (cursor);\n\n return enrichMap;\n } catch {\n return new Map();\n }\n}\n\n/** Backwards-compatible wrapper for fetchProjectEnrichment. */\nexport function fetchProjectTargetDates(repo: string, projectNumber: number): Map<number, string> {\n const enrichMap = fetchProjectEnrichment(repo, projectNumber);\n const dateMap = new Map<number, string>();\n for (const [num, e] of enrichMap) {\n if (e.targetDate) dateMap.set(num, e.targetDate);\n }\n return dateMap;\n}\n\nexport interface StatusOption {\n id: string;\n name: string;\n}\n\n/**\n * Fetch available project status options (the SingleSelectField values).\n * Returns options in the order defined on the project board.\n */\nexport function fetchProjectStatusOptions(\n repo: string,\n projectNumber: number,\n _statusFieldId: string,\n): StatusOption[] {\n const [owner] = repo.split(\"/\");\n if (!owner) return [];\n\n const query = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) {\n projectV2(number: $projectNumber) {\n field(name: \"Status\") {\n ... on ProjectV2SingleSelectField {\n options {\n id\n name\n }\n }\n }\n }\n }\n }\n `;\n\n try {\n const result = runGhJson<ProjectStatusResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n return result?.data?.organization?.projectV2?.field?.options ?? [];\n } catch {\n return [];\n }\n}\n\nexport function addLabel(repo: string, issueNumber: number, label: string): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport interface LabelOption {\n name: string;\n color: string;\n}\n\n/**\n * Fetch all labels defined in the repo asynchronously.\n * Uses execFileAsync (not execFileSync) to avoid blocking the React render thread.\n */\nexport async function fetchRepoLabelsAsync(repo: string): Promise<LabelOption[]> {\n try {\n const result = await runGhJsonAsync<LabelOption[]>([\n \"label\",\n \"list\",\n \"--repo\",\n repo,\n \"--json\",\n \"name,color\",\n ]);\n return Array.isArray(result) ? result : [];\n } catch {\n return [];\n }\n}\n\n/** Cache for GitHub Projects node IDs — these are immutable per project number. */\nconst projectNodeIdCache = new Map<string, string>();\n\n/** Clears the project node ID cache. Intended for use in tests only. */\nexport function clearProjectNodeIdCache(): void {\n projectNodeIdCache.clear();\n}\n\nasync function getProjectNodeId(owner: string, projectNumber: number): Promise<string | null> {\n const key = `${owner}/${String(projectNumber)}`;\n const cached = projectNodeIdCache.get(key);\n if (cached !== undefined) return cached;\n\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) {\n projectV2(number: $projectNumber) {\n id\n }\n }\n }\n `;\n\n const projectResult = await runGhJsonAsync<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 projectId = projectResult?.data?.organization?.projectV2?.id;\n if (!projectId) return null;\n projectNodeIdCache.set(key, projectId);\n return projectId;\n}\n\nexport function updateProjectItemStatus(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): void {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n // First get the project item ID\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = runGhJson<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectNumber = projectConfig.projectNumber;\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem?.id) return;\n\n // Get the project ID\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) {\n projectV2(number: $projectNumber) {\n id\n }\n }\n }\n `;\n\n const projectResult = runGhJson<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 projectId = projectResult?.data?.organization?.projectV2?.id;\n if (!projectId) return;\n\n const statusFieldId = projectConfig.statusFieldId;\n const optionId = projectConfig.optionId;\n\n // Mutation to update the status\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n runGh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${statusFieldId}`,\n \"-F\",\n `optionId=${optionId}`,\n ]);\n}\n\nexport async function updateProjectItemStatusAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = await runGhJsonAsync<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectNumber = projectConfig.projectNumber;\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectNumber);\n if (!projectId) return;\n\n const statusFieldId = projectConfig.statusFieldId;\n const optionId = projectConfig.optionId;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${statusFieldId}`,\n \"-F\",\n `optionId=${optionId}`,\n ]);\n}\n\nexport interface RepoDueDateConfig {\n projectNumber: number;\n dueDateFieldId: string;\n}\n\n/**\n * Set a date field value on a GitHub Projects v2 item for the given issue.\n * Uses the same 3-step pattern as updateProjectItemStatusAsync.\n */\nexport async function updateProjectItemDateAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoDueDateConfig,\n dueDate: string,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = await runGhJsonAsync<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectItem = items.find((item) => item?.project?.number === projectConfig.projectNumber);\n\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);\n if (!projectId) return;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { date: $date }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${projectConfig.dueDateFieldId}`,\n \"-F\",\n `date=${dueDate}`,\n ]);\n}\n\n// Internal GraphQL response types\n\ninterface FieldValue {\n field?: { name?: string };\n date?: string;\n name?: string;\n text?: string;\n number?: number;\n title?: string; // iteration field title\n}\n\ninterface ProjectItem {\n id?: string;\n project?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface GraphQLResult {\n data?: {\n repository?: {\n issue?: {\n projectItems?: {\n nodes?: (ProjectItem | null)[];\n };\n };\n };\n };\n}\n\ninterface GraphQLProjectResult {\n data?: {\n organization?: {\n projectV2?: {\n id?: string;\n };\n };\n };\n}\n\ninterface ProjectItemNode {\n content?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface ProjectItemsResult {\n data?: {\n organization?: {\n projectV2?: {\n items?: {\n pageInfo?: { hasNextPage: boolean; endCursor?: string };\n nodes?: (ProjectItemNode | null)[];\n };\n };\n };\n };\n}\n\ninterface ProjectStatusResult {\n data?: {\n organization?: {\n projectV2?: {\n field?: {\n options?: StatusOption[];\n };\n };\n };\n };\n}\n","// ── Result Type (no throwing in data layer) ──\n\nexport type Result<T, E> =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly error: E };\n\nexport interface FetchError {\n readonly type: \"github\" | \"ticktick\" | \"network\";\n readonly message: string;\n}\n\n// ── Board Data Types ──\n\nexport interface BoardIssue {\n readonly number: number;\n readonly title: string;\n readonly url: string;\n readonly state: string;\n readonly assignee: string | null;\n readonly labels: readonly string[];\n readonly updatedAt: string;\n readonly repo: string;\n}\n\nexport interface BoardData {\n readonly github: readonly BoardIssue[];\n readonly ticktick: readonly Task[];\n readonly fetchedAt: Date;\n}\n\n// ── Pick Command ──\n\nexport interface PickResult {\n readonly success: boolean;\n readonly issue: BoardIssue;\n readonly ticktickTask?: Task;\n readonly warning?: string;\n}\n\n// ── TickTick Open API types ──\n\nexport interface Task {\n id: string;\n projectId: string;\n title: string;\n content: string;\n desc: string;\n isAllDay: boolean;\n startDate: string;\n dueDate: string;\n completedTime: string;\n priority: Priority;\n reminders: string[];\n repeatFlag: string;\n sortOrder: number;\n status: TaskStatus;\n timeZone: string;\n tags: string[];\n items: ChecklistItem[];\n}\n\nexport interface ChecklistItem {\n id: string;\n title: string;\n status: number;\n completedTime: number;\n isAllDay: boolean;\n sortOrder: number;\n startDate: string;\n timeZone: string;\n}\n\nexport interface Project {\n id: string;\n name: string;\n color: string;\n sortOrder: number;\n closed: boolean;\n groupId: string;\n viewMode: string;\n kind: string;\n}\n\nexport interface ProjectData {\n project: Project;\n tasks: Task[];\n}\n\nexport enum Priority {\n None = 0,\n Low = 1,\n Medium = 3,\n High = 5,\n}\n\nexport enum TaskStatus {\n Active = 0,\n Completed = 2,\n}\n\nexport interface CreateTaskInput {\n title: string;\n projectId?: string;\n content?: string;\n priority?: Priority;\n startDate?: string;\n dueDate?: string;\n isAllDay?: boolean;\n timeZone?: string;\n tags?: string[];\n}\n\nexport interface UpdateTaskInput {\n id: string;\n projectId: string;\n title?: string;\n content?: string;\n priority?: Priority;\n startDate?: string;\n dueDate?: string;\n isAllDay?: boolean;\n tags?: string[];\n}\n","/** Formats an unknown error as a string. */\nexport function formatError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","import { execFileSync } from \"node:child_process\";\nimport { TickTickClient } from \"../api.js\";\nimport type { HogConfig, RepoConfig } from \"../config.js\";\nimport { requireAuth } from \"../config.js\";\nimport type { GitHubIssue, StatusOption } from \"../github.js\";\nimport { fetchProjectEnrichment, fetchProjectStatusOptions, fetchRepoIssues } from \"../github.js\";\nimport type { Task } from \"../types.js\";\nimport { TaskStatus } from \"../types.js\";\nimport { formatError } from \"../utils.js\";\n\nexport interface RepoData {\n repo: RepoConfig;\n issues: GitHubIssue[];\n statusOptions: StatusOption[];\n error: string | null;\n}\n\nexport interface ActivityEvent {\n type: \"comment\" | \"status\" | \"assignment\" | \"opened\" | \"closed\" | \"labeled\";\n repoShortName: string;\n issueNumber: number;\n actor: string;\n summary: string;\n timestamp: Date;\n}\n\nexport interface DashboardData {\n repos: RepoData[];\n ticktick: Task[];\n ticktickError: string | null;\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/** Fetch recent activity events for a repo (last 24h, max 30 events) */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub event types\nexport function fetchRecentActivity(repoName: string, shortName: string): ActivityEvent[] {\n try {\n const output = execFileSync(\n \"gh\",\n [\n \"api\",\n `repos/${repoName}/events`,\n \"--paginate\",\n \"-q\",\n '.[] | select(.type == \"IssuesEvent\" or .type == \"IssueCommentEvent\" or .type == \"PullRequestEvent\") | {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, created_at: .created_at}',\n ],\n { encoding: \"utf-8\", timeout: 15_000 },\n );\n\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n const events: ActivityEvent[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const ev = JSON.parse(line) as {\n type: string;\n actor: string;\n action: string;\n number: number | null;\n title: string | null;\n body: string | null;\n created_at: string;\n };\n\n const timestamp = new Date(ev.created_at);\n if (timestamp.getTime() < cutoff) continue;\n if (!ev.number) continue;\n\n let eventType: ActivityEvent[\"type\"];\n let summary: string;\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 {\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 });\n } catch {\n // Skip malformed event\n }\n }\n\n return events.slice(0, 15);\n } catch {\n return [];\n }\n}\n\nexport async function fetchDashboard(\n config: HogConfig,\n options: FetchOptions = {},\n): Promise<DashboardData> {\n const repos = options.repoFilter\n ? config.repos.filter(\n (r) => r.shortName === options.repoFilter || r.name === options.repoFilter,\n )\n : config.repos;\n\n // GitHub: synchronous (uses gh CLI via execFileSync)\n const repoData: RepoData[] = repos.map((repo) => {\n try {\n const fetchOpts: { assignee?: string } = {};\n if (options.mineOnly) {\n fetchOpts.assignee = config.board.assignee;\n }\n const issues = fetchRepoIssues(repo.name, fetchOpts);\n\n // Enrich issues with target dates + statuses from GitHub Projects (batched)\n let enrichedIssues = issues;\n let statusOptions: StatusOption[] = [];\n try {\n const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const e = enrichMap.get(issue.number);\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return {\n ...issue,\n ...(e?.targetDate !== undefined ? { targetDate: e.targetDate } : {}),\n ...(e?.projectStatus !== undefined ? { projectStatus: e.projectStatus } : {}),\n ...(e?.customFields !== undefined ? { customFields: e.customFields } : {}),\n ...(slackUrl ? { slackThreadUrl: slackUrl } : {}),\n };\n });\n statusOptions = fetchProjectStatusOptions(\n repo.name,\n repo.projectNumber,\n repo.statusFieldId,\n );\n } catch {\n // Non-critical: silently skip if project fields fail\n // Compute Slack thread URLs from original issue bodies\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;\n });\n }\n\n return { repo, issues: enrichedIssues, statusOptions, error: null };\n } catch (err) {\n return { repo, issues: [], statusOptions: [], error: formatError(err) };\n }\n });\n\n // TickTick: async (uses HTTP API) — skip when disabled in config\n let ticktick: Task[] = [];\n let ticktickError: string | null = null;\n if (config.ticktick.enabled) {\n try {\n const auth = requireAuth();\n const api = new TickTickClient(auth.accessToken);\n if (config.defaultProjectId) {\n const tasks = await api.listTasks(config.defaultProjectId);\n ticktick = tasks.filter((t) => t.status !== TaskStatus.Completed);\n }\n } catch (err) {\n ticktickError = formatError(err);\n }\n }\n\n // Activity: fetch recent events from all repos (non-blocking, best-effort)\n const activity: ActivityEvent[] = [];\n for (const repo of repos) {\n const events = fetchRecentActivity(repo.name, repo.shortName);\n activity.push(...events);\n }\n // Sort by timestamp descending\n activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());\n\n return {\n repos: repoData,\n ticktick,\n ticktickError,\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":["text","options","execFileSync","config","options","fetchDashboard"]}
1
+ {"version":3,"sources":["../src/github.ts","../src/utils.ts","../src/board/fetch.ts","../src/board/fetch-worker.ts"],"sourcesContent":["import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface GitHubIssue {\n readonly number: number;\n readonly title: string;\n readonly url: string;\n readonly state: string;\n readonly updatedAt: string;\n readonly labels: { name: string }[];\n readonly assignees?: { login: string }[];\n readonly targetDate?: string;\n readonly body?: string;\n readonly projectStatus?: string;\n readonly slackThreadUrl?: string;\n /**\n * All other GitHub Project custom field values keyed by field name.\n * Includes single-select, text, number, and iteration fields — excluding\n * Status (→ projectStatus) and date fields (→ targetDate).\n * Example: { Workstream: \"Platform\", Size: \"M\", Priority: \"High\" }\n */\n readonly customFields?: Record<string, string>;\n}\n\nexport interface ProjectFieldValues {\n targetDate?: string;\n status?: string;\n customFields?: Record<string, string>;\n}\n\nexport interface RepoProjectConfig {\n projectNumber: number;\n statusFieldId: string;\n optionId: string;\n}\n\n/** Matches common date field names used in GitHub Projects v2 (case-insensitive). */\nconst DATE_FIELD_NAME_RE = /^(target\\s*date|due\\s*date|due|deadline)$/i;\n\nfunction runGh(args: string[]): string {\n return execFileSync(\"gh\", args, { encoding: \"utf-8\", timeout: 30_000 }).trim();\n}\n\nfunction runGhJson<T>(args: string[]): T {\n const output = runGh(args);\n return JSON.parse(output) as T;\n}\n\nasync function runGhAsync(args: string[]): Promise<string> {\n const { stdout } = await execFileAsync(\"gh\", args, { encoding: \"utf-8\", timeout: 30_000 });\n return stdout.trim();\n}\n\nasync function runGhJsonAsync<T>(args: string[]): Promise<T> {\n const output = await runGhAsync(args);\n return JSON.parse(output) as T;\n}\n\n/**\n * Run a GraphQL query via `gh api graphql`. Handles partial errors: when the\n * query returns data for one alias but NOT_FOUND for another (e.g. org vs user\n * owner), `gh` exits with code 1 but still emits valid JSON on stdout. This\n * helper recovers that JSON from the error object instead of throwing.\n */\nfunction runGhGraphQL<T>(args: string[]): T {\n try {\n return runGhJson<T>(args);\n } catch (err: unknown) {\n if (err && typeof err === \"object\" && \"stdout\" in err) {\n const stdout = (err as { stdout: string | Buffer }).stdout;\n const output = typeof stdout === \"string\" ? stdout : stdout?.toString(\"utf-8\");\n if (output) {\n try {\n return JSON.parse(output.trim()) as T;\n } catch {\n // stdout wasn't valid JSON — rethrow original\n }\n }\n }\n throw err;\n }\n}\n\nasync function runGhGraphQLAsync<T>(args: string[]): Promise<T> {\n try {\n return await runGhJsonAsync<T>(args);\n } catch (err: unknown) {\n if (err && typeof err === \"object\" && \"stdout\" in err) {\n const stdout = (err as { stdout: string | Buffer }).stdout;\n const output = typeof stdout === \"string\" ? stdout : stdout?.toString(\"utf-8\");\n if (output) {\n try {\n return JSON.parse(output.trim()) as T;\n } catch {\n // stdout wasn't valid JSON — rethrow original\n }\n }\n }\n throw err;\n }\n}\n\nexport function fetchAssignedIssues(repo: string, assignee: string): GitHubIssue[] {\n return runGhJson<GitHubIssue[]>([\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--assignee\",\n assignee,\n \"--state\",\n \"open\",\n \"--json\",\n \"number,title,url,state,updatedAt,labels\",\n \"--limit\",\n \"100\",\n ]);\n}\n\nexport interface FetchIssuesOptions {\n assignee?: string | undefined;\n state?: \"open\" | \"closed\" | \"all\" | undefined;\n limit?: number | undefined;\n}\n\nexport function fetchRepoIssues(repo: string, options: FetchIssuesOptions = {}): GitHubIssue[] {\n const { state = \"open\", limit = 100 } = options;\n const args = [\n \"issue\",\n \"list\",\n \"--repo\",\n repo,\n \"--state\",\n state,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body\",\n \"--limit\",\n String(limit),\n ];\n if (options.assignee) {\n args.push(\"--assignee\", options.assignee);\n }\n return runGhJson<GitHubIssue[]>(args);\n}\n\nexport function assignIssue(repo: string, issueNumber: number): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", \"@me\"]);\n}\n\nexport async function assignIssueToAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-assignee\", user]);\n}\n\nexport async function unassignIssueAsync(\n repo: string,\n issueNumber: number,\n user: string,\n): Promise<void> {\n await runGhAsync([\n \"issue\",\n \"edit\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--remove-assignee\",\n user,\n ]);\n}\n\nexport async function fetchIssueAsync(repo: string, issueNumber: number): Promise<GitHubIssue> {\n return runGhJsonAsync<GitHubIssue>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"number,title,url,state,updatedAt,labels,assignees,body,projectStatus\",\n ]);\n}\n\nexport async function closeIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"close\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function reopenIssueAsync(repo: string, issueNumber: number): Promise<void> {\n await runGhAsync([\"issue\", \"reopen\", String(issueNumber), \"--repo\", repo]);\n}\n\nexport async function createIssueAsync(\n repo: string,\n title: string,\n body: string,\n labels?: string[],\n): Promise<string> {\n const args = [\"issue\", \"create\", \"--repo\", repo, \"--title\", title, \"--body\", body];\n if (labels && labels.length > 0) {\n for (const label of labels) {\n args.push(\"--label\", label);\n }\n }\n return runGhAsync(args);\n}\n\nexport async function editIssueTitleAsync(\n repo: string,\n issueNumber: number,\n title: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--title\", title]);\n}\n\nexport async function editIssueBodyAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addCommentAsync(\n repo: string,\n issueNumber: number,\n body: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"comment\", String(issueNumber), \"--repo\", repo, \"--body\", body]);\n}\n\nexport async function addLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport async function removeLabelAsync(\n repo: string,\n issueNumber: number,\n label: string,\n): Promise<void> {\n await runGhAsync([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--remove-label\", label]);\n}\n\nexport async function updateLabelsAsync(\n repo: string,\n issueNumber: number,\n addLabels: string[],\n removeLabels: string[],\n): Promise<void> {\n const args = [\"issue\", \"edit\", String(issueNumber), \"--repo\", repo];\n for (const label of addLabels) args.push(\"--add-label\", label);\n for (const label of removeLabels) args.push(\"--remove-label\", label);\n await runGhAsync(args);\n}\n\nexport interface IssueComment {\n readonly body: string;\n readonly author: { readonly login: string };\n readonly createdAt: string;\n}\n\nexport async function fetchIssueCommentsAsync(\n repo: string,\n issueNumber: number,\n): Promise<IssueComment[]> {\n const result = await runGhJsonAsync<{ comments: IssueComment[] }>([\n \"issue\",\n \"view\",\n String(issueNumber),\n \"--repo\",\n repo,\n \"--json\",\n \"comments\",\n ]);\n return result.comments ?? [];\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub Project field types\nexport function fetchProjectFields(\n repo: string,\n issueNumber: number,\n projectNumber: number,\n): ProjectFieldValues {\n // GraphQL query to get project item fields for this issue\n const query = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n project { number }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n }\n }\n `;\n\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return {};\n\n try {\n const result = runGhJson<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem) return {};\n\n const fields: ProjectFieldValues = {};\n const fieldValues = projectItem.fieldValues?.nodes ?? [];\n\n for (const fv of fieldValues) {\n if (!fv) continue;\n const fieldName = fv.field?.name ?? \"\";\n if (\"date\" in fv && DATE_FIELD_NAME_RE.test(fieldName)) {\n fields.targetDate = fv.date;\n } else if (\"name\" in fv && fieldName === \"Status\") {\n fields.status = fv.name;\n } else if (fieldName) {\n const value =\n \"text\" in fv && fv.text != null\n ? fv.text\n : \"number\" in fv && fv.number != null\n ? String(fv.number)\n : \"name\" in fv && fv.name != null\n ? fv.name\n : \"title\" in fv && fv.title != null\n ? fv.title\n : null;\n if (value != null) {\n if (!fields.customFields) fields.customFields = {};\n fields.customFields[fieldName] = value;\n }\n }\n }\n\n return fields;\n } catch {\n return {};\n }\n}\n\nexport interface ProjectEnrichment {\n targetDate?: string;\n projectStatus?: string;\n customFields?: Record<string, string>;\n}\n\n/**\n * Fetch target dates and project statuses for all issues in a project in one GraphQL call.\n * Returns a Map from issue number to enrichment data.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub Project field types across all items\nexport function fetchProjectEnrichment(\n repo: string,\n projectNumber: number,\n): Map<number, ProjectEnrichment> {\n const [owner] = repo.split(\"/\");\n if (!owner) return new Map();\n\n const projectItemsFragment = `\n projectV2(number: $projectNumber) {\n items(first: 100, after: $cursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n content {\n ... on Issue {\n number\n }\n }\n fieldValues(first: 20) {\n nodes {\n ... on ProjectV2ItemFieldDateValue {\n field { ... on ProjectV2Field { name } }\n date\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field { ... on ProjectV2SingleSelectField { name } }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field { ... on ProjectV2Field { name } }\n text\n }\n ... on ProjectV2ItemFieldNumberValue {\n field { ... on ProjectV2Field { name } }\n number\n }\n ... on ProjectV2ItemFieldIterationValue {\n field { ... on ProjectV2IterationField { name } }\n title\n }\n }\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!, $cursor: String) {\n organization(login: $owner) { ${projectItemsFragment} }\n user(login: $owner) { ${projectItemsFragment} }\n }\n `;\n\n try {\n const enrichMap = new Map<number, ProjectEnrichment>();\n let cursor: string | null = null;\n\n do {\n const args = [\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ];\n if (cursor) args.push(\"-f\", `cursor=${cursor}`);\n const result = runGhGraphQL<ProjectItemsResult>(args);\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n const page = ownerNode?.projectV2?.items;\n const nodes = page?.nodes ?? [];\n\n for (const item of nodes) {\n if (!item?.content?.number) continue;\n const enrichment: ProjectEnrichment = {};\n const fieldValues = item.fieldValues?.nodes ?? [];\n for (const fv of fieldValues) {\n if (!fv) continue;\n const fieldName = fv.field?.name ?? \"\";\n if (\"date\" in fv && fv.date && DATE_FIELD_NAME_RE.test(fieldName)) {\n enrichment.targetDate = fv.date;\n } else if (\"name\" in fv && fieldName === \"Status\" && fv.name) {\n enrichment.projectStatus = fv.name;\n } else if (fieldName) {\n const value =\n \"text\" in fv && fv.text != null\n ? fv.text\n : \"number\" in fv && fv.number != null\n ? String(fv.number)\n : \"name\" in fv && fv.name != null\n ? fv.name\n : \"title\" in fv && fv.title != null\n ? fv.title\n : null;\n if (value != null) {\n if (!enrichment.customFields) enrichment.customFields = {};\n enrichment.customFields[fieldName] = value;\n }\n }\n }\n enrichMap.set(item.content.number, enrichment);\n }\n\n if (!page?.pageInfo?.hasNextPage) break;\n cursor = page.pageInfo.endCursor ?? null;\n } while (cursor);\n\n return enrichMap;\n } catch {\n return new Map();\n }\n}\n\n/** Backwards-compatible wrapper for fetchProjectEnrichment. */\nexport function fetchProjectTargetDates(repo: string, projectNumber: number): Map<number, string> {\n const enrichMap = fetchProjectEnrichment(repo, projectNumber);\n const dateMap = new Map<number, string>();\n for (const [num, e] of enrichMap) {\n if (e.targetDate) dateMap.set(num, e.targetDate);\n }\n return dateMap;\n}\n\nexport interface StatusOption {\n id: string;\n name: string;\n}\n\n/**\n * Fetch available project status options (the SingleSelectField values).\n * Returns options in the order defined on the project board.\n */\nexport function fetchProjectStatusOptions(\n repo: string,\n projectNumber: number,\n _statusFieldId: string,\n): StatusOption[] {\n const [owner] = repo.split(\"/\");\n if (!owner) return [];\n\n const statusFragment = `\n projectV2(number: $projectNumber) {\n field(name: \"Status\") {\n ... on ProjectV2SingleSelectField {\n options {\n id\n name\n }\n }\n }\n }\n `;\n\n const query = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${statusFragment} }\n user(login: $owner) { ${statusFragment} }\n }\n `;\n\n try {\n const result = runGhGraphQL<ProjectStatusResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = result?.data?.organization ?? result?.data?.user;\n return ownerNode?.projectV2?.field?.options ?? [];\n } catch {\n return [];\n }\n}\n\nexport function addLabel(repo: string, issueNumber: number, label: string): void {\n runGh([\"issue\", \"edit\", String(issueNumber), \"--repo\", repo, \"--add-label\", label]);\n}\n\nexport interface LabelOption {\n name: string;\n color: string;\n}\n\n/**\n * Fetch all labels defined in the repo asynchronously.\n * Uses execFileAsync (not execFileSync) to avoid blocking the React render thread.\n */\nexport async function fetchRepoLabelsAsync(repo: string): Promise<LabelOption[]> {\n try {\n const result = await runGhJsonAsync<LabelOption[]>([\n \"label\",\n \"list\",\n \"--repo\",\n repo,\n \"--json\",\n \"name,color\",\n ]);\n return Array.isArray(result) ? result : [];\n } catch {\n return [];\n }\n}\n\n/** Cache for GitHub Projects node IDs — these are immutable per project number. */\nconst projectNodeIdCache = new Map<string, string>();\n\n/** Clears the project node ID cache. Intended for use in tests only. */\nexport function clearProjectNodeIdCache(): void {\n projectNodeIdCache.clear();\n}\n\nasync function getProjectNodeId(owner: string, projectNumber: number): Promise<string | null> {\n const key = `${owner}/${String(projectNumber)}`;\n const cached = projectNodeIdCache.get(key);\n if (cached !== undefined) return cached;\n\n const idFragment = `projectV2(number: $projectNumber) { id }`;\n\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${idFragment} }\n user(login: $owner) { ${idFragment} }\n }\n `;\n\n const projectResult = await runGhGraphQLAsync<GraphQLProjectResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${projectQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const ownerNode = projectResult?.data?.organization ?? projectResult?.data?.user;\n const projectId = ownerNode?.projectV2?.id;\n if (!projectId) return null;\n projectNodeIdCache.set(key, projectId);\n return projectId;\n}\n\nexport function updateProjectItemStatus(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): void {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n // First get the project item ID\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = runGhJson<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectNumber = projectConfig.projectNumber;\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem?.id) return;\n\n // Get the project ID\n const idFragment = `projectV2(number: $projectNumber) { id }`;\n\n const projectQuery = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) { ${idFragment} }\n user(login: $owner) { ${idFragment} }\n }\n `;\n\n const projectResult = runGhGraphQL<GraphQLProjectResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${projectQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const projectOwner = projectResult?.data?.organization ?? projectResult?.data?.user;\n const projectId = projectOwner?.projectV2?.id;\n if (!projectId) return;\n\n const statusFieldId = projectConfig.statusFieldId;\n const optionId = projectConfig.optionId;\n\n // Mutation to update the status\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n runGh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${statusFieldId}`,\n \"-F\",\n `optionId=${optionId}`,\n ]);\n}\n\nexport async function updateProjectItemStatusAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = await runGhJsonAsync<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectNumber = projectConfig.projectNumber;\n const projectItem = items.find((item) => item?.project?.number === projectNumber);\n\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectNumber);\n if (!projectId) return;\n\n const statusFieldId = projectConfig.statusFieldId;\n const optionId = projectConfig.optionId;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { singleSelectOptionId: $optionId }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${statusFieldId}`,\n \"-F\",\n `optionId=${optionId}`,\n ]);\n}\n\nexport interface RepoDueDateConfig {\n projectNumber: number;\n dueDateFieldId: string;\n}\n\n/**\n * Set a date field value on a GitHub Projects v2 item for the given issue.\n * Uses the same 3-step pattern as updateProjectItemStatusAsync.\n */\nexport async function updateProjectItemDateAsync(\n repo: string,\n issueNumber: number,\n projectConfig: RepoDueDateConfig,\n dueDate: string,\n): Promise<void> {\n const [owner, repoName] = repo.split(\"/\");\n if (!(owner && repoName)) return;\n\n const findItemQuery = `\n query($owner: String!, $repo: String!, $issueNumber: Int!) {\n repository(owner: $owner, name: $repo) {\n issue(number: $issueNumber) {\n projectItems(first: 10) {\n nodes {\n id\n project { number }\n }\n }\n }\n }\n }\n `;\n\n const findResult = await runGhJsonAsync<GraphQLResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${findItemQuery}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `repo=${repoName}`,\n \"-F\",\n `issueNumber=${String(issueNumber)}`,\n ]);\n\n const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];\n const projectItem = items.find((item) => item?.project?.number === projectConfig.projectNumber);\n\n if (!projectItem?.id) return;\n\n const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);\n if (!projectId) return;\n\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {\n updateProjectV2ItemFieldValue(\n input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: { date: $date }\n }\n ) {\n projectV2Item { id }\n }\n }\n `;\n\n await runGhAsync([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${mutation}`,\n \"-F\",\n `projectId=${projectId}`,\n \"-F\",\n `itemId=${projectItem.id}`,\n \"-F\",\n `fieldId=${projectConfig.dueDateFieldId}`,\n \"-F\",\n `date=${dueDate}`,\n ]);\n}\n\n// Internal GraphQL response types\n\ninterface FieldValue {\n field?: { name?: string };\n date?: string;\n name?: string;\n text?: string;\n number?: number;\n title?: string; // iteration field title\n}\n\ninterface ProjectItem {\n id?: string;\n project?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface GraphQLResult {\n data?: {\n repository?: {\n issue?: {\n projectItems?: {\n nodes?: (ProjectItem | null)[];\n };\n };\n };\n };\n}\n\ninterface ProjectV2IdNode {\n projectV2?: {\n id?: string;\n };\n}\n\ninterface GraphQLProjectResult {\n data?: {\n organization?: ProjectV2IdNode;\n user?: ProjectV2IdNode;\n };\n}\n\ninterface ProjectItemNode {\n content?: { number?: number };\n fieldValues?: { nodes?: (FieldValue | null)[] };\n}\n\ninterface ProjectV2ItemsNode {\n projectV2?: {\n items?: {\n pageInfo?: { hasNextPage: boolean; endCursor?: string };\n nodes?: (ProjectItemNode | null)[];\n };\n };\n}\n\ninterface ProjectItemsResult {\n data?: {\n organization?: ProjectV2ItemsNode;\n user?: ProjectV2ItemsNode;\n };\n}\n\ninterface ProjectV2StatusNode {\n projectV2?: {\n field?: {\n options?: StatusOption[];\n };\n };\n}\n\ninterface ProjectStatusResult {\n data?: {\n organization?: ProjectV2StatusNode;\n user?: ProjectV2StatusNode;\n };\n}\n","/** Formats an unknown error as a string. */\nexport function formatError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { HogConfig, RepoConfig } from \"../config.js\";\nimport type { GitHubIssue, StatusOption } from \"../github.js\";\nimport { fetchProjectEnrichment, fetchProjectStatusOptions, fetchRepoIssues } from \"../github.js\";\nimport { formatError } from \"../utils.js\";\n\nexport interface RepoData {\n repo: RepoConfig;\n issues: GitHubIssue[];\n statusOptions: StatusOption[];\n error: string | null;\n}\n\nexport interface ActivityEvent {\n type:\n | \"comment\"\n | \"status\"\n | \"assignment\"\n | \"opened\"\n | \"closed\"\n | \"labeled\"\n | \"branch_created\"\n | \"pr_opened\"\n | \"pr_merged\"\n | \"pr_closed\";\n repoShortName: string;\n issueNumber: number;\n actor: string;\n summary: string;\n timestamp: Date;\n /** For branch_created events: the full branch name */\n branchName?: string | undefined;\n /** For pr_* events: the PR number (distinct from linked issueNumber) */\n prNumber?: number | undefined;\n}\n\nexport interface DashboardData {\n repos: RepoData[];\n activity: ActivityEvent[];\n fetchedAt: Date;\n}\n\nexport interface FetchOptions {\n repoFilter?: string | undefined;\n mineOnly?: boolean | undefined;\n backlogOnly?: boolean | undefined;\n}\n\nexport const SLACK_URL_RE = /https:\\/\\/[^/]+\\.slack\\.com\\/archives\\/[A-Z0-9]+\\/p[0-9]+/i;\n\nexport function extractSlackUrl(body: string | undefined): string | undefined {\n if (!body) return undefined;\n const match = body.match(SLACK_URL_RE);\n return match?.[0];\n}\n\n/** Extract issue numbers from a branch name (e.g. \"feat/42-add-auth\" → [42]) */\nexport function extractIssueNumbersFromBranch(branchName: string): number[] {\n // Find numbers that look like issue numbers (1-5 digits, word boundary)\n const matches = branchName.match(/\\b(\\d{1,5})\\b/g);\n if (!matches) return [];\n return [...new Set(matches.map((m) => parseInt(m, 10)).filter((n) => n > 0))];\n}\n\n/** Extract issue numbers linked in PR title/body (e.g. \"Fixes #42\" → [42]) */\nexport function extractLinkedIssueNumbers(title: string | null, body: string | null): number[] {\n const text = `${title ?? \"\"} ${body ?? \"\"}`;\n const matches = text.match(/#(\\d{1,5})\\b/g);\n if (!matches) return [];\n return [...new Set(matches.map((m) => parseInt(m.slice(1), 10)).filter((n) => n > 0))];\n}\n\n/** Fetch recent activity events for a repo (last 24h, max 30 events) */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: parses multiple GitHub event types\nexport function fetchRecentActivity(repoName: string, shortName: string): ActivityEvent[] {\n try {\n const output = execFileSync(\n \"gh\",\n [\n \"api\",\n `repos/${repoName}/events`,\n \"-f\",\n \"per_page=30\",\n \"-q\",\n '.[] | select(.type == \"IssuesEvent\" or .type == \"IssueCommentEvent\" or .type == \"PullRequestEvent\" or .type == \"CreateEvent\") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: (.payload.comment.body // .payload.pull_request.body), created_at: .created_at, ref: .payload.ref, ref_type: .payload.ref_type, merged: .payload.pull_request.merged}',\n ],\n { encoding: \"utf-8\", timeout: 15_000 },\n );\n\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n const events: ActivityEvent[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const ev = JSON.parse(line) as {\n type: string;\n actor: string;\n action: string;\n number: number | null;\n title: string | null;\n body: string | null;\n created_at: string;\n ref: string | null;\n ref_type: string | null;\n merged: boolean | null;\n };\n\n const timestamp = new Date(ev.created_at);\n if (timestamp.getTime() < cutoff) continue;\n\n // CreateEvent (branch creation) has no issue/PR number — handled separately\n if (ev.type === \"CreateEvent\") {\n if (ev.ref_type !== \"branch\" || !ev.ref) continue;\n // Extract issue numbers from branch name (e.g. \"feat/42-add-auth\" → 42)\n const issueNumbers = extractIssueNumbersFromBranch(ev.ref);\n for (const num of issueNumbers) {\n events.push({\n type: \"branch_created\",\n repoShortName: shortName,\n issueNumber: num,\n actor: ev.actor,\n summary: `created branch ${ev.ref}`,\n timestamp,\n branchName: ev.ref,\n });\n }\n continue;\n }\n\n if (!ev.number) continue;\n\n let eventType: ActivityEvent[\"type\"];\n let summary: string;\n let extras: Partial<Pick<ActivityEvent, \"prNumber\">> = {};\n\n if (ev.type === \"IssueCommentEvent\") {\n eventType = \"comment\";\n const preview = ev.body ? ev.body.slice(0, 60).replace(/\\n/g, \" \") : \"\";\n summary = `commented on #${ev.number}${preview ? ` — \"${preview}${(ev.body?.length ?? 0) > 60 ? \"...\" : \"\"}\"` : \"\"}`;\n } else if (ev.type === \"IssuesEvent\") {\n switch (ev.action) {\n case \"opened\":\n eventType = \"opened\";\n summary = `opened #${ev.number}: ${ev.title ?? \"\"}`;\n break;\n case \"closed\":\n eventType = \"closed\";\n summary = `closed #${ev.number}`;\n break;\n case \"assigned\":\n eventType = \"assignment\";\n summary = `assigned #${ev.number}`;\n break;\n case \"labeled\":\n eventType = \"labeled\";\n summary = `labeled #${ev.number}`;\n break;\n default:\n continue;\n }\n } else if (ev.type === \"PullRequestEvent\") {\n const prNumber = ev.number;\n extras = { prNumber };\n if (ev.action === \"opened\") {\n eventType = \"pr_opened\";\n summary = `opened PR #${prNumber}: ${ev.title ?? \"\"}`;\n } else if (ev.action === \"closed\" && ev.merged) {\n eventType = \"pr_merged\";\n summary = `merged PR #${prNumber}: ${ev.title ?? \"\"}`;\n } else if (ev.action === \"closed\") {\n eventType = \"pr_closed\";\n summary = `closed PR #${prNumber}`;\n } else {\n continue;\n }\n\n // For PR events, also create events for each linked issue number\n const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);\n for (const issueNum of linkedIssues) {\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: issueNum,\n actor: ev.actor,\n summary,\n timestamp,\n prNumber,\n });\n }\n // If no linked issues, use the PR number as issueNumber\n if (linkedIssues.length === 0) {\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: prNumber,\n actor: ev.actor,\n summary,\n timestamp,\n prNumber,\n });\n }\n continue;\n } else {\n continue;\n }\n\n events.push({\n type: eventType,\n repoShortName: shortName,\n issueNumber: ev.number,\n actor: ev.actor,\n summary,\n timestamp,\n ...extras,\n });\n } catch {\n // Skip malformed event\n }\n }\n\n return events.slice(0, 15);\n } catch {\n return [];\n }\n}\n\nexport async function fetchDashboard(\n config: HogConfig,\n options: FetchOptions = {},\n): Promise<DashboardData> {\n const repos = options.repoFilter\n ? config.repos.filter(\n (r) => r.shortName === options.repoFilter || r.name === options.repoFilter,\n )\n : config.repos;\n\n // GitHub: synchronous (uses gh CLI via execFileSync)\n const repoData: RepoData[] = repos.map((repo) => {\n try {\n const fetchOpts: { assignee?: string } = {};\n if (options.mineOnly) {\n fetchOpts.assignee = config.board.assignee;\n }\n const issues = fetchRepoIssues(repo.name, fetchOpts);\n\n // Enrich issues with target dates + statuses from GitHub Projects (batched)\n let enrichedIssues = issues;\n let statusOptions: StatusOption[] = [];\n try {\n const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const e = enrichMap.get(issue.number);\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return {\n ...issue,\n ...(e?.targetDate !== undefined ? { targetDate: e.targetDate } : {}),\n ...(e?.projectStatus !== undefined ? { projectStatus: e.projectStatus } : {}),\n ...(e?.customFields !== undefined ? { customFields: e.customFields } : {}),\n ...(slackUrl ? { slackThreadUrl: slackUrl } : {}),\n };\n });\n statusOptions = fetchProjectStatusOptions(\n repo.name,\n repo.projectNumber,\n repo.statusFieldId,\n );\n } catch {\n // Non-critical: silently skip if project fields fail\n // Compute Slack thread URLs from original issue bodies\n enrichedIssues = issues.map((issue): GitHubIssue => {\n const slackUrl = extractSlackUrl(issue.body ?? \"\");\n return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;\n });\n }\n\n return { repo, issues: enrichedIssues, statusOptions, error: null };\n } catch (err) {\n return { repo, issues: [], statusOptions: [], error: formatError(err) };\n }\n });\n\n // Activity: fetch recent events from all repos (non-blocking, best-effort)\n const activity: ActivityEvent[] = [];\n for (const repo of repos) {\n const events = fetchRecentActivity(repo.name, repo.shortName);\n activity.push(...events);\n }\n // Sort by timestamp descending\n activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());\n\n return {\n repos: repoData,\n activity: activity.slice(0, 15),\n fetchedAt: new Date(),\n };\n}\n","import { parentPort, workerData } from \"node:worker_threads\";\nimport type { HogConfig } from \"../config.js\";\nimport type { FetchOptions } from \"./fetch.js\";\n\nconst { config, options } = workerData as { config: HogConfig; options: FetchOptions };\n\nconst { fetchDashboard } = await import(\"./fetch.js\");\n\nif (!parentPort) throw new Error(\"fetch-worker must run in a worker thread\");\n\ntry {\n const data = await fetchDashboard(config, options);\n parentPort.postMessage({ type: \"success\", data });\n} catch (err) {\n parentPort.postMessage({\n type: \"error\",\n error: err instanceof Error ? err.message : String(err),\n });\n}\n"],"mappings":"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","names":["options","execFileSync","config","options","fetchDashboard"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondrej-svec/hog",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Personal command deck — unified task dashboard for GitHub Projects + TickTick",
5
5
  "author": "Ondrej Svec",
6
6
  "repository": {