@ondrej-svec/hog 1.18.0 → 1.20.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.
@@ -91,19 +91,13 @@ import { z } from "zod";
91
91
  function getAuth() {
92
92
  if (!existsSync(AUTH_FILE)) return null;
93
93
  try {
94
- return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
94
+ const raw = JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
95
+ const result = AUTH_SCHEMA.safeParse(raw);
96
+ return result.success ? result.data : null;
95
97
  } catch {
96
98
  return null;
97
99
  }
98
100
  }
99
- function getConfig() {
100
- if (!existsSync(CONFIG_FILE)) return {};
101
- try {
102
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
103
- } catch {
104
- return {};
105
- }
106
- }
107
101
  function requireAuth() {
108
102
  const auth = getAuth();
109
103
  if (!auth) {
@@ -112,13 +106,19 @@ function requireAuth() {
112
106
  }
113
107
  return auth;
114
108
  }
115
- var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, CLAUDE_START_COMMAND_SCHEMA, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA;
109
+ var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, AUTH_SCHEMA, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, CLAUDE_START_COMMAND_SCHEMA, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA;
116
110
  var init_config = __esm({
117
111
  "src/config.ts"() {
118
112
  "use strict";
119
113
  CONFIG_DIR = join(homedir(), ".config", "hog");
120
114
  AUTH_FILE = join(CONFIG_DIR, "auth.json");
121
115
  CONFIG_FILE = join(CONFIG_DIR, "config.json");
116
+ AUTH_SCHEMA = z.object({
117
+ accessToken: z.string(),
118
+ clientId: z.string(),
119
+ clientSecret: z.string(),
120
+ openrouterApiKey: z.string().optional()
121
+ });
122
122
  COMPLETION_ACTION_SCHEMA = z.discriminatedUnion("type", [
123
123
  z.object({ type: z.literal("updateProjectStatus"), optionId: z.string() }),
124
124
  z.object({ type: z.literal("closeIssue") }),
@@ -140,7 +140,8 @@ var init_config = __esm({
140
140
  localPath: z.string().refine((p) => isAbsolute(p), { message: "localPath must be an absolute path" }).refine((p) => normalize(p) === p, {
141
141
  message: "localPath must be normalized (no .. segments)"
142
142
  }).refine((p) => !p.includes("\0"), { message: "localPath must not contain null bytes" }).optional(),
143
- claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional()
143
+ claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional(),
144
+ claudePrompt: z.string().optional()
144
145
  });
145
146
  BOARD_CONFIG_SCHEMA = z.object({
146
147
  refreshInterval: z.number().int().min(10).default(60),
@@ -148,6 +149,7 @@ var init_config = __esm({
148
149
  assignee: z.string().min(1),
149
150
  focusDuration: z.number().int().min(60).default(1500),
150
151
  claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional(),
152
+ claudePrompt: z.string().optional(),
151
153
  claudeLaunchMode: z.enum(["auto", "tmux", "terminal"]).optional(),
152
154
  claudeTerminalApp: z.enum(["Terminal", "iTerm", "Ghostty", "WezTerm", "Kitty", "Alacritty"]).optional()
153
155
  });
@@ -344,12 +346,12 @@ var init_types = __esm({
344
346
  }
345
347
  });
346
348
 
347
- // src/board/constants.ts
349
+ // src/utils.ts
348
350
  function formatError(err) {
349
351
  return err instanceof Error ? err.message : String(err);
350
352
  }
351
- var init_constants = __esm({
352
- "src/board/constants.ts"() {
353
+ var init_utils = __esm({
354
+ "src/utils.ts"() {
353
355
  "use strict";
354
356
  }
355
357
  });
@@ -484,9 +486,8 @@ async function fetchDashboard(config2, options2 = {}) {
484
486
  try {
485
487
  const auth = requireAuth();
486
488
  const api = new TickTickClient(auth.accessToken);
487
- const cfg = getConfig();
488
- if (cfg.defaultProjectId) {
489
- const tasks = await api.listTasks(cfg.defaultProjectId);
489
+ if (config2.defaultProjectId) {
490
+ const tasks = await api.listTasks(config2.defaultProjectId);
490
491
  ticktick = tasks.filter((t) => t.status !== 2 /* Completed */);
491
492
  }
492
493
  } catch (err) {
@@ -515,7 +516,7 @@ var init_fetch = __esm({
515
516
  init_config();
516
517
  init_github();
517
518
  init_types();
518
- init_constants();
519
+ init_utils();
519
520
  SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/i;
520
521
  }
521
522
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api.ts","../src/config.ts","../src/github.ts","../src/types.ts","../src/board/constants.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\ninterface AuthData {\n accessToken: string;\n clientId: string;\n clientSecret: string;\n openrouterApiKey?: string;\n}\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});\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 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 return JSON.parse(readFileSync(AUTH_FILE, \"utf-8\"));\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 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!) {\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 ]);\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!) {\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 ]);\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","/**\n * Shared board constants and utilities.\n * Extracted to prevent duplication across components and hooks.\n */\n\n/** Statuses that trigger completion actions (TickTick close, project complete). */\nexport const TERMINAL_STATUS_RE = /^(done|shipped|won't|wont|closed|complete|completed)$/i;\n\nexport function isTerminalStatus(status: string): boolean {\n return TERMINAL_STATUS_RE.test(status);\n}\n\n/** Returns true if a nav ID is a header row (not a navigable issue/task). */\nexport function isHeaderId(id: string | null): boolean {\n return id != null && (id.startsWith(\"header:\") || id.startsWith(\"sub:\"));\n}\n\n/** Formats a date as a relative \"Xm ago\" string. */\nexport function timeAgo(date: Date): string {\n const seconds = Math.floor((Date.now() - date.getTime()) / 1000);\n if (seconds < 10) return \"just now\";\n if (seconds < 60) return `${seconds}s ago`;\n const minutes = Math.floor(seconds / 60);\n return `${minutes}m ago`;\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 { TickTickClient } from \"../api.js\";\nimport type { HogConfig, RepoConfig } from \"../config.js\";\nimport { getConfig, 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 \"./constants.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 const cfg = getConfig();\n if (cfg.defaultProjectId) {\n const tasks = await api.listTasks(cfg.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/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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondrej-svec/hog",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Personal command deck — unified task dashboard for GitHub Projects + TickTick",
5
5
  "author": "Ondrej Svec",
6
6
  "repository": {