@ondrej-svec/hog 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,469 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/api.ts
12
+ var BASE_URL, TickTickClient;
13
+ var init_api = __esm({
14
+ "src/api.ts"() {
15
+ "use strict";
16
+ BASE_URL = "https://api.ticktick.com/open/v1";
17
+ TickTickClient = class {
18
+ token;
19
+ constructor(token) {
20
+ this.token = token;
21
+ }
22
+ async request(method, path, body) {
23
+ const url = `${BASE_URL}${path}`;
24
+ const init = {
25
+ method,
26
+ headers: {
27
+ Authorization: `Bearer ${this.token}`,
28
+ "Content-Type": "application/json"
29
+ }
30
+ };
31
+ if (body !== void 0) {
32
+ init.body = JSON.stringify(body);
33
+ }
34
+ const res = await fetch(url, init);
35
+ if (!res.ok) {
36
+ const text2 = await res.text();
37
+ throw new Error(`TickTick API error ${res.status}: ${text2}`);
38
+ }
39
+ const text = await res.text();
40
+ if (!text) return void 0;
41
+ return JSON.parse(text);
42
+ }
43
+ async listProjects() {
44
+ return this.request("GET", "/project");
45
+ }
46
+ async getProject(projectId) {
47
+ return this.request("GET", `/project/${projectId}`);
48
+ }
49
+ async getProjectData(projectId) {
50
+ return this.request("GET", `/project/${projectId}/data`);
51
+ }
52
+ async listTasks(projectId) {
53
+ const data = await this.getProjectData(projectId);
54
+ return data.tasks ?? [];
55
+ }
56
+ async getTask(projectId, taskId) {
57
+ return this.request("GET", `/project/${projectId}/task/${taskId}`);
58
+ }
59
+ async createTask(input) {
60
+ return this.request("POST", "/task", input);
61
+ }
62
+ async updateTask(input) {
63
+ return this.request("POST", `/task/${input.id}`, input);
64
+ }
65
+ async completeTask(projectId, taskId) {
66
+ await this.request("POST", `/project/${projectId}/task/${taskId}/complete`);
67
+ }
68
+ async deleteTask(projectId, taskId) {
69
+ await this.request("DELETE", `/project/${projectId}/task/${taskId}`);
70
+ }
71
+ };
72
+ }
73
+ });
74
+
75
+ // src/config.ts
76
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
77
+ import { homedir } from "os";
78
+ import { join } from "path";
79
+ import { z } from "zod";
80
+ function getAuth() {
81
+ if (!existsSync(AUTH_FILE)) return null;
82
+ try {
83
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+ function getConfig() {
89
+ if (!existsSync(CONFIG_FILE)) return {};
90
+ try {
91
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
92
+ } catch {
93
+ return {};
94
+ }
95
+ }
96
+ function requireAuth() {
97
+ const auth = getAuth();
98
+ if (!auth) {
99
+ console.error("Not authenticated. Run `hog init` first.");
100
+ process.exit(1);
101
+ }
102
+ return auth;
103
+ }
104
+ var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA;
105
+ var init_config = __esm({
106
+ "src/config.ts"() {
107
+ "use strict";
108
+ CONFIG_DIR = join(homedir(), ".config", "hog");
109
+ AUTH_FILE = join(CONFIG_DIR, "auth.json");
110
+ CONFIG_FILE = join(CONFIG_DIR, "config.json");
111
+ COMPLETION_ACTION_SCHEMA = z.discriminatedUnion("type", [
112
+ z.object({ type: z.literal("updateProjectStatus"), optionId: z.string() }),
113
+ z.object({ type: z.literal("closeIssue") }),
114
+ z.object({ type: z.literal("addLabel"), label: z.string() })
115
+ ]);
116
+ REPO_NAME_PATTERN = /^[\w.-]+\/[\w.-]+$/;
117
+ REPO_CONFIG_SCHEMA = z.object({
118
+ name: z.string().regex(REPO_NAME_PATTERN, "Must be owner/repo format"),
119
+ shortName: z.string().min(1),
120
+ projectNumber: z.number().int().positive(),
121
+ statusFieldId: z.string().min(1),
122
+ completionAction: COMPLETION_ACTION_SCHEMA,
123
+ statusGroups: z.array(z.string()).optional()
124
+ });
125
+ BOARD_CONFIG_SCHEMA = z.object({
126
+ refreshInterval: z.number().int().min(10).default(60),
127
+ backlogLimit: z.number().int().min(1).default(20),
128
+ assignee: z.string().min(1),
129
+ focusDuration: z.number().int().min(60).default(1500)
130
+ });
131
+ TICKTICK_CONFIG_SCHEMA = z.object({
132
+ enabled: z.boolean().default(true)
133
+ });
134
+ PROFILE_SCHEMA = z.object({
135
+ repos: z.array(REPO_CONFIG_SCHEMA).default([]),
136
+ board: BOARD_CONFIG_SCHEMA,
137
+ ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true })
138
+ });
139
+ HOG_CONFIG_SCHEMA = z.object({
140
+ version: z.number().int().default(3),
141
+ defaultProjectId: z.string().optional(),
142
+ defaultProjectName: z.string().optional(),
143
+ repos: z.array(REPO_CONFIG_SCHEMA).default([]),
144
+ board: BOARD_CONFIG_SCHEMA,
145
+ ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true }),
146
+ profiles: z.record(z.string(), PROFILE_SCHEMA).default({}),
147
+ defaultProfile: z.string().optional()
148
+ });
149
+ }
150
+ });
151
+
152
+ // src/github.ts
153
+ import { execFile, execFileSync } from "child_process";
154
+ import { promisify } from "util";
155
+ function runGh(args) {
156
+ return execFileSync("gh", args, { encoding: "utf-8", timeout: 3e4 }).trim();
157
+ }
158
+ function runGhJson(args) {
159
+ const output = runGh(args);
160
+ return JSON.parse(output);
161
+ }
162
+ function fetchRepoIssues(repo, options2 = {}) {
163
+ const { state = "open", limit = 100 } = options2;
164
+ const args = [
165
+ "issue",
166
+ "list",
167
+ "--repo",
168
+ repo,
169
+ "--state",
170
+ state,
171
+ "--json",
172
+ "number,title,url,state,updatedAt,labels,assignees,body",
173
+ "--limit",
174
+ String(limit)
175
+ ];
176
+ if (options2.assignee) {
177
+ args.push("--assignee", options2.assignee);
178
+ }
179
+ return runGhJson(args);
180
+ }
181
+ function fetchProjectEnrichment(repo, projectNumber) {
182
+ const [owner] = repo.split("/");
183
+ const query = `
184
+ query($owner: String!, $projectNumber: Int!) {
185
+ organization(login: $owner) {
186
+ projectV2(number: $projectNumber) {
187
+ items(first: 100) {
188
+ nodes {
189
+ content {
190
+ ... on Issue {
191
+ number
192
+ }
193
+ }
194
+ fieldValues(first: 20) {
195
+ nodes {
196
+ ... on ProjectV2ItemFieldDateValue {
197
+ field { ... on ProjectV2Field { name } }
198
+ date
199
+ }
200
+ ... on ProjectV2ItemFieldSingleSelectValue {
201
+ field { ... on ProjectV2SingleSelectField { name } }
202
+ name
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ `;
212
+ try {
213
+ const result = runGhJson([
214
+ "api",
215
+ "graphql",
216
+ "-f",
217
+ `query=${query}`,
218
+ "-F",
219
+ `owner=${owner}`,
220
+ "-F",
221
+ `projectNumber=${String(projectNumber)}`
222
+ ]);
223
+ const items = result?.data?.organization?.projectV2?.items?.nodes ?? [];
224
+ const enrichMap = /* @__PURE__ */ new Map();
225
+ for (const item of items) {
226
+ if (!item?.content?.number) continue;
227
+ const enrichment = {};
228
+ const fieldValues = item.fieldValues?.nodes ?? [];
229
+ for (const fv of fieldValues) {
230
+ if (!fv) continue;
231
+ if ("date" in fv && fv.field?.name === "Target date" && fv.date) {
232
+ enrichment.targetDate = fv.date;
233
+ }
234
+ if ("name" in fv && fv.field?.name === "Status" && fv.name) {
235
+ enrichment.projectStatus = fv.name;
236
+ }
237
+ }
238
+ enrichMap.set(item.content.number, enrichment);
239
+ }
240
+ return enrichMap;
241
+ } catch {
242
+ return /* @__PURE__ */ new Map();
243
+ }
244
+ }
245
+ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
246
+ const [owner] = repo.split("/");
247
+ const query = `
248
+ query($owner: String!, $projectNumber: Int!) {
249
+ organization(login: $owner) {
250
+ projectV2(number: $projectNumber) {
251
+ field(name: "Status") {
252
+ ... on ProjectV2SingleSelectField {
253
+ options {
254
+ id
255
+ name
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ `;
263
+ try {
264
+ const result = runGhJson([
265
+ "api",
266
+ "graphql",
267
+ "-f",
268
+ `query=${query}`,
269
+ "-F",
270
+ `owner=${owner}`,
271
+ "-F",
272
+ `projectNumber=${String(projectNumber)}`
273
+ ]);
274
+ return result?.data?.organization?.projectV2?.field?.options ?? [];
275
+ } catch {
276
+ return [];
277
+ }
278
+ }
279
+ var execFileAsync;
280
+ var init_github = __esm({
281
+ "src/github.ts"() {
282
+ "use strict";
283
+ execFileAsync = promisify(execFile);
284
+ }
285
+ });
286
+
287
+ // src/types.ts
288
+ var init_types = __esm({
289
+ "src/types.ts"() {
290
+ "use strict";
291
+ }
292
+ });
293
+
294
+ // src/board/fetch.ts
295
+ var fetch_exports = {};
296
+ __export(fetch_exports, {
297
+ SLACK_URL_RE: () => SLACK_URL_RE,
298
+ extractSlackUrl: () => extractSlackUrl,
299
+ fetchDashboard: () => fetchDashboard,
300
+ fetchRecentActivity: () => fetchRecentActivity
301
+ });
302
+ import { execFileSync as execFileSync2 } from "child_process";
303
+ function extractSlackUrl(body) {
304
+ if (!body) return void 0;
305
+ const match = body.match(SLACK_URL_RE);
306
+ return match?.[0];
307
+ }
308
+ function formatError(err) {
309
+ return err instanceof Error ? err.message : String(err);
310
+ }
311
+ function fetchRecentActivity(repoName, shortName) {
312
+ try {
313
+ const output = execFileSync2(
314
+ "gh",
315
+ [
316
+ "api",
317
+ `repos/${repoName}/events`,
318
+ "--paginate",
319
+ "-q",
320
+ '.[] | 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}'
321
+ ],
322
+ { encoding: "utf-8", timeout: 15e3 }
323
+ );
324
+ const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
325
+ const events = [];
326
+ for (const line of output.trim().split("\n")) {
327
+ if (!line.trim()) continue;
328
+ try {
329
+ const ev = JSON.parse(line);
330
+ const timestamp = new Date(ev.created_at);
331
+ if (timestamp.getTime() < cutoff) continue;
332
+ if (!ev.number) continue;
333
+ let eventType;
334
+ let summary;
335
+ if (ev.type === "IssueCommentEvent") {
336
+ eventType = "comment";
337
+ const preview = ev.body ? ev.body.slice(0, 60).replace(/\n/g, " ") : "";
338
+ summary = `commented on #${ev.number}${preview ? ` \u2014 "${preview}${(ev.body?.length ?? 0) > 60 ? "..." : ""}"` : ""}`;
339
+ } else if (ev.type === "IssuesEvent") {
340
+ switch (ev.action) {
341
+ case "opened":
342
+ eventType = "opened";
343
+ summary = `opened #${ev.number}: ${ev.title ?? ""}`;
344
+ break;
345
+ case "closed":
346
+ eventType = "closed";
347
+ summary = `closed #${ev.number}`;
348
+ break;
349
+ case "assigned":
350
+ eventType = "assignment";
351
+ summary = `assigned #${ev.number}`;
352
+ break;
353
+ case "labeled":
354
+ eventType = "labeled";
355
+ summary = `labeled #${ev.number}`;
356
+ break;
357
+ default:
358
+ continue;
359
+ }
360
+ } else {
361
+ continue;
362
+ }
363
+ events.push({
364
+ type: eventType,
365
+ repoShortName: shortName,
366
+ issueNumber: ev.number,
367
+ actor: ev.actor,
368
+ summary,
369
+ timestamp
370
+ });
371
+ } catch {
372
+ }
373
+ }
374
+ return events.slice(0, 15);
375
+ } catch {
376
+ return [];
377
+ }
378
+ }
379
+ async function fetchDashboard(config2, options2 = {}) {
380
+ const repos = options2.repoFilter ? config2.repos.filter(
381
+ (r) => r.shortName === options2.repoFilter || r.name === options2.repoFilter
382
+ ) : config2.repos;
383
+ const repoData = repos.map((repo) => {
384
+ try {
385
+ const fetchOpts = {};
386
+ if (options2.mineOnly) {
387
+ fetchOpts.assignee = config2.board.assignee;
388
+ }
389
+ const issues = fetchRepoIssues(repo.name, fetchOpts);
390
+ let statusOptions = [];
391
+ try {
392
+ const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);
393
+ for (const issue of issues) {
394
+ const e = enrichMap.get(issue.number);
395
+ if (e?.targetDate) issue.targetDate = e.targetDate;
396
+ if (e?.projectStatus) issue.projectStatus = e.projectStatus;
397
+ }
398
+ statusOptions = fetchProjectStatusOptions(
399
+ repo.name,
400
+ repo.projectNumber,
401
+ repo.statusFieldId
402
+ );
403
+ } catch {
404
+ }
405
+ for (const issue of issues) {
406
+ const slackUrl = extractSlackUrl(issue.body);
407
+ if (slackUrl) issue.slackThreadUrl = slackUrl;
408
+ }
409
+ return { repo, issues, statusOptions, error: null };
410
+ } catch (err) {
411
+ return { repo, issues: [], statusOptions: [], error: formatError(err) };
412
+ }
413
+ });
414
+ let ticktick = [];
415
+ let ticktickError = null;
416
+ if (config2.ticktick.enabled) {
417
+ try {
418
+ const auth = requireAuth();
419
+ const api = new TickTickClient(auth.accessToken);
420
+ const cfg = getConfig();
421
+ if (cfg.defaultProjectId) {
422
+ const tasks = await api.listTasks(cfg.defaultProjectId);
423
+ ticktick = tasks.filter((t) => t.status !== 2 /* Completed */);
424
+ }
425
+ } catch (err) {
426
+ ticktickError = formatError(err);
427
+ }
428
+ }
429
+ const activity = [];
430
+ for (const repo of repos) {
431
+ const events = fetchRecentActivity(repo.name, repo.shortName);
432
+ activity.push(...events);
433
+ }
434
+ activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
435
+ return {
436
+ repos: repoData,
437
+ ticktick,
438
+ ticktickError,
439
+ activity: activity.slice(0, 15),
440
+ fetchedAt: /* @__PURE__ */ new Date()
441
+ };
442
+ }
443
+ var SLACK_URL_RE;
444
+ var init_fetch = __esm({
445
+ "src/board/fetch.ts"() {
446
+ "use strict";
447
+ init_api();
448
+ init_config();
449
+ init_github();
450
+ init_types();
451
+ SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/i;
452
+ }
453
+ });
454
+
455
+ // src/board/fetch-worker.ts
456
+ import { parentPort, workerData } from "worker_threads";
457
+ var { config, options } = workerData;
458
+ var { fetchDashboard: fetchDashboard2 } = await Promise.resolve().then(() => (init_fetch(), fetch_exports));
459
+ if (!parentPort) throw new Error("fetch-worker must run in a worker thread");
460
+ try {
461
+ const data = await fetchDashboard2(config, options);
462
+ parentPort.postMessage({ type: "success", data });
463
+ } catch (err) {
464
+ parentPort.postMessage({
465
+ type: "error",
466
+ error: err instanceof Error ? err.message : String(err)
467
+ });
468
+ }
469
+ //# sourceMappingURL=fetch-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api.ts","../src/config.ts","../src/github.ts","../src/types.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> {\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 undefined as T;\n return JSON.parse(text) as T;\n }\n\n async listProjects(): Promise<Project[]> {\n return this.request<Project[]>(\"GET\", \"/project\");\n }\n\n async getProject(projectId: string): Promise<Project> {\n return this.request<Project>(\"GET\", `/project/${projectId}`);\n }\n\n async getProjectData(projectId: string): Promise<ProjectData> {\n return this.request<ProjectData>(\"GET\", `/project/${projectId}/data`);\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 return this.request<Task>(\"GET\", `/project/${projectId}/task/${taskId}`);\n }\n\n async createTask(input: CreateTaskInput): Promise<Task> {\n return this.request<Task>(\"POST\", \"/task\", input);\n }\n\n async updateTask(input: UpdateTaskInput): Promise<Task> {\n return this.request<Task>(\"POST\", `/task/${input.id}`, input);\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 { join } 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}\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 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 completionAction: COMPLETION_ACTION_SCHEMA,\n statusGroups: z.array(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});\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// ── Legacy Repo Defaults (for migration) ──\n\nconst LEGACY_REPOS: RepoConfig[] = [];\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: LEGACY_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 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}\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 number: number;\n title: string;\n url: string;\n state: string;\n updatedAt: string;\n labels: { name: string }[];\n assignees?: { login: string }[];\n targetDate?: string;\n body?: string;\n projectStatus?: string;\n slackThreadUrl?: string;\n}\n\nexport interface ProjectFieldValues {\n targetDate?: string;\n status?: string;\n}\n\nexport interface RepoProjectConfig {\n projectNumber: number;\n statusFieldId: string;\n optionId: string;\n}\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 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 }\n }\n }\n }\n }\n }\n }\n `;\n\n const [owner, repoName] = repo.split(\"/\");\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 if (\"date\" in fv && fv.field?.name === \"Target date\") {\n fields.targetDate = fv.date;\n }\n if (\"name\" in fv && fv.field?.name === \"Status\") {\n fields.status = fv.name;\n }\n }\n\n return fields;\n } catch {\n return {};\n }\n}\n\nexport interface ProjectEnrichment {\n targetDate?: string;\n projectStatus?: string;\n}\n\n/**\n * Fetch target dates and project statuses for all issues in a project in one GraphQL call.\n * Returns a Map from issue number to enrichment data.\n */\nexport function fetchProjectEnrichment(\n repo: string,\n projectNumber: number,\n): Map<number, ProjectEnrichment> {\n const [owner] = repo.split(\"/\");\n\n const query = `\n query($owner: String!, $projectNumber: Int!) {\n organization(login: $owner) {\n projectV2(number: $projectNumber) {\n items(first: 100) {\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 }\n }\n }\n }\n }\n }\n }\n `;\n\n try {\n const result = runGhJson<ProjectItemsResult>([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `projectNumber=${String(projectNumber)}`,\n ]);\n\n const items = result?.data?.organization?.projectV2?.items?.nodes ?? [];\n const enrichMap = new Map<number, ProjectEnrichment>();\n\n for (const item of items) {\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 if (\"date\" in fv && fv.field?.name === \"Target date\" && fv.date) {\n enrichment.targetDate = fv.date;\n }\n if (\"name\" in fv && fv.field?.name === \"Status\" && fv.name) {\n enrichment.projectStatus = fv.name;\n }\n }\n enrichMap.set(item.content.number, enrichment);\n }\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\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 function updateProjectItemStatus(\n repo: string,\n issueNumber: number,\n projectConfig: RepoProjectConfig,\n): void {\n const [owner, repoName] = repo.split(\"/\");\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\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 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;\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\n// Internal GraphQL response types\n\ninterface FieldValue {\n field?: { name?: string };\n date?: string;\n name?: string;\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 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","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\";\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\nfunction formatError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\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 statusOptions: StatusOption[] = [];\n try {\n const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);\n for (const issue of issues) {\n const e = enrichMap.get(issue.number);\n if (e?.targetDate) issue.targetDate = e.targetDate;\n if (e?.projectStatus) issue.projectStatus = e.projectStatus;\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 }\n\n // Compute Slack thread URLs from issue bodies\n for (const issue of issues) {\n const slackUrl = extractSlackUrl(issue.body);\n if (slackUrl) issue.slackThreadUrl = slackUrl;\n }\n\n return { repo, issues, 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"]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@ondrej-svec/hog",
3
+ "version": "1.1.1",
4
+ "description": "Personal command deck — unified task dashboard for GitHub Projects + TickTick",
5
+ "author": "Ondrej Svec",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ondrej-svec/hog.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "hog": "dist/cli.js"
16
+ },
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsx src/cli.ts",
20
+ "start": "node dist/cli.js",
21
+ "check": "biome check src/",
22
+ "check:fix": "biome check --write src/",
23
+ "lint": "biome lint src/",
24
+ "format": "biome format --write src/",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:coverage": "vitest run --coverage",
29
+ "ci": "npm run typecheck && npm run check && npm run test",
30
+ "prepublishOnly": "npm run ci && npm run build"
31
+ },
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "keywords": [
39
+ "cli",
40
+ "ticktick",
41
+ "github-projects",
42
+ "task-manager",
43
+ "dashboard",
44
+ "tui",
45
+ "agent-friendly",
46
+ "command-deck"
47
+ ],
48
+ "license": "MIT",
49
+ "devDependencies": {
50
+ "@biomejs/biome": "^2.4.1",
51
+ "@types/node": "^22.0.0",
52
+ "@types/react": "^19.2.14",
53
+ "@vitest/coverage-v8": "^4.0.18",
54
+ "ink-testing-library": "^4.0.0",
55
+ "tsup": "^8.5.1",
56
+ "tsx": "^4.19.0",
57
+ "typescript": "^5.7.0",
58
+ "vitest": "^4.0.18"
59
+ },
60
+ "dependencies": {
61
+ "@inkjs/ui": "^2.0.0",
62
+ "@inquirer/prompts": "^8.2.1",
63
+ "chalk": "^5.6.2",
64
+ "commander": "^14.0.3",
65
+ "ink": "^6.7.0",
66
+ "react": "^19.2.4",
67
+ "zod": "^4.3.6"
68
+ }
69
+ }