@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.
package/dist/cli.js ADDED
@@ -0,0 +1,4988 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/api.ts
13
+ var BASE_URL, TickTickClient;
14
+ var init_api = __esm({
15
+ "src/api.ts"() {
16
+ "use strict";
17
+ BASE_URL = "https://api.ticktick.com/open/v1";
18
+ TickTickClient = class {
19
+ token;
20
+ constructor(token) {
21
+ this.token = token;
22
+ }
23
+ async request(method, path, body) {
24
+ const url = `${BASE_URL}${path}`;
25
+ const init = {
26
+ method,
27
+ headers: {
28
+ Authorization: `Bearer ${this.token}`,
29
+ "Content-Type": "application/json"
30
+ }
31
+ };
32
+ if (body !== void 0) {
33
+ init.body = JSON.stringify(body);
34
+ }
35
+ const res = await fetch(url, init);
36
+ if (!res.ok) {
37
+ const text2 = await res.text();
38
+ throw new Error(`TickTick API error ${res.status}: ${text2}`);
39
+ }
40
+ const text = await res.text();
41
+ if (!text) return void 0;
42
+ return JSON.parse(text);
43
+ }
44
+ async listProjects() {
45
+ return this.request("GET", "/project");
46
+ }
47
+ async getProject(projectId) {
48
+ return this.request("GET", `/project/${projectId}`);
49
+ }
50
+ async getProjectData(projectId) {
51
+ return this.request("GET", `/project/${projectId}/data`);
52
+ }
53
+ async listTasks(projectId) {
54
+ const data = await this.getProjectData(projectId);
55
+ return data.tasks ?? [];
56
+ }
57
+ async getTask(projectId, taskId) {
58
+ return this.request("GET", `/project/${projectId}/task/${taskId}`);
59
+ }
60
+ async createTask(input2) {
61
+ return this.request("POST", "/task", input2);
62
+ }
63
+ async updateTask(input2) {
64
+ return this.request("POST", `/task/${input2.id}`, input2);
65
+ }
66
+ async completeTask(projectId, taskId) {
67
+ await this.request("POST", `/project/${projectId}/task/${taskId}/complete`);
68
+ }
69
+ async deleteTask(projectId, taskId) {
70
+ await this.request("DELETE", `/project/${projectId}/task/${taskId}`);
71
+ }
72
+ };
73
+ }
74
+ });
75
+
76
+ // src/config.ts
77
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
78
+ import { homedir } from "os";
79
+ import { join } from "path";
80
+ import { z } from "zod";
81
+ function migrateConfig(raw) {
82
+ const version = typeof raw["version"] === "number" ? raw["version"] : 1;
83
+ if (version < 2) {
84
+ raw = {
85
+ ...raw,
86
+ version: 2,
87
+ repos: LEGACY_REPOS,
88
+ board: {
89
+ refreshInterval: 60,
90
+ backlogLimit: 20,
91
+ assignee: "unknown"
92
+ }
93
+ };
94
+ }
95
+ const currentVersion = typeof raw["version"] === "number" ? raw["version"] : 2;
96
+ if (currentVersion < 3) {
97
+ raw = {
98
+ ...raw,
99
+ version: 3,
100
+ ticktick: { enabled: existsSync(AUTH_FILE) }
101
+ };
102
+ }
103
+ return HOG_CONFIG_SCHEMA.parse(raw);
104
+ }
105
+ function loadFullConfig() {
106
+ const raw = loadRawConfig();
107
+ if (Object.keys(raw).length === 0) {
108
+ const config2 = migrateConfig({});
109
+ saveFullConfig(config2);
110
+ return config2;
111
+ }
112
+ const version = typeof raw["version"] === "number" ? raw["version"] : 1;
113
+ if (version < 3) {
114
+ const migrated = migrateConfig(raw);
115
+ saveFullConfig(migrated);
116
+ return migrated;
117
+ }
118
+ return HOG_CONFIG_SCHEMA.parse(raw);
119
+ }
120
+ function saveFullConfig(config2) {
121
+ ensureDir();
122
+ writeFileSync(CONFIG_FILE, `${JSON.stringify(config2, null, 2)}
123
+ `, { mode: 384 });
124
+ }
125
+ function loadRawConfig() {
126
+ if (!existsSync(CONFIG_FILE)) return {};
127
+ try {
128
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
129
+ } catch {
130
+ return {};
131
+ }
132
+ }
133
+ function resolveProfile(config2, profileName) {
134
+ const name = profileName ?? config2.defaultProfile;
135
+ if (!name) {
136
+ return { resolved: config2, activeProfile: null };
137
+ }
138
+ const profile = config2.profiles[name];
139
+ if (!profile) {
140
+ console.error(
141
+ `Profile "${name}" not found. Available: ${Object.keys(config2.profiles).join(", ") || "(none)"}`
142
+ );
143
+ process.exit(1);
144
+ }
145
+ return {
146
+ resolved: { ...config2, repos: profile.repos, board: profile.board, ticktick: profile.ticktick },
147
+ activeProfile: name
148
+ };
149
+ }
150
+ function findRepo(config2, shortNameOrFull) {
151
+ return config2.repos.find((r) => r.shortName === shortNameOrFull || r.name === shortNameOrFull);
152
+ }
153
+ function validateRepoName(name) {
154
+ return REPO_NAME_PATTERN.test(name);
155
+ }
156
+ function ensureDir() {
157
+ mkdirSync(CONFIG_DIR, { recursive: true });
158
+ }
159
+ function getAuth() {
160
+ if (!existsSync(AUTH_FILE)) return null;
161
+ try {
162
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+ function saveAuth(data) {
168
+ ensureDir();
169
+ writeFileSync(AUTH_FILE, `${JSON.stringify(data, null, 2)}
170
+ `, {
171
+ mode: 384
172
+ });
173
+ }
174
+ function getConfig() {
175
+ if (!existsSync(CONFIG_FILE)) return {};
176
+ try {
177
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
178
+ } catch {
179
+ return {};
180
+ }
181
+ }
182
+ function saveConfig(data) {
183
+ ensureDir();
184
+ const existing = getConfig();
185
+ writeFileSync(CONFIG_FILE, `${JSON.stringify({ ...existing, ...data }, null, 2)}
186
+ `);
187
+ }
188
+ function requireAuth() {
189
+ const auth = getAuth();
190
+ if (!auth) {
191
+ console.error("Not authenticated. Run `hog init` first.");
192
+ process.exit(1);
193
+ }
194
+ return auth;
195
+ }
196
+ 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, LEGACY_REPOS;
197
+ var init_config = __esm({
198
+ "src/config.ts"() {
199
+ "use strict";
200
+ CONFIG_DIR = join(homedir(), ".config", "hog");
201
+ AUTH_FILE = join(CONFIG_DIR, "auth.json");
202
+ CONFIG_FILE = join(CONFIG_DIR, "config.json");
203
+ COMPLETION_ACTION_SCHEMA = z.discriminatedUnion("type", [
204
+ z.object({ type: z.literal("updateProjectStatus"), optionId: z.string() }),
205
+ z.object({ type: z.literal("closeIssue") }),
206
+ z.object({ type: z.literal("addLabel"), label: z.string() })
207
+ ]);
208
+ REPO_NAME_PATTERN = /^[\w.-]+\/[\w.-]+$/;
209
+ REPO_CONFIG_SCHEMA = z.object({
210
+ name: z.string().regex(REPO_NAME_PATTERN, "Must be owner/repo format"),
211
+ shortName: z.string().min(1),
212
+ projectNumber: z.number().int().positive(),
213
+ statusFieldId: z.string().min(1),
214
+ completionAction: COMPLETION_ACTION_SCHEMA,
215
+ statusGroups: z.array(z.string()).optional()
216
+ });
217
+ BOARD_CONFIG_SCHEMA = z.object({
218
+ refreshInterval: z.number().int().min(10).default(60),
219
+ backlogLimit: z.number().int().min(1).default(20),
220
+ assignee: z.string().min(1),
221
+ focusDuration: z.number().int().min(60).default(1500)
222
+ });
223
+ TICKTICK_CONFIG_SCHEMA = z.object({
224
+ enabled: z.boolean().default(true)
225
+ });
226
+ PROFILE_SCHEMA = z.object({
227
+ repos: z.array(REPO_CONFIG_SCHEMA).default([]),
228
+ board: BOARD_CONFIG_SCHEMA,
229
+ ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true })
230
+ });
231
+ HOG_CONFIG_SCHEMA = z.object({
232
+ version: z.number().int().default(3),
233
+ defaultProjectId: z.string().optional(),
234
+ defaultProjectName: z.string().optional(),
235
+ repos: z.array(REPO_CONFIG_SCHEMA).default([]),
236
+ board: BOARD_CONFIG_SCHEMA,
237
+ ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true }),
238
+ profiles: z.record(z.string(), PROFILE_SCHEMA).default({}),
239
+ defaultProfile: z.string().optional()
240
+ });
241
+ LEGACY_REPOS = [];
242
+ }
243
+ });
244
+
245
+ // src/types.ts
246
+ var init_types = __esm({
247
+ "src/types.ts"() {
248
+ "use strict";
249
+ }
250
+ });
251
+
252
+ // src/github.ts
253
+ import { execFile, execFileSync as execFileSync2 } from "child_process";
254
+ import { promisify } from "util";
255
+ function runGh(args) {
256
+ return execFileSync2("gh", args, { encoding: "utf-8", timeout: 3e4 }).trim();
257
+ }
258
+ function runGhJson(args) {
259
+ const output = runGh(args);
260
+ return JSON.parse(output);
261
+ }
262
+ async function runGhAsync(args) {
263
+ const { stdout } = await execFileAsync("gh", args, { encoding: "utf-8", timeout: 3e4 });
264
+ return stdout.trim();
265
+ }
266
+ async function runGhJsonAsync(args) {
267
+ const output = await runGhAsync(args);
268
+ return JSON.parse(output);
269
+ }
270
+ function fetchAssignedIssues(repo, assignee) {
271
+ return runGhJson([
272
+ "issue",
273
+ "list",
274
+ "--repo",
275
+ repo,
276
+ "--assignee",
277
+ assignee,
278
+ "--state",
279
+ "open",
280
+ "--json",
281
+ "number,title,url,state,updatedAt,labels",
282
+ "--limit",
283
+ "100"
284
+ ]);
285
+ }
286
+ function fetchRepoIssues(repo, options = {}) {
287
+ const { state = "open", limit = 100 } = options;
288
+ const args = [
289
+ "issue",
290
+ "list",
291
+ "--repo",
292
+ repo,
293
+ "--state",
294
+ state,
295
+ "--json",
296
+ "number,title,url,state,updatedAt,labels,assignees,body",
297
+ "--limit",
298
+ String(limit)
299
+ ];
300
+ if (options.assignee) {
301
+ args.push("--assignee", options.assignee);
302
+ }
303
+ return runGhJson(args);
304
+ }
305
+ function assignIssue(repo, issueNumber) {
306
+ runGh(["issue", "edit", String(issueNumber), "--repo", repo, "--add-assignee", "@me"]);
307
+ }
308
+ async function assignIssueAsync(repo, issueNumber) {
309
+ await runGhAsync(["issue", "edit", String(issueNumber), "--repo", repo, "--add-assignee", "@me"]);
310
+ }
311
+ function fetchProjectFields(repo, issueNumber, projectNumber) {
312
+ const query = `
313
+ query($owner: String!, $repo: String!, $issueNumber: Int!) {
314
+ repository(owner: $owner, name: $repo) {
315
+ issue(number: $issueNumber) {
316
+ projectItems(first: 10) {
317
+ nodes {
318
+ project { number }
319
+ fieldValues(first: 20) {
320
+ nodes {
321
+ ... on ProjectV2ItemFieldDateValue {
322
+ field { ... on ProjectV2Field { name } }
323
+ date
324
+ }
325
+ ... on ProjectV2ItemFieldSingleSelectValue {
326
+ field { ... on ProjectV2SingleSelectField { name } }
327
+ name
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ `;
337
+ const [owner, repoName] = repo.split("/");
338
+ try {
339
+ const result = runGhJson([
340
+ "api",
341
+ "graphql",
342
+ "-f",
343
+ `query=${query}`,
344
+ "-F",
345
+ `owner=${owner}`,
346
+ "-F",
347
+ `repo=${repoName}`,
348
+ "-F",
349
+ `issueNumber=${String(issueNumber)}`
350
+ ]);
351
+ const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];
352
+ const projectItem = items.find((item) => item?.project?.number === projectNumber);
353
+ if (!projectItem) return {};
354
+ const fields = {};
355
+ const fieldValues = projectItem.fieldValues?.nodes ?? [];
356
+ for (const fv of fieldValues) {
357
+ if (!fv) continue;
358
+ if ("date" in fv && fv.field?.name === "Target date") {
359
+ fields.targetDate = fv.date;
360
+ }
361
+ if ("name" in fv && fv.field?.name === "Status") {
362
+ fields.status = fv.name;
363
+ }
364
+ }
365
+ return fields;
366
+ } catch {
367
+ return {};
368
+ }
369
+ }
370
+ function fetchProjectEnrichment(repo, projectNumber) {
371
+ const [owner] = repo.split("/");
372
+ const query = `
373
+ query($owner: String!, $projectNumber: Int!) {
374
+ organization(login: $owner) {
375
+ projectV2(number: $projectNumber) {
376
+ items(first: 100) {
377
+ nodes {
378
+ content {
379
+ ... on Issue {
380
+ number
381
+ }
382
+ }
383
+ fieldValues(first: 20) {
384
+ nodes {
385
+ ... on ProjectV2ItemFieldDateValue {
386
+ field { ... on ProjectV2Field { name } }
387
+ date
388
+ }
389
+ ... on ProjectV2ItemFieldSingleSelectValue {
390
+ field { ... on ProjectV2SingleSelectField { name } }
391
+ name
392
+ }
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ `;
401
+ try {
402
+ const result = runGhJson([
403
+ "api",
404
+ "graphql",
405
+ "-f",
406
+ `query=${query}`,
407
+ "-F",
408
+ `owner=${owner}`,
409
+ "-F",
410
+ `projectNumber=${String(projectNumber)}`
411
+ ]);
412
+ const items = result?.data?.organization?.projectV2?.items?.nodes ?? [];
413
+ const enrichMap = /* @__PURE__ */ new Map();
414
+ for (const item of items) {
415
+ if (!item?.content?.number) continue;
416
+ const enrichment = {};
417
+ const fieldValues = item.fieldValues?.nodes ?? [];
418
+ for (const fv of fieldValues) {
419
+ if (!fv) continue;
420
+ if ("date" in fv && fv.field?.name === "Target date" && fv.date) {
421
+ enrichment.targetDate = fv.date;
422
+ }
423
+ if ("name" in fv && fv.field?.name === "Status" && fv.name) {
424
+ enrichment.projectStatus = fv.name;
425
+ }
426
+ }
427
+ enrichMap.set(item.content.number, enrichment);
428
+ }
429
+ return enrichMap;
430
+ } catch {
431
+ return /* @__PURE__ */ new Map();
432
+ }
433
+ }
434
+ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
435
+ const [owner] = repo.split("/");
436
+ const query = `
437
+ query($owner: String!, $projectNumber: Int!) {
438
+ organization(login: $owner) {
439
+ projectV2(number: $projectNumber) {
440
+ field(name: "Status") {
441
+ ... on ProjectV2SingleSelectField {
442
+ options {
443
+ id
444
+ name
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+ `;
452
+ try {
453
+ const result = runGhJson([
454
+ "api",
455
+ "graphql",
456
+ "-f",
457
+ `query=${query}`,
458
+ "-F",
459
+ `owner=${owner}`,
460
+ "-F",
461
+ `projectNumber=${String(projectNumber)}`
462
+ ]);
463
+ return result?.data?.organization?.projectV2?.field?.options ?? [];
464
+ } catch {
465
+ return [];
466
+ }
467
+ }
468
+ function addLabel(repo, issueNumber, label) {
469
+ runGh(["issue", "edit", String(issueNumber), "--repo", repo, "--add-label", label]);
470
+ }
471
+ function updateProjectItemStatus(repo, issueNumber, projectConfig) {
472
+ const [owner, repoName] = repo.split("/");
473
+ const findItemQuery = `
474
+ query($owner: String!, $repo: String!, $issueNumber: Int!) {
475
+ repository(owner: $owner, name: $repo) {
476
+ issue(number: $issueNumber) {
477
+ projectItems(first: 10) {
478
+ nodes {
479
+ id
480
+ project { number }
481
+ }
482
+ }
483
+ }
484
+ }
485
+ }
486
+ `;
487
+ const findResult = runGhJson([
488
+ "api",
489
+ "graphql",
490
+ "-f",
491
+ `query=${findItemQuery}`,
492
+ "-F",
493
+ `owner=${owner}`,
494
+ "-F",
495
+ `repo=${repoName}`,
496
+ "-F",
497
+ `issueNumber=${String(issueNumber)}`
498
+ ]);
499
+ const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];
500
+ const projectNumber = projectConfig.projectNumber;
501
+ const projectItem = items.find((item) => item?.project?.number === projectNumber);
502
+ if (!projectItem?.id) return;
503
+ const projectQuery = `
504
+ query($owner: String!) {
505
+ organization(login: $owner) {
506
+ projectV2(number: ${projectNumber}) {
507
+ id
508
+ }
509
+ }
510
+ }
511
+ `;
512
+ const projectResult = runGhJson([
513
+ "api",
514
+ "graphql",
515
+ "-f",
516
+ `query=${projectQuery}`,
517
+ "-F",
518
+ `owner=${owner}`
519
+ ]);
520
+ const projectId = projectResult?.data?.organization?.projectV2?.id;
521
+ if (!projectId) return;
522
+ const statusFieldId = projectConfig.statusFieldId;
523
+ const optionId = projectConfig.optionId;
524
+ const mutation = `
525
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
526
+ updateProjectV2ItemFieldValue(
527
+ input: {
528
+ projectId: $projectId
529
+ itemId: $itemId
530
+ fieldId: $fieldId
531
+ value: { singleSelectOptionId: $optionId }
532
+ }
533
+ ) {
534
+ projectV2Item { id }
535
+ }
536
+ }
537
+ `;
538
+ runGh([
539
+ "api",
540
+ "graphql",
541
+ "-f",
542
+ `query=${mutation}`,
543
+ "-F",
544
+ `projectId=${projectId}`,
545
+ "-F",
546
+ `itemId=${projectItem.id}`,
547
+ "-F",
548
+ `fieldId=${statusFieldId}`,
549
+ "-F",
550
+ `optionId=${optionId}`
551
+ ]);
552
+ }
553
+ async function updateProjectItemStatusAsync(repo, issueNumber, projectConfig) {
554
+ const [owner, repoName] = repo.split("/");
555
+ const findItemQuery = `
556
+ query($owner: String!, $repo: String!, $issueNumber: Int!) {
557
+ repository(owner: $owner, name: $repo) {
558
+ issue(number: $issueNumber) {
559
+ projectItems(first: 10) {
560
+ nodes {
561
+ id
562
+ project { number }
563
+ }
564
+ }
565
+ }
566
+ }
567
+ }
568
+ `;
569
+ const findResult = await runGhJsonAsync([
570
+ "api",
571
+ "graphql",
572
+ "-f",
573
+ `query=${findItemQuery}`,
574
+ "-F",
575
+ `owner=${owner}`,
576
+ "-F",
577
+ `repo=${repoName}`,
578
+ "-F",
579
+ `issueNumber=${String(issueNumber)}`
580
+ ]);
581
+ const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];
582
+ const projectNumber = projectConfig.projectNumber;
583
+ const projectItem = items.find((item) => item?.project?.number === projectNumber);
584
+ if (!projectItem?.id) return;
585
+ const projectQuery = `
586
+ query($owner: String!) {
587
+ organization(login: $owner) {
588
+ projectV2(number: ${projectNumber}) {
589
+ id
590
+ }
591
+ }
592
+ }
593
+ `;
594
+ const projectResult = await runGhJsonAsync([
595
+ "api",
596
+ "graphql",
597
+ "-f",
598
+ `query=${projectQuery}`,
599
+ "-F",
600
+ `owner=${owner}`
601
+ ]);
602
+ const projectId = projectResult?.data?.organization?.projectV2?.id;
603
+ if (!projectId) return;
604
+ const statusFieldId = projectConfig.statusFieldId;
605
+ const optionId = projectConfig.optionId;
606
+ const mutation = `
607
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
608
+ updateProjectV2ItemFieldValue(
609
+ input: {
610
+ projectId: $projectId
611
+ itemId: $itemId
612
+ fieldId: $fieldId
613
+ value: { singleSelectOptionId: $optionId }
614
+ }
615
+ ) {
616
+ projectV2Item { id }
617
+ }
618
+ }
619
+ `;
620
+ await runGhAsync([
621
+ "api",
622
+ "graphql",
623
+ "-f",
624
+ `query=${mutation}`,
625
+ "-F",
626
+ `projectId=${projectId}`,
627
+ "-F",
628
+ `itemId=${projectItem.id}`,
629
+ "-F",
630
+ `fieldId=${statusFieldId}`,
631
+ "-F",
632
+ `optionId=${optionId}`
633
+ ]);
634
+ }
635
+ var execFileAsync;
636
+ var init_github = __esm({
637
+ "src/github.ts"() {
638
+ "use strict";
639
+ execFileAsync = promisify(execFile);
640
+ }
641
+ });
642
+
643
+ // src/sync-state.ts
644
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
645
+ import { homedir as homedir2 } from "os";
646
+ import { join as join2 } from "path";
647
+ function loadSyncState() {
648
+ if (!existsSync3(STATE_FILE)) return { mappings: [] };
649
+ try {
650
+ return JSON.parse(readFileSync2(STATE_FILE, "utf-8"));
651
+ } catch {
652
+ return { mappings: [] };
653
+ }
654
+ }
655
+ function saveSyncState(state) {
656
+ writeFileSync2(STATE_FILE, `${JSON.stringify(state, null, 2)}
657
+ `);
658
+ }
659
+ function findMapping(state, githubRepo, issueNumber) {
660
+ return state.mappings.find(
661
+ (m) => m.githubRepo === githubRepo && m.githubIssueNumber === issueNumber
662
+ );
663
+ }
664
+ function upsertMapping(state, mapping) {
665
+ const idx = state.mappings.findIndex(
666
+ (m) => m.githubRepo === mapping.githubRepo && m.githubIssueNumber === mapping.githubIssueNumber
667
+ );
668
+ if (idx >= 0) {
669
+ state.mappings[idx] = mapping;
670
+ } else {
671
+ state.mappings.push(mapping);
672
+ }
673
+ }
674
+ function removeMapping(state, githubRepo, issueNumber) {
675
+ state.mappings = state.mappings.filter(
676
+ (m) => !(m.githubRepo === githubRepo && m.githubIssueNumber === issueNumber)
677
+ );
678
+ }
679
+ var CONFIG_DIR2, STATE_FILE;
680
+ var init_sync_state = __esm({
681
+ "src/sync-state.ts"() {
682
+ "use strict";
683
+ CONFIG_DIR2 = join2(homedir2(), ".config", "hog");
684
+ STATE_FILE = join2(CONFIG_DIR2, "sync-state.json");
685
+ }
686
+ });
687
+
688
+ // src/pick.ts
689
+ var pick_exports = {};
690
+ __export(pick_exports, {
691
+ parseIssueRef: () => parseIssueRef,
692
+ pickIssue: () => pickIssue
693
+ });
694
+ function parseIssueRef(input2, config2) {
695
+ const match = input2.match(ISSUE_REF_PATTERN);
696
+ if (!(match?.[1] && match[2])) {
697
+ throw new Error("Invalid format. Use: shortName/number (e.g., myrepo/145)");
698
+ }
699
+ const repoShortName2 = match[1];
700
+ const repo = findRepo(config2, repoShortName2);
701
+ if (!repo) {
702
+ throw new Error(`Unknown repo "${repoShortName2}". Run: hog config repos`);
703
+ }
704
+ const num = Number.parseInt(match[2], 10);
705
+ if (num < 1 || num > 999999) {
706
+ throw new Error("Invalid issue number");
707
+ }
708
+ return { repo, issueNumber: num };
709
+ }
710
+ function appendWarning(existing, addition) {
711
+ return existing ? `${existing}. ${addition}` : addition;
712
+ }
713
+ function mapPriority2(labels) {
714
+ for (const label of labels) {
715
+ if (label === "priority:critical" || label === "priority:high") return 5 /* High */;
716
+ if (label === "priority:medium") return 3 /* Medium */;
717
+ if (label === "priority:low") return 1 /* Low */;
718
+ }
719
+ return 0 /* None */;
720
+ }
721
+ function toBoardIssue(issue, repoName) {
722
+ return {
723
+ number: issue.number,
724
+ title: issue.title,
725
+ url: issue.url,
726
+ state: issue.state,
727
+ assignee: issue.assignees?.[0]?.login ?? null,
728
+ labels: issue.labels.map((l) => l.name),
729
+ updatedAt: issue.updatedAt,
730
+ repo: repoName
731
+ };
732
+ }
733
+ async function syncToTickTick(repo, issue, boardIssue) {
734
+ const state = loadSyncState();
735
+ const existing = findMapping(state, repo.name, issue.number);
736
+ if (existing) {
737
+ return { warning: "TickTick task already exists from sync." };
738
+ }
739
+ const auth = requireAuth();
740
+ const api = new TickTickClient(auth.accessToken);
741
+ const projectFields = fetchProjectFields(repo.name, issue.number, repo.projectNumber);
742
+ const input2 = {
743
+ title: issue.title,
744
+ content: `GitHub: ${issue.url}`,
745
+ priority: mapPriority2(boardIssue.labels),
746
+ tags: ["github", repo.shortName],
747
+ ...projectFields.targetDate ? { dueDate: projectFields.targetDate, isAllDay: true } : {}
748
+ };
749
+ const task2 = await api.createTask(input2);
750
+ upsertMapping(state, {
751
+ githubRepo: repo.name,
752
+ githubIssueNumber: issue.number,
753
+ githubUrl: issue.url,
754
+ ticktickTaskId: task2.id,
755
+ ticktickProjectId: task2.projectId,
756
+ githubUpdatedAt: issue.updatedAt,
757
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
758
+ });
759
+ saveSyncState(state);
760
+ return { task: task2 };
761
+ }
762
+ async function pickIssue(config2, ref) {
763
+ const { repo, issueNumber } = ref;
764
+ const allIssues = fetchRepoIssues(repo.name, { state: "open", limit: 200 });
765
+ const issue = allIssues.find((i) => i.number === issueNumber);
766
+ if (!issue) {
767
+ throw new Error(`Issue #${issueNumber} not found in ${repo.name}. Is it open?`);
768
+ }
769
+ const boardIssue = toBoardIssue(issue, repo.name);
770
+ let warning;
771
+ if (boardIssue.assignee === config2.board.assignee) {
772
+ warning = "Issue is already assigned to you";
773
+ } else if (boardIssue.assignee) {
774
+ warning = `Issue is currently assigned to ${boardIssue.assignee}. Reassigning to you.`;
775
+ }
776
+ assignIssue(repo.name, issueNumber);
777
+ let ticktickTask;
778
+ try {
779
+ const result = await syncToTickTick(repo, issue, boardIssue);
780
+ ticktickTask = result.task;
781
+ if (result.warning) {
782
+ warning = appendWarning(warning, result.warning);
783
+ }
784
+ } catch (err) {
785
+ const msg = err instanceof Error ? err.message : String(err);
786
+ warning = appendWarning(warning, `TickTick sync failed: ${msg}. Run 'hog sync run' to retry.`);
787
+ }
788
+ return {
789
+ success: true,
790
+ issue: boardIssue,
791
+ ...ticktickTask ? { ticktickTask } : {},
792
+ ...warning ? { warning } : {}
793
+ };
794
+ }
795
+ var ISSUE_REF_PATTERN;
796
+ var init_pick = __esm({
797
+ "src/pick.ts"() {
798
+ "use strict";
799
+ init_api();
800
+ init_config();
801
+ init_github();
802
+ init_sync_state();
803
+ init_types();
804
+ ISSUE_REF_PATTERN = /^([a-zA-Z0-9_.-]+)\/(\d+)$/;
805
+ }
806
+ });
807
+
808
+ // src/board/hooks/use-actions.ts
809
+ import { execFile as execFile2 } from "child_process";
810
+ import { promisify as promisify2 } from "util";
811
+ import { useCallback, useRef } from "react";
812
+ function findIssueContext(repos, selectedId, config2) {
813
+ if (!selectedId?.startsWith("gh:")) {
814
+ return { issue: null, repoName: null, repoConfig: null, statusOptions: [] };
815
+ }
816
+ for (const rd of repos) {
817
+ for (const issue of rd.issues) {
818
+ if (`gh:${rd.repo.name}:${issue.number}` === selectedId) {
819
+ const repoConfig = config2.repos.find((r) => r.name === rd.repo.name) ?? null;
820
+ return { issue, repoName: rd.repo.name, repoConfig, statusOptions: rd.statusOptions };
821
+ }
822
+ }
823
+ }
824
+ return { issue: null, repoName: null, repoConfig: null, statusOptions: [] };
825
+ }
826
+ async function triggerCompletionActionAsync(action, repoName, issueNumber) {
827
+ switch (action.type) {
828
+ case "closeIssue":
829
+ await execFileAsync2("gh", ["issue", "close", String(issueNumber), "--repo", repoName], {
830
+ encoding: "utf-8",
831
+ timeout: 3e4
832
+ });
833
+ break;
834
+ case "addLabel":
835
+ await execFileAsync2(
836
+ "gh",
837
+ ["issue", "edit", String(issueNumber), "--repo", repoName, "--add-label", action.label],
838
+ { encoding: "utf-8", timeout: 3e4 }
839
+ );
840
+ break;
841
+ case "updateProjectStatus":
842
+ break;
843
+ }
844
+ }
845
+ function optimisticSetStatus(data, repoName, issueNumber, statusOptions, optionId) {
846
+ const statusName = statusOptions.find((o) => o.id === optionId)?.name;
847
+ if (!statusName) return data;
848
+ return {
849
+ ...data,
850
+ repos: data.repos.map((rd) => {
851
+ if (rd.repo.name !== repoName) return rd;
852
+ return {
853
+ ...rd,
854
+ issues: rd.issues.map(
855
+ (issue) => issue.number === issueNumber ? { ...issue, projectStatus: statusName } : issue
856
+ )
857
+ };
858
+ })
859
+ };
860
+ }
861
+ function useActions({
862
+ config: config2,
863
+ repos,
864
+ selectedId,
865
+ toast,
866
+ refresh,
867
+ mutateData,
868
+ onOverlayDone
869
+ }) {
870
+ const configRef = useRef(config2);
871
+ const reposRef = useRef(repos);
872
+ const selectedIdRef = useRef(selectedId);
873
+ configRef.current = config2;
874
+ reposRef.current = repos;
875
+ selectedIdRef.current = selectedId;
876
+ const handlePick = useCallback(() => {
877
+ const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
878
+ if (!(ctx.issue && ctx.repoConfig)) return;
879
+ const { issue, repoConfig } = ctx;
880
+ const assignees = issue.assignees ?? [];
881
+ if (assignees.some((a) => a.login === configRef.current.board.assignee)) {
882
+ toast.info(`Already assigned to @${configRef.current.board.assignee}`);
883
+ return;
884
+ }
885
+ const firstAssignee = assignees[0];
886
+ if (firstAssignee) {
887
+ toast.info(`Already assigned to @${firstAssignee.login}`);
888
+ return;
889
+ }
890
+ const t = toast.loading(`Picking ${repoConfig.shortName}#${issue.number}...`);
891
+ pickIssue(configRef.current, { repo: repoConfig, issueNumber: issue.number }).then((result) => {
892
+ const msg = `Picked ${repoConfig.shortName}#${issue.number} \u2014 assigned + synced to TickTick`;
893
+ t.resolve(result.warning ? `${msg} (${result.warning})` : msg);
894
+ refresh();
895
+ }).catch((err) => {
896
+ t.reject(`Pick failed: ${err instanceof Error ? err.message : String(err)}`);
897
+ });
898
+ }, [toast, refresh]);
899
+ const handleComment = useCallback(
900
+ (body) => {
901
+ const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
902
+ if (!(ctx.issue && ctx.repoName)) {
903
+ onOverlayDone();
904
+ return;
905
+ }
906
+ const { issue, repoName } = ctx;
907
+ const t = toast.loading("Commenting...");
908
+ execFileAsync2(
909
+ "gh",
910
+ ["issue", "comment", String(issue.number), "--repo", repoName, "--body", body],
911
+ { encoding: "utf-8", timeout: 3e4 }
912
+ ).then(() => {
913
+ t.resolve(`Comment posted on #${issue.number}`);
914
+ refresh();
915
+ }).catch((err) => {
916
+ t.reject(`Comment failed: ${err instanceof Error ? err.message : String(err)}`);
917
+ }).finally(() => {
918
+ onOverlayDone();
919
+ });
920
+ },
921
+ [toast, refresh, onOverlayDone]
922
+ );
923
+ const handleStatusChange = useCallback(
924
+ (optionId) => {
925
+ const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
926
+ if (!(ctx.issue && ctx.repoName && ctx.repoConfig)) {
927
+ onOverlayDone();
928
+ return;
929
+ }
930
+ const { issue, repoName, repoConfig, statusOptions } = ctx;
931
+ mutateData(
932
+ (data) => optimisticSetStatus(data, repoName, issue.number, statusOptions, optionId)
933
+ );
934
+ const t = toast.loading("Moving...");
935
+ const projectConfig = {
936
+ projectNumber: repoConfig.projectNumber,
937
+ statusFieldId: repoConfig.statusFieldId,
938
+ optionId
939
+ };
940
+ updateProjectItemStatusAsync(repoName, issue.number, projectConfig).then(async () => {
941
+ const optionName = statusOptions.find((o) => o.id === optionId)?.name ?? optionId;
942
+ if (TERMINAL_STATUS_RE.test(optionName) && repoConfig.completionAction) {
943
+ try {
944
+ await triggerCompletionActionAsync(
945
+ repoConfig.completionAction,
946
+ repoName,
947
+ issue.number
948
+ );
949
+ t.resolve(
950
+ `#${issue.number} \u2192 ${optionName} (${repoConfig.completionAction.type})`
951
+ );
952
+ } catch {
953
+ toast.info(`#${issue.number} \u2192 ${optionName} (completion action failed)`);
954
+ }
955
+ } else {
956
+ t.resolve(`#${issue.number} \u2192 ${optionName}`);
957
+ }
958
+ refresh();
959
+ }).catch((err) => {
960
+ t.reject(`Status change failed: ${err instanceof Error ? err.message : String(err)}`);
961
+ refresh();
962
+ }).finally(() => {
963
+ onOverlayDone();
964
+ });
965
+ },
966
+ [toast, refresh, mutateData, onOverlayDone]
967
+ );
968
+ const handleAssign = useCallback(() => {
969
+ const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
970
+ if (!(ctx.issue && ctx.repoName)) return;
971
+ const { issue, repoName } = ctx;
972
+ const assignees = issue.assignees ?? [];
973
+ if (assignees.some((a) => a.login === configRef.current.board.assignee)) {
974
+ toast.info(`Already assigned to @${configRef.current.board.assignee}`);
975
+ return;
976
+ }
977
+ const firstAssignee = assignees[0];
978
+ if (firstAssignee) {
979
+ toast.info(`Already assigned to @${firstAssignee.login}`);
980
+ return;
981
+ }
982
+ const t = toast.loading("Assigning...");
983
+ assignIssueAsync(repoName, issue.number).then(() => {
984
+ t.resolve(`Assigned #${issue.number} to @${configRef.current.board.assignee}`);
985
+ refresh();
986
+ }).catch((err) => {
987
+ t.reject(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
988
+ });
989
+ }, [toast, refresh]);
990
+ const handleUnassign = useCallback(() => {
991
+ const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
992
+ if (!(ctx.issue && ctx.repoName)) return;
993
+ const { issue, repoName } = ctx;
994
+ const assignees = issue.assignees ?? [];
995
+ const selfAssigned = assignees.some((a) => a.login === configRef.current.board.assignee);
996
+ if (!selfAssigned) {
997
+ const firstAssignee = assignees[0];
998
+ if (firstAssignee) {
999
+ toast.info(`Assigned to @${firstAssignee.login} \u2014 can only unassign self`);
1000
+ } else {
1001
+ toast.info("Not assigned");
1002
+ }
1003
+ return;
1004
+ }
1005
+ const t = toast.loading("Unassigning...");
1006
+ execFileAsync2(
1007
+ "gh",
1008
+ ["issue", "edit", String(issue.number), "--repo", repoName, "--remove-assignee", "@me"],
1009
+ { encoding: "utf-8", timeout: 3e4 }
1010
+ ).then(() => {
1011
+ t.resolve(`Unassigned #${issue.number} from @${configRef.current.board.assignee}`);
1012
+ refresh();
1013
+ }).catch((err) => {
1014
+ t.reject(`Unassign failed: ${err instanceof Error ? err.message : String(err)}`);
1015
+ });
1016
+ }, [toast, refresh]);
1017
+ const handleCreateIssue = useCallback(
1018
+ async (repo, title, labels) => {
1019
+ const args = ["issue", "create", "--repo", repo, "--title", title];
1020
+ if (labels && labels.length > 0) {
1021
+ for (const label of labels) {
1022
+ args.push("--label", label);
1023
+ }
1024
+ }
1025
+ const t = toast.loading("Creating...");
1026
+ try {
1027
+ const { stdout } = await execFileAsync2("gh", args, { encoding: "utf-8", timeout: 3e4 });
1028
+ const output = stdout.trim();
1029
+ const match = output.match(/\/(\d+)$/);
1030
+ const issueNumber = match?.[1] ? parseInt(match[1], 10) : 0;
1031
+ const shortName = configRef.current.repos.find((r) => r.name === repo)?.shortName ?? repo;
1032
+ t.resolve(`Created ${shortName}#${issueNumber}`);
1033
+ refresh();
1034
+ onOverlayDone();
1035
+ return issueNumber > 0 ? { repo, issueNumber } : null;
1036
+ } catch (err) {
1037
+ t.reject(`Create failed: ${err instanceof Error ? err.message : String(err)}`);
1038
+ onOverlayDone();
1039
+ return null;
1040
+ }
1041
+ },
1042
+ [toast, refresh, onOverlayDone]
1043
+ );
1044
+ const handleBulkAssign = useCallback(
1045
+ async (ids) => {
1046
+ const failed = [];
1047
+ const t = toast.loading(`Assigning ${ids.size} issue${ids.size > 1 ? "s" : ""}...`);
1048
+ for (const id of ids) {
1049
+ const ctx = findIssueContext(reposRef.current, id, configRef.current);
1050
+ if (!(ctx.issue && ctx.repoName)) {
1051
+ failed.push(id);
1052
+ continue;
1053
+ }
1054
+ const assignees = ctx.issue.assignees ?? [];
1055
+ if (assignees.some((a) => a.login === configRef.current.board.assignee)) continue;
1056
+ try {
1057
+ await assignIssueAsync(ctx.repoName, ctx.issue.number);
1058
+ } catch {
1059
+ failed.push(id);
1060
+ }
1061
+ }
1062
+ const total = ids.size;
1063
+ const ok = total - failed.length;
1064
+ if (failed.length === 0) {
1065
+ t.resolve(
1066
+ `Assigned ${total} issue${total > 1 ? "s" : ""} to @${configRef.current.board.assignee}`
1067
+ );
1068
+ } else {
1069
+ t.reject(`${ok} assigned, ${failed.length} failed`);
1070
+ }
1071
+ refresh();
1072
+ return failed;
1073
+ },
1074
+ [toast, refresh]
1075
+ );
1076
+ const handleBulkUnassign = useCallback(
1077
+ async (ids) => {
1078
+ const failed = [];
1079
+ const t = toast.loading(`Unassigning ${ids.size} issue${ids.size > 1 ? "s" : ""}...`);
1080
+ for (const id of ids) {
1081
+ const ctx = findIssueContext(reposRef.current, id, configRef.current);
1082
+ if (!(ctx.issue && ctx.repoName)) {
1083
+ failed.push(id);
1084
+ continue;
1085
+ }
1086
+ const assignees = ctx.issue.assignees ?? [];
1087
+ if (!assignees.some((a) => a.login === configRef.current.board.assignee)) continue;
1088
+ try {
1089
+ await execFileAsync2(
1090
+ "gh",
1091
+ [
1092
+ "issue",
1093
+ "edit",
1094
+ String(ctx.issue.number),
1095
+ "--repo",
1096
+ ctx.repoName,
1097
+ "--remove-assignee",
1098
+ "@me"
1099
+ ],
1100
+ { encoding: "utf-8", timeout: 3e4 }
1101
+ );
1102
+ } catch {
1103
+ failed.push(id);
1104
+ }
1105
+ }
1106
+ const total = ids.size;
1107
+ const ok = total - failed.length;
1108
+ if (failed.length === 0) {
1109
+ t.resolve(`Unassigned ${total} issue${total > 1 ? "s" : ""}`);
1110
+ } else {
1111
+ t.reject(`${ok} unassigned, ${failed.length} failed`);
1112
+ }
1113
+ refresh();
1114
+ return failed;
1115
+ },
1116
+ [toast, refresh]
1117
+ );
1118
+ const handleBulkStatusChange = useCallback(
1119
+ async (ids, optionId) => {
1120
+ for (const id of ids) {
1121
+ const ctx = findIssueContext(reposRef.current, id, configRef.current);
1122
+ if (ctx.issue && ctx.repoName) {
1123
+ const { issue: ctxIssue, repoName: ctxRepo, statusOptions: ctxOpts } = ctx;
1124
+ mutateData(
1125
+ (data) => optimisticSetStatus(data, ctxRepo, ctxIssue.number, ctxOpts, optionId)
1126
+ );
1127
+ }
1128
+ }
1129
+ const t = toast.loading(`Moving ${ids.size} issue${ids.size > 1 ? "s" : ""}...`);
1130
+ const failed = [];
1131
+ for (const id of ids) {
1132
+ const ctx = findIssueContext(reposRef.current, id, configRef.current);
1133
+ if (!(ctx.issue && ctx.repoName && ctx.repoConfig)) {
1134
+ failed.push(id);
1135
+ continue;
1136
+ }
1137
+ try {
1138
+ const projectConfig = {
1139
+ projectNumber: ctx.repoConfig.projectNumber,
1140
+ statusFieldId: ctx.repoConfig.statusFieldId,
1141
+ optionId
1142
+ };
1143
+ await updateProjectItemStatusAsync(ctx.repoName, ctx.issue.number, projectConfig);
1144
+ } catch {
1145
+ failed.push(id);
1146
+ }
1147
+ }
1148
+ const total = ids.size;
1149
+ const ok = total - failed.length;
1150
+ const optionName = (() => {
1151
+ for (const id of ids) {
1152
+ const ctx = findIssueContext(reposRef.current, id, configRef.current);
1153
+ const name = ctx.statusOptions.find((o) => o.id === optionId)?.name;
1154
+ if (name) return name;
1155
+ }
1156
+ return optionId;
1157
+ })();
1158
+ if (failed.length === 0) {
1159
+ t.resolve(`Moved ${total} issue${total > 1 ? "s" : ""} to ${optionName}`);
1160
+ } else {
1161
+ t.reject(`${ok} moved to ${optionName}, ${failed.length} failed`);
1162
+ }
1163
+ refresh();
1164
+ return failed;
1165
+ },
1166
+ [toast, refresh, mutateData]
1167
+ );
1168
+ return {
1169
+ handlePick,
1170
+ handleComment,
1171
+ handleStatusChange,
1172
+ handleAssign,
1173
+ handleUnassign,
1174
+ handleCreateIssue,
1175
+ handleBulkAssign,
1176
+ handleBulkUnassign,
1177
+ handleBulkStatusChange
1178
+ };
1179
+ }
1180
+ var execFileAsync2, TERMINAL_STATUS_RE;
1181
+ var init_use_actions = __esm({
1182
+ "src/board/hooks/use-actions.ts"() {
1183
+ "use strict";
1184
+ init_github();
1185
+ init_pick();
1186
+ execFileAsync2 = promisify2(execFile2);
1187
+ TERMINAL_STATUS_RE = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
1188
+ }
1189
+ });
1190
+
1191
+ // src/board/hooks/use-data.ts
1192
+ import { Worker } from "worker_threads";
1193
+ import { useCallback as useCallback2, useEffect, useRef as useRef2, useState } from "react";
1194
+ function refreshAgeColor(lastRefresh) {
1195
+ if (!lastRefresh) return "gray";
1196
+ const age = Date.now() - lastRefresh.getTime();
1197
+ if (age < STALE_THRESHOLDS.FRESH) return "green";
1198
+ if (age < STALE_THRESHOLDS.AGING) return "yellow";
1199
+ return "red";
1200
+ }
1201
+ function useData(config2, options, refreshIntervalMs) {
1202
+ const [state, setState] = useState(INITIAL_STATE);
1203
+ const activeRequestRef = useRef2(null);
1204
+ const workerRef = useRef2(null);
1205
+ const intervalRef = useRef2(null);
1206
+ const configRef = useRef2(config2);
1207
+ const optionsRef = useRef2(options);
1208
+ configRef.current = config2;
1209
+ optionsRef.current = options;
1210
+ const refresh = useCallback2(() => {
1211
+ if (activeRequestRef.current) {
1212
+ activeRequestRef.current.canceled = true;
1213
+ }
1214
+ workerRef.current?.terminate();
1215
+ const token = { canceled: false };
1216
+ activeRequestRef.current = token;
1217
+ setState((prev) => ({ ...prev, isRefreshing: true }));
1218
+ const worker = new Worker(
1219
+ new URL(
1220
+ import.meta.url.endsWith(".ts") ? "../fetch-worker.ts" : "./fetch-worker.js",
1221
+ // prod: tsup bundle in dist/
1222
+ import.meta.url
1223
+ ),
1224
+ { workerData: { config: configRef.current, options: optionsRef.current } }
1225
+ );
1226
+ workerRef.current = worker;
1227
+ worker.on("message", (msg) => {
1228
+ if (token.canceled) {
1229
+ worker.terminate();
1230
+ return;
1231
+ }
1232
+ if (msg.type === "success" && msg.data) {
1233
+ const data = msg.data;
1234
+ data.fetchedAt = new Date(data.fetchedAt);
1235
+ for (const ev of data.activity) {
1236
+ ev.timestamp = new Date(ev.timestamp);
1237
+ }
1238
+ setState({
1239
+ status: "success",
1240
+ data,
1241
+ error: null,
1242
+ lastRefresh: /* @__PURE__ */ new Date(),
1243
+ isRefreshing: false,
1244
+ consecutiveFailures: 0,
1245
+ autoRefreshPaused: false
1246
+ });
1247
+ } else {
1248
+ setState((prev) => {
1249
+ const failures = prev.consecutiveFailures + 1;
1250
+ return {
1251
+ ...prev,
1252
+ status: prev.data ? "success" : "error",
1253
+ error: msg.error ?? "Unknown error",
1254
+ isRefreshing: false,
1255
+ consecutiveFailures: failures,
1256
+ autoRefreshPaused: failures >= MAX_REFRESH_FAILURES
1257
+ };
1258
+ });
1259
+ }
1260
+ worker.terminate();
1261
+ });
1262
+ worker.on("error", (err) => {
1263
+ if (token.canceled) return;
1264
+ setState((prev) => {
1265
+ const failures = prev.consecutiveFailures + 1;
1266
+ return {
1267
+ ...prev,
1268
+ status: prev.data ? "success" : "error",
1269
+ error: err.message,
1270
+ isRefreshing: false,
1271
+ consecutiveFailures: failures,
1272
+ autoRefreshPaused: failures >= MAX_REFRESH_FAILURES
1273
+ };
1274
+ });
1275
+ });
1276
+ }, []);
1277
+ useEffect(() => {
1278
+ refresh();
1279
+ }, [refresh]);
1280
+ const stateRef = useRef2(state);
1281
+ stateRef.current = state;
1282
+ useEffect(() => {
1283
+ if (refreshIntervalMs <= 0) return;
1284
+ intervalRef.current = setInterval(() => {
1285
+ if (!stateRef.current.autoRefreshPaused) {
1286
+ refresh();
1287
+ }
1288
+ }, refreshIntervalMs);
1289
+ return () => {
1290
+ if (intervalRef.current) {
1291
+ clearInterval(intervalRef.current);
1292
+ }
1293
+ };
1294
+ }, [refresh, refreshIntervalMs]);
1295
+ useEffect(() => {
1296
+ return () => {
1297
+ if (activeRequestRef.current) {
1298
+ activeRequestRef.current.canceled = true;
1299
+ }
1300
+ workerRef.current?.terminate();
1301
+ };
1302
+ }, []);
1303
+ const mutateData = useCallback2((fn) => {
1304
+ setState((prev) => {
1305
+ if (!prev.data) return prev;
1306
+ return { ...prev, data: fn(prev.data) };
1307
+ });
1308
+ }, []);
1309
+ return { ...state, refresh, mutateData };
1310
+ }
1311
+ var INITIAL_STATE, STALE_THRESHOLDS, MAX_REFRESH_FAILURES;
1312
+ var init_use_data = __esm({
1313
+ "src/board/hooks/use-data.ts"() {
1314
+ "use strict";
1315
+ INITIAL_STATE = {
1316
+ status: "loading",
1317
+ data: null,
1318
+ error: null,
1319
+ lastRefresh: null,
1320
+ isRefreshing: false,
1321
+ consecutiveFailures: 0,
1322
+ autoRefreshPaused: false
1323
+ };
1324
+ STALE_THRESHOLDS = {
1325
+ FRESH: 6e4,
1326
+ // 0-60s → green
1327
+ AGING: 3e5
1328
+ // 60s-5m → yellow
1329
+ // 5m+ → red
1330
+ };
1331
+ MAX_REFRESH_FAILURES = 3;
1332
+ }
1333
+ });
1334
+
1335
+ // src/board/hooks/use-multi-select.ts
1336
+ import { useCallback as useCallback3, useRef as useRef3, useState as useState2 } from "react";
1337
+ function useMultiSelect(getRepoForId) {
1338
+ const [selected, setSelected] = useState2(/* @__PURE__ */ new Set());
1339
+ const repoRef = useRef3(null);
1340
+ const getRepoRef = useRef3(getRepoForId);
1341
+ getRepoRef.current = getRepoForId;
1342
+ const toggle = useCallback3((id) => {
1343
+ setSelected((prev) => {
1344
+ const repo = getRepoRef.current(id);
1345
+ if (!repo) return prev;
1346
+ const next = new Set(prev);
1347
+ if (next.has(id)) {
1348
+ next.delete(id);
1349
+ if (next.size === 0) repoRef.current = null;
1350
+ } else {
1351
+ if (repoRef.current && repoRef.current !== repo) {
1352
+ next.clear();
1353
+ }
1354
+ repoRef.current = repo;
1355
+ next.add(id);
1356
+ }
1357
+ return next;
1358
+ });
1359
+ }, []);
1360
+ const clear = useCallback3(() => {
1361
+ setSelected(/* @__PURE__ */ new Set());
1362
+ repoRef.current = null;
1363
+ }, []);
1364
+ const prune = useCallback3((validIds) => {
1365
+ setSelected((prev) => {
1366
+ const next = /* @__PURE__ */ new Set();
1367
+ for (const id of prev) {
1368
+ if (validIds.has(id)) next.add(id);
1369
+ }
1370
+ if (next.size === prev.size) return prev;
1371
+ if (next.size === 0) repoRef.current = null;
1372
+ return next;
1373
+ });
1374
+ }, []);
1375
+ const isSelected = useCallback3((id) => selected.has(id), [selected]);
1376
+ return {
1377
+ selected,
1378
+ count: selected.size,
1379
+ isSelected,
1380
+ toggle,
1381
+ clear,
1382
+ prune,
1383
+ constrainedRepo: repoRef.current
1384
+ };
1385
+ }
1386
+ var init_use_multi_select = __esm({
1387
+ "src/board/hooks/use-multi-select.ts"() {
1388
+ "use strict";
1389
+ }
1390
+ });
1391
+
1392
+ // src/board/hooks/use-navigation.ts
1393
+ import { useCallback as useCallback4, useMemo, useReducer, useRef as useRef4 } from "react";
1394
+ function arraysEqual(a, b) {
1395
+ if (a.length !== b.length) return false;
1396
+ for (let i = 0; i < a.length; i++) {
1397
+ if (a[i] !== b[i]) return false;
1398
+ }
1399
+ return true;
1400
+ }
1401
+ function findFallback(items, oldSection) {
1402
+ if (oldSection) {
1403
+ const sectionItem = items.find((i) => i.section === oldSection && i.type === "item");
1404
+ if (sectionItem) return sectionItem;
1405
+ const sectionHeader = items.find((i) => i.section === oldSection && i.type === "header");
1406
+ if (sectionHeader) return sectionHeader;
1407
+ }
1408
+ return items.find((i) => i.type === "header") ?? items[0];
1409
+ }
1410
+ function navReducer(state, action) {
1411
+ switch (action.type) {
1412
+ case "SET_ITEMS": {
1413
+ const sections = [...new Set(action.items.map((i) => i.section))];
1414
+ const isFirstLoad = state.sections.length === 0;
1415
+ const collapsedSections = isFirstLoad ? new Set(sections) : state.collapsedSections;
1416
+ const selectionValid = state.selectedId != null && action.items.some((i) => i.id === state.selectedId);
1417
+ if (!isFirstLoad && selectionValid && arraysEqual(sections, state.sections)) {
1418
+ return state;
1419
+ }
1420
+ if (selectionValid) {
1421
+ const selected = action.items.find((i) => i.id === state.selectedId);
1422
+ return {
1423
+ ...state,
1424
+ selectedSection: selected?.section ?? state.selectedSection,
1425
+ sections,
1426
+ collapsedSections
1427
+ };
1428
+ }
1429
+ const fallback = findFallback(action.items, state.selectedSection);
1430
+ return {
1431
+ selectedId: fallback?.id ?? null,
1432
+ selectedSection: fallback?.section ?? null,
1433
+ sections,
1434
+ collapsedSections
1435
+ };
1436
+ }
1437
+ case "SELECT": {
1438
+ return {
1439
+ ...state,
1440
+ selectedId: action.id,
1441
+ selectedSection: action.section ?? state.selectedSection
1442
+ };
1443
+ }
1444
+ case "TOGGLE_SECTION": {
1445
+ const next = new Set(state.collapsedSections);
1446
+ if (next.has(action.section)) {
1447
+ next.delete(action.section);
1448
+ } else {
1449
+ next.add(action.section);
1450
+ }
1451
+ return { ...state, collapsedSections: next };
1452
+ }
1453
+ default:
1454
+ return state;
1455
+ }
1456
+ }
1457
+ function getVisibleItems(allItems, collapsedSections) {
1458
+ return allItems.filter((item) => {
1459
+ if (item.type === "header") return true;
1460
+ if (collapsedSections.has(item.section)) return false;
1461
+ if (item.type === "subHeader") return true;
1462
+ if (item.subSection && collapsedSections.has(item.subSection)) return false;
1463
+ return true;
1464
+ });
1465
+ }
1466
+ function useNavigation(allItems) {
1467
+ const [state, dispatch] = useReducer(navReducer, {
1468
+ selectedId: null,
1469
+ selectedSection: null,
1470
+ sections: [],
1471
+ collapsedSections: /* @__PURE__ */ new Set()
1472
+ });
1473
+ const prevItemsRef = useRef4(null);
1474
+ if (allItems !== prevItemsRef.current) {
1475
+ prevItemsRef.current = allItems;
1476
+ dispatch({ type: "SET_ITEMS", items: allItems });
1477
+ }
1478
+ const visibleItems = useMemo(
1479
+ () => getVisibleItems(allItems, state.collapsedSections),
1480
+ [allItems, state.collapsedSections]
1481
+ );
1482
+ const selectedIndex = useMemo(() => {
1483
+ if (!state.selectedId) return 0;
1484
+ const idx = visibleItems.findIndex((i) => i.id === state.selectedId);
1485
+ return idx >= 0 ? idx : 0;
1486
+ }, [state.selectedId, visibleItems]);
1487
+ const moveUp = useCallback4(() => {
1488
+ const newIdx = Math.max(0, selectedIndex - 1);
1489
+ const item = visibleItems[newIdx];
1490
+ if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1491
+ }, [selectedIndex, visibleItems]);
1492
+ const moveDown = useCallback4(() => {
1493
+ const newIdx = Math.min(visibleItems.length - 1, selectedIndex + 1);
1494
+ const item = visibleItems[newIdx];
1495
+ if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1496
+ }, [selectedIndex, visibleItems]);
1497
+ const nextSection = useCallback4(() => {
1498
+ const currentItem = visibleItems[selectedIndex];
1499
+ if (!currentItem) return;
1500
+ const currentSectionIdx = state.sections.indexOf(currentItem.section);
1501
+ const nextSectionId = state.sections[currentSectionIdx + 1];
1502
+ if (!nextSectionId) return;
1503
+ const header = visibleItems.find((i) => i.section === nextSectionId && i.type === "header");
1504
+ if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1505
+ }, [selectedIndex, visibleItems, state.sections]);
1506
+ const prevSection = useCallback4(() => {
1507
+ const currentItem = visibleItems[selectedIndex];
1508
+ if (!currentItem) return;
1509
+ const currentSectionIdx = state.sections.indexOf(currentItem.section);
1510
+ const prevSectionId = state.sections[currentSectionIdx - 1];
1511
+ if (!prevSectionId) return;
1512
+ const header = visibleItems.find((i) => i.section === prevSectionId && i.type === "header");
1513
+ if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1514
+ }, [selectedIndex, visibleItems, state.sections]);
1515
+ const toggleSection = useCallback4(() => {
1516
+ const currentItem = visibleItems[selectedIndex];
1517
+ if (!currentItem) return;
1518
+ const key = currentItem.type === "subHeader" ? currentItem.id : currentItem.section;
1519
+ dispatch({ type: "TOGGLE_SECTION", section: key });
1520
+ }, [selectedIndex, visibleItems]);
1521
+ const allItemsRef = useRef4(allItems);
1522
+ allItemsRef.current = allItems;
1523
+ const select2 = useCallback4((id) => {
1524
+ const item = allItemsRef.current.find((i) => i.id === id);
1525
+ dispatch({ type: "SELECT", id, section: item?.section });
1526
+ }, []);
1527
+ const isCollapsed = useCallback4(
1528
+ (section) => state.collapsedSections.has(section),
1529
+ [state.collapsedSections]
1530
+ );
1531
+ return {
1532
+ selectedId: state.selectedId,
1533
+ selectedIndex,
1534
+ collapsedSections: state.collapsedSections,
1535
+ moveUp,
1536
+ moveDown,
1537
+ nextSection,
1538
+ prevSection,
1539
+ toggleSection,
1540
+ select: select2,
1541
+ isCollapsed
1542
+ };
1543
+ }
1544
+ var init_use_navigation = __esm({
1545
+ "src/board/hooks/use-navigation.ts"() {
1546
+ "use strict";
1547
+ }
1548
+ });
1549
+
1550
+ // src/board/hooks/use-toast.ts
1551
+ import { useCallback as useCallback5, useRef as useRef5, useState as useState3 } from "react";
1552
+ function useToast() {
1553
+ const [toasts, setToasts] = useState3([]);
1554
+ const timersRef = useRef5(/* @__PURE__ */ new Map());
1555
+ const clearTimer = useCallback5((id) => {
1556
+ const timer = timersRef.current.get(id);
1557
+ if (timer) {
1558
+ clearTimeout(timer);
1559
+ timersRef.current.delete(id);
1560
+ }
1561
+ }, []);
1562
+ const removeToast = useCallback5(
1563
+ (id) => {
1564
+ clearTimer(id);
1565
+ setToasts((prev) => prev.filter((t) => t.id !== id));
1566
+ },
1567
+ [clearTimer]
1568
+ );
1569
+ const addToast = useCallback5(
1570
+ (t) => {
1571
+ const id = `toast-${++nextId}`;
1572
+ const newToast = { ...t, id, createdAt: Date.now() };
1573
+ setToasts((prev) => {
1574
+ const next = [...prev, newToast];
1575
+ while (next.length > MAX_VISIBLE) {
1576
+ const evictIdx = next.findIndex((x) => x.type !== "error" && x.type !== "loading");
1577
+ if (evictIdx >= 0) {
1578
+ const evictToast = next[evictIdx];
1579
+ if (evictToast) clearTimer(evictToast.id);
1580
+ next.splice(evictIdx, 1);
1581
+ } else {
1582
+ const oldest = next[0];
1583
+ if (oldest) clearTimer(oldest.id);
1584
+ next.shift();
1585
+ }
1586
+ }
1587
+ return next;
1588
+ });
1589
+ if (t.type === "info" || t.type === "success") {
1590
+ const timer = setTimeout(() => removeToast(id), AUTO_DISMISS_MS);
1591
+ timersRef.current.set(id, timer);
1592
+ }
1593
+ return id;
1594
+ },
1595
+ [removeToast, clearTimer]
1596
+ );
1597
+ const toast = {
1598
+ info: useCallback5(
1599
+ (message) => {
1600
+ addToast({ type: "info", message });
1601
+ },
1602
+ [addToast]
1603
+ ),
1604
+ success: useCallback5(
1605
+ (message) => {
1606
+ addToast({ type: "success", message });
1607
+ },
1608
+ [addToast]
1609
+ ),
1610
+ error: useCallback5(
1611
+ (message, retry) => {
1612
+ addToast(retry ? { type: "error", message, retry } : { type: "error", message });
1613
+ },
1614
+ [addToast]
1615
+ ),
1616
+ loading: useCallback5(
1617
+ (message) => {
1618
+ const id = addToast({ type: "loading", message });
1619
+ return {
1620
+ resolve: (msg) => {
1621
+ removeToast(id);
1622
+ addToast({ type: "success", message: msg });
1623
+ },
1624
+ reject: (msg) => {
1625
+ removeToast(id);
1626
+ addToast({ type: "error", message: msg });
1627
+ }
1628
+ };
1629
+ },
1630
+ [addToast, removeToast]
1631
+ )
1632
+ };
1633
+ const dismiss = useCallback5(
1634
+ (id) => {
1635
+ removeToast(id);
1636
+ },
1637
+ [removeToast]
1638
+ );
1639
+ const dismissAll = useCallback5(() => {
1640
+ for (const timer of timersRef.current.values()) {
1641
+ clearTimeout(timer);
1642
+ }
1643
+ timersRef.current.clear();
1644
+ setToasts([]);
1645
+ }, []);
1646
+ const handleErrorAction = useCallback5(
1647
+ (action) => {
1648
+ const errorToast = toasts.find((t) => t.type === "error");
1649
+ if (!errorToast) return false;
1650
+ if (action === "retry" && errorToast.retry) {
1651
+ removeToast(errorToast.id);
1652
+ errorToast.retry();
1653
+ return true;
1654
+ }
1655
+ if (action === "dismiss") {
1656
+ removeToast(errorToast.id);
1657
+ return true;
1658
+ }
1659
+ return false;
1660
+ },
1661
+ [toasts, removeToast]
1662
+ );
1663
+ return { toasts, toast, dismiss, dismissAll, handleErrorAction };
1664
+ }
1665
+ var MAX_VISIBLE, AUTO_DISMISS_MS, nextId;
1666
+ var init_use_toast = __esm({
1667
+ "src/board/hooks/use-toast.ts"() {
1668
+ "use strict";
1669
+ MAX_VISIBLE = 3;
1670
+ AUTO_DISMISS_MS = 3e3;
1671
+ nextId = 0;
1672
+ }
1673
+ });
1674
+
1675
+ // src/board/hooks/use-ui-state.ts
1676
+ import { useCallback as useCallback6, useReducer as useReducer2 } from "react";
1677
+ function uiReducer(state, action) {
1678
+ switch (action.type) {
1679
+ case "ENTER_SEARCH":
1680
+ if (state.mode !== "normal") return state;
1681
+ return { ...state, mode: "search", previousMode: "normal" };
1682
+ case "ENTER_COMMENT":
1683
+ if (state.mode !== "normal") return state;
1684
+ return { ...state, mode: "overlay:comment", previousMode: "normal" };
1685
+ case "ENTER_STATUS":
1686
+ if (state.mode !== "normal" && state.mode !== "overlay:bulkAction") return state;
1687
+ return {
1688
+ ...state,
1689
+ mode: "overlay:status",
1690
+ previousMode: state.mode === "overlay:bulkAction" ? "multiSelect" : "normal"
1691
+ };
1692
+ case "ENTER_CREATE":
1693
+ if (state.mode !== "normal") return state;
1694
+ return { ...state, mode: "overlay:create", previousMode: "normal" };
1695
+ case "ENTER_MULTI_SELECT":
1696
+ if (state.mode !== "normal" && state.mode !== "multiSelect") return state;
1697
+ return { ...state, mode: "multiSelect", previousMode: "normal" };
1698
+ case "ENTER_BULK_ACTION":
1699
+ if (state.mode !== "multiSelect") return state;
1700
+ return { ...state, mode: "overlay:bulkAction", previousMode: "multiSelect" };
1701
+ case "ENTER_CONFIRM_PICK":
1702
+ return { ...state, mode: "overlay:confirmPick", previousMode: "normal" };
1703
+ case "ENTER_FOCUS":
1704
+ if (state.mode !== "normal") return state;
1705
+ return { ...state, mode: "focus", previousMode: "normal" };
1706
+ case "TOGGLE_HELP":
1707
+ return { ...state, helpVisible: !state.helpVisible };
1708
+ case "EXIT_OVERLAY":
1709
+ if (state.helpVisible) {
1710
+ return { ...state, helpVisible: false };
1711
+ }
1712
+ return { ...state, mode: state.previousMode, previousMode: "normal" };
1713
+ case "EXIT_TO_NORMAL":
1714
+ return { ...state, mode: "normal", helpVisible: false, previousMode: "normal" };
1715
+ case "CLEAR_MULTI_SELECT":
1716
+ if (state.mode === "multiSelect") {
1717
+ return { ...state, mode: "normal", previousMode: "normal" };
1718
+ }
1719
+ return state;
1720
+ default:
1721
+ return state;
1722
+ }
1723
+ }
1724
+ function canNavigate(state) {
1725
+ const { mode } = state;
1726
+ return mode === "normal" || mode === "multiSelect" || mode === "focus";
1727
+ }
1728
+ function canAct(state) {
1729
+ return state.mode === "normal";
1730
+ }
1731
+ function isOverlay(state) {
1732
+ return state.mode.startsWith("overlay:") || state.mode === "search";
1733
+ }
1734
+ function useUIState() {
1735
+ const [state, dispatch] = useReducer2(uiReducer, INITIAL_STATE2);
1736
+ return {
1737
+ state,
1738
+ enterSearch: useCallback6(() => dispatch({ type: "ENTER_SEARCH" }), []),
1739
+ enterComment: useCallback6(() => dispatch({ type: "ENTER_COMMENT" }), []),
1740
+ enterStatus: useCallback6(() => dispatch({ type: "ENTER_STATUS" }), []),
1741
+ enterCreate: useCallback6(() => dispatch({ type: "ENTER_CREATE" }), []),
1742
+ enterMultiSelect: useCallback6(() => dispatch({ type: "ENTER_MULTI_SELECT" }), []),
1743
+ enterBulkAction: useCallback6(() => dispatch({ type: "ENTER_BULK_ACTION" }), []),
1744
+ enterConfirmPick: useCallback6(() => dispatch({ type: "ENTER_CONFIRM_PICK" }), []),
1745
+ enterFocus: useCallback6(() => dispatch({ type: "ENTER_FOCUS" }), []),
1746
+ toggleHelp: useCallback6(() => dispatch({ type: "TOGGLE_HELP" }), []),
1747
+ exitOverlay: useCallback6(() => dispatch({ type: "EXIT_OVERLAY" }), []),
1748
+ exitToNormal: useCallback6(() => dispatch({ type: "EXIT_TO_NORMAL" }), []),
1749
+ clearMultiSelect: useCallback6(() => dispatch({ type: "CLEAR_MULTI_SELECT" }), []),
1750
+ canNavigate: canNavigate(state),
1751
+ canAct: canAct(state),
1752
+ isOverlay: isOverlay(state)
1753
+ };
1754
+ }
1755
+ var INITIAL_STATE2;
1756
+ var init_use_ui_state = __esm({
1757
+ "src/board/hooks/use-ui-state.ts"() {
1758
+ "use strict";
1759
+ INITIAL_STATE2 = {
1760
+ mode: "normal",
1761
+ helpVisible: false,
1762
+ previousMode: "normal"
1763
+ };
1764
+ }
1765
+ });
1766
+
1767
+ // src/board/components/bulk-action-menu.tsx
1768
+ import { Box, Text, useInput } from "ink";
1769
+ import { useState as useState4 } from "react";
1770
+ import { jsx, jsxs } from "react/jsx-runtime";
1771
+ function getMenuItems(selectionType) {
1772
+ if (selectionType === "github") {
1773
+ return [
1774
+ { label: "Assign all to me", action: { type: "assign" } },
1775
+ { label: "Unassign all from me", action: { type: "unassign" } },
1776
+ { label: "Move status (all)", action: { type: "statusChange" } }
1777
+ ];
1778
+ }
1779
+ if (selectionType === "ticktick") {
1780
+ return [
1781
+ { label: "Complete all", action: { type: "complete" } },
1782
+ { label: "Delete all", action: { type: "delete" } }
1783
+ ];
1784
+ }
1785
+ return [];
1786
+ }
1787
+ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1788
+ const items = getMenuItems(selectionType);
1789
+ const [selectedIdx, setSelectedIdx] = useState4(0);
1790
+ useInput((input2, key) => {
1791
+ if (key.escape) return onCancel();
1792
+ if (key.return) {
1793
+ const item = items[selectedIdx];
1794
+ if (item) onSelect(item.action);
1795
+ return;
1796
+ }
1797
+ if (input2 === "j" || key.downArrow) {
1798
+ setSelectedIdx((i) => Math.min(i + 1, items.length - 1));
1799
+ }
1800
+ if (input2 === "k" || key.upArrow) {
1801
+ setSelectedIdx((i) => Math.max(i - 1, 0));
1802
+ }
1803
+ });
1804
+ if (items.length === 0) {
1805
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1806
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "No bulk actions for mixed selection types." }),
1807
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Esc to cancel" })
1808
+ ] });
1809
+ }
1810
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1811
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1812
+ "Bulk action (",
1813
+ count,
1814
+ " selected):"
1815
+ ] }),
1816
+ items.map((item, i) => {
1817
+ const isSelected = i === selectedIdx;
1818
+ const prefix = isSelected ? "> " : " ";
1819
+ return /* @__PURE__ */ jsxs(Text, { ...isSelected ? { color: "cyan" } : {}, children: [
1820
+ prefix,
1821
+ item.label
1822
+ ] }, item.action.type);
1823
+ }),
1824
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
1825
+ ] });
1826
+ }
1827
+ var init_bulk_action_menu = __esm({
1828
+ "src/board/components/bulk-action-menu.tsx"() {
1829
+ "use strict";
1830
+ }
1831
+ });
1832
+
1833
+ // src/board/components/comment-input.tsx
1834
+ import { TextInput } from "@inkjs/ui";
1835
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
1836
+ import { useState as useState5 } from "react";
1837
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1838
+ function CommentInput({ issueNumber, onSubmit, onCancel }) {
1839
+ const [value, setValue] = useState5("");
1840
+ useInput2((_input, key) => {
1841
+ if (key.escape) onCancel();
1842
+ });
1843
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1844
+ /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
1845
+ "comment #",
1846
+ issueNumber,
1847
+ ": "
1848
+ ] }),
1849
+ /* @__PURE__ */ jsx2(
1850
+ TextInput,
1851
+ {
1852
+ defaultValue: value,
1853
+ placeholder: "type comment, Enter to post...",
1854
+ onChange: setValue,
1855
+ onSubmit: (text) => {
1856
+ if (text.trim()) onSubmit(text.trim());
1857
+ else onCancel();
1858
+ }
1859
+ }
1860
+ )
1861
+ ] });
1862
+ }
1863
+ var init_comment_input = __esm({
1864
+ "src/board/components/comment-input.tsx"() {
1865
+ "use strict";
1866
+ }
1867
+ });
1868
+
1869
+ // src/board/components/confirm-prompt.tsx
1870
+ import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
1871
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1872
+ function ConfirmPrompt({ message, onConfirm, onCancel }) {
1873
+ useInput3((input2, key) => {
1874
+ if (input2 === "y" || input2 === "Y") return onConfirm();
1875
+ if (input2 === "n" || input2 === "N" || key.escape) return onCancel();
1876
+ });
1877
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1878
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: message }),
1879
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " (y/n)" })
1880
+ ] });
1881
+ }
1882
+ var init_confirm_prompt = __esm({
1883
+ "src/board/components/confirm-prompt.tsx"() {
1884
+ "use strict";
1885
+ }
1886
+ });
1887
+
1888
+ // src/board/components/create-issue-form.tsx
1889
+ import { TextInput as TextInput2 } from "@inkjs/ui";
1890
+ import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
1891
+ import { useState as useState6 } from "react";
1892
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1893
+ function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
1894
+ const defaultRepoIdx = defaultRepo ? Math.max(
1895
+ 0,
1896
+ repos.findIndex((r) => r.name === defaultRepo)
1897
+ ) : 0;
1898
+ const [repoIdx, setRepoIdx] = useState6(defaultRepoIdx);
1899
+ const [title, setTitle] = useState6("");
1900
+ const [field, setField] = useState6("title");
1901
+ useInput4((input2, key) => {
1902
+ if (key.escape) return onCancel();
1903
+ if (field === "repo") {
1904
+ if (input2 === "j" || key.downArrow) {
1905
+ setRepoIdx((i) => Math.min(i + 1, repos.length - 1));
1906
+ }
1907
+ if (input2 === "k" || key.upArrow) {
1908
+ setRepoIdx((i) => Math.max(i - 1, 0));
1909
+ }
1910
+ if (key.tab) setField("title");
1911
+ if (key.return) setField("title");
1912
+ }
1913
+ });
1914
+ const selectedRepo = repos[repoIdx];
1915
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1916
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "Create Issue" }),
1917
+ /* @__PURE__ */ jsxs4(Box4, { children: [
1918
+ /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "repo", children: "Repo: " }),
1919
+ repos.map((r, i) => /* @__PURE__ */ jsx4(
1920
+ Text4,
1921
+ {
1922
+ ...i === repoIdx ? { color: "cyan", bold: true } : {},
1923
+ dimColor: field !== "repo",
1924
+ children: i === repoIdx ? `[${r.shortName}]` : ` ${r.shortName} `
1925
+ },
1926
+ r.name
1927
+ )),
1928
+ field === "repo" ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " j/k:select Tab:next" }) : null
1929
+ ] }),
1930
+ /* @__PURE__ */ jsxs4(Box4, { children: [
1931
+ /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "title", children: "Title: " }),
1932
+ field === "title" ? /* @__PURE__ */ jsx4(
1933
+ TextInput2,
1934
+ {
1935
+ defaultValue: title,
1936
+ placeholder: "issue title...",
1937
+ onChange: setTitle,
1938
+ onSubmit: (text) => {
1939
+ if (text.trim() && selectedRepo) {
1940
+ onSubmit(selectedRepo.name, text.trim());
1941
+ }
1942
+ }
1943
+ }
1944
+ ) : /* @__PURE__ */ jsx4(Text4, { children: title || "(empty)" })
1945
+ ] }),
1946
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Tab:switch fields Enter:submit Esc:cancel" })
1947
+ ] });
1948
+ }
1949
+ var init_create_issue_form = __esm({
1950
+ "src/board/components/create-issue-form.tsx"() {
1951
+ "use strict";
1952
+ }
1953
+ });
1954
+
1955
+ // src/board/components/detail-panel.tsx
1956
+ import { Box as Box5, Text as Text5 } from "ink";
1957
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1958
+ function truncateLines(text, maxLines) {
1959
+ const lines = text.split("\n").slice(0, maxLines);
1960
+ return lines.join("\n");
1961
+ }
1962
+ function stripMarkdown(text) {
1963
+ return text.replace(/^#{1,6}\s+/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`{1,3}[^`]*`{1,3}/g, (m) => m.replace(/`/g, "")).replace(/^\s*[-*+]\s+/gm, " - ").replace(/^\s*\d+\.\s+/gm, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "[$1]").replace(/^>\s+/gm, " ").replace(/---+/g, "").replace(/\n{3,}/g, "\n\n").trim();
1964
+ }
1965
+ function formatBody(body, maxLines) {
1966
+ const plain = stripMarkdown(body);
1967
+ const lines = plain.split("\n");
1968
+ const truncated = lines.slice(0, maxLines).join("\n");
1969
+ return { text: truncated, remaining: Math.max(0, lines.length - maxLines) };
1970
+ }
1971
+ function countSlackLinks(body) {
1972
+ if (!body) return 0;
1973
+ return (body.match(SLACK_URL_RE) ?? []).length;
1974
+ }
1975
+ function BodySection({
1976
+ body,
1977
+ issueNumber
1978
+ }) {
1979
+ const { text, remaining } = formatBody(body, 15);
1980
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
1981
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
1982
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "--- Description ---" }),
1983
+ /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: text }),
1984
+ remaining > 0 ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1985
+ "... (",
1986
+ remaining,
1987
+ " more lines \u2014 gh issue view ",
1988
+ issueNumber,
1989
+ " for full)"
1990
+ ] }) : null
1991
+ ] });
1992
+ }
1993
+ function DetailPanel({ issue, task: task2, width }) {
1994
+ if (!(issue || task2)) {
1995
+ return /* @__PURE__ */ jsx5(
1996
+ Box5,
1997
+ {
1998
+ width,
1999
+ borderStyle: "single",
2000
+ borderColor: "gray",
2001
+ flexDirection: "column",
2002
+ paddingX: 1,
2003
+ children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "No item selected" })
2004
+ }
2005
+ );
2006
+ }
2007
+ if (issue) {
2008
+ return /* @__PURE__ */ jsxs5(
2009
+ Box5,
2010
+ {
2011
+ width,
2012
+ borderStyle: "single",
2013
+ borderColor: "cyan",
2014
+ flexDirection: "column",
2015
+ paddingX: 1,
2016
+ children: [
2017
+ /* @__PURE__ */ jsxs5(Text5, { color: "cyan", bold: true, children: [
2018
+ "#",
2019
+ issue.number,
2020
+ " ",
2021
+ issue.title
2022
+ ] }),
2023
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
2024
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2025
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "State: " }),
2026
+ /* @__PURE__ */ jsx5(Text5, { color: issue.state === "open" ? "green" : "red", children: issue.state })
2027
+ ] }),
2028
+ (issue.assignees ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2029
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Assignees: " }),
2030
+ /* @__PURE__ */ jsx5(Text5, { children: (issue.assignees ?? []).map((a) => a.login).join(", ") })
2031
+ ] }) : null,
2032
+ issue.labels.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2033
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Labels: " }),
2034
+ /* @__PURE__ */ jsx5(Text5, { children: issue.labels.map((l) => l.name).join(", ") })
2035
+ ] }) : null,
2036
+ issue.projectStatus ? /* @__PURE__ */ jsxs5(Box5, { children: [
2037
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Status: " }),
2038
+ /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: issue.projectStatus })
2039
+ ] }) : null,
2040
+ issue.targetDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2041
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Target: " }),
2042
+ /* @__PURE__ */ jsx5(Text5, { children: issue.targetDate })
2043
+ ] }) : null,
2044
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2045
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Updated: " }),
2046
+ /* @__PURE__ */ jsx5(Text5, { children: new Date(issue.updatedAt).toLocaleString() })
2047
+ ] }),
2048
+ issue.slackThreadUrl ? /* @__PURE__ */ jsxs5(Box5, { children: [
2049
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Slack: " }),
2050
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: countSlackLinks(issue.body) > 1 ? `${countSlackLinks(issue.body)} links (s opens first)` : "thread (s to open)" })
2051
+ ] }) : null,
2052
+ issue.body ? /* @__PURE__ */ jsx5(BodySection, { body: issue.body, issueNumber: issue.number }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
2053
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
2054
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "(no description)" })
2055
+ ] }),
2056
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
2057
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: issue.url })
2058
+ ]
2059
+ }
2060
+ );
2061
+ }
2062
+ const t = task2;
2063
+ return /* @__PURE__ */ jsxs5(
2064
+ Box5,
2065
+ {
2066
+ width,
2067
+ borderStyle: "single",
2068
+ borderColor: "yellow",
2069
+ flexDirection: "column",
2070
+ paddingX: 1,
2071
+ children: [
2072
+ /* @__PURE__ */ jsx5(Text5, { color: "yellow", bold: true, children: t.title }),
2073
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
2074
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2075
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Priority: " }),
2076
+ /* @__PURE__ */ jsx5(Text5, { children: PRIORITY_LABELS2[t.priority] ?? "None" })
2077
+ ] }),
2078
+ t.dueDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2079
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Due: " }),
2080
+ /* @__PURE__ */ jsx5(Text5, { children: new Date(t.dueDate).toLocaleDateString() })
2081
+ ] }) : null,
2082
+ (t.tags ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2083
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Tags: " }),
2084
+ /* @__PURE__ */ jsx5(Text5, { children: t.tags.join(", ") })
2085
+ ] }) : null,
2086
+ t.content ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2087
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
2088
+ /* @__PURE__ */ jsx5(Text5, { children: truncateLines(t.content, 8) })
2089
+ ] }) : null,
2090
+ (t.items ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2091
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
2092
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Checklist:" }),
2093
+ t.items.slice(0, 5).map((item) => /* @__PURE__ */ jsxs5(Text5, { children: [
2094
+ item.status === 2 ? "\u2611" : "\u2610",
2095
+ " ",
2096
+ item.title
2097
+ ] }, item.id)),
2098
+ t.items.length > 5 ? /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
2099
+ "...and ",
2100
+ t.items.length - 5,
2101
+ " more"
2102
+ ] }) : null
2103
+ ] }) : null
2104
+ ]
2105
+ }
2106
+ );
2107
+ }
2108
+ var SLACK_URL_RE, PRIORITY_LABELS2;
2109
+ var init_detail_panel = __esm({
2110
+ "src/board/components/detail-panel.tsx"() {
2111
+ "use strict";
2112
+ init_types();
2113
+ SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/gi;
2114
+ PRIORITY_LABELS2 = {
2115
+ [5 /* High */]: "High",
2116
+ [3 /* Medium */]: "Medium",
2117
+ [1 /* Low */]: "Low",
2118
+ [0 /* None */]: "None"
2119
+ };
2120
+ }
2121
+ });
2122
+
2123
+ // src/board/components/focus-mode.tsx
2124
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
2125
+ import { useCallback as useCallback7, useEffect as useEffect2, useRef as useRef6, useState as useState7 } from "react";
2126
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2127
+ function formatTime(secs) {
2128
+ const m = Math.floor(secs / 60);
2129
+ const s = secs % 60;
2130
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2131
+ }
2132
+ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2133
+ const [remaining, setRemaining] = useState7(durationSec);
2134
+ const [timerDone, setTimerDone] = useState7(false);
2135
+ const bellSentRef = useRef6(false);
2136
+ useEffect2(() => {
2137
+ if (timerDone) return;
2138
+ const interval = setInterval(() => {
2139
+ setRemaining((prev) => {
2140
+ if (prev <= 1) {
2141
+ clearInterval(interval);
2142
+ setTimerDone(true);
2143
+ return 0;
2144
+ }
2145
+ return prev - 1;
2146
+ });
2147
+ }, 1e3);
2148
+ return () => clearInterval(interval);
2149
+ }, [timerDone]);
2150
+ useEffect2(() => {
2151
+ if (timerDone && !bellSentRef.current) {
2152
+ bellSentRef.current = true;
2153
+ process.stdout.write("\x07");
2154
+ }
2155
+ }, [timerDone]);
2156
+ const handleInput = useCallback7(
2157
+ (input2, key) => {
2158
+ if (key.escape) {
2159
+ if (timerDone) {
2160
+ onEndAction("exit");
2161
+ } else {
2162
+ onExit();
2163
+ }
2164
+ return;
2165
+ }
2166
+ if (!timerDone) return;
2167
+ switch (input2.toLowerCase()) {
2168
+ case "c":
2169
+ onEndAction("restart");
2170
+ break;
2171
+ case "b":
2172
+ onEndAction("break");
2173
+ break;
2174
+ case "d":
2175
+ onEndAction("done");
2176
+ break;
2177
+ }
2178
+ },
2179
+ [timerDone, onExit, onEndAction]
2180
+ );
2181
+ useInput5(handleInput);
2182
+ if (timerDone) {
2183
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2184
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2185
+ /* @__PURE__ */ jsx6(Text6, { color: "green", bold: true, children: "Focus complete!" }),
2186
+ /* @__PURE__ */ jsxs6(Text6, { color: "gray", children: [
2187
+ " ",
2188
+ label
2189
+ ] })
2190
+ ] }),
2191
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
2192
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[c]" }),
2193
+ /* @__PURE__ */ jsx6(Text6, { children: " Continue " }),
2194
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[b]" }),
2195
+ /* @__PURE__ */ jsx6(Text6, { children: " Break " }),
2196
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[d]" }),
2197
+ /* @__PURE__ */ jsx6(Text6, { children: " Done " }),
2198
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "[Esc]" }),
2199
+ /* @__PURE__ */ jsx6(Text6, { children: " Exit" })
2200
+ ] })
2201
+ ] });
2202
+ }
2203
+ const progress = 1 - remaining / durationSec;
2204
+ const barWidth = 20;
2205
+ const filled = Math.round(progress * barWidth);
2206
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
2207
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2208
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2209
+ /* @__PURE__ */ jsxs6(Text6, { color: "magenta", bold: true, children: [
2210
+ "Focus:",
2211
+ " "
2212
+ ] }),
2213
+ /* @__PURE__ */ jsx6(Text6, { children: label })
2214
+ ] }),
2215
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2216
+ /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: bar }),
2217
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
2218
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: formatTime(remaining) }),
2219
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: " remaining" })
2220
+ ] }),
2221
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "Esc to exit focus" })
2222
+ ] });
2223
+ }
2224
+ var init_focus_mode = __esm({
2225
+ "src/board/components/focus-mode.tsx"() {
2226
+ "use strict";
2227
+ }
2228
+ });
2229
+
2230
+ // src/board/components/help-overlay.tsx
2231
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
2232
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2233
+ function HelpOverlay({ currentMode, onClose }) {
2234
+ useInput6((_input, key) => {
2235
+ if (key.escape) onClose();
2236
+ });
2237
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2238
+ /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
2239
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: "Keyboard Shortcuts" }),
2240
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2241
+ "mode: ",
2242
+ currentMode
2243
+ ] })
2244
+ ] }),
2245
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
2246
+ SHORTCUTS.map((group) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
2247
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", bold: true, children: group.category }),
2248
+ group.items.map((item) => /* @__PURE__ */ jsxs7(Box7, { children: [
2249
+ /* @__PURE__ */ jsx7(Box7, { width: 16, children: /* @__PURE__ */ jsx7(Text7, { color: "green", children: item.key }) }),
2250
+ /* @__PURE__ */ jsx7(Text7, { children: item.desc })
2251
+ ] }, item.key))
2252
+ ] }, group.category)),
2253
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press ? or Esc to close" })
2254
+ ] });
2255
+ }
2256
+ var SHORTCUTS;
2257
+ var init_help_overlay = __esm({
2258
+ "src/board/components/help-overlay.tsx"() {
2259
+ "use strict";
2260
+ SHORTCUTS = [
2261
+ {
2262
+ category: "Navigation",
2263
+ items: [
2264
+ { key: "j / Down", desc: "Move down" },
2265
+ { key: "k / Up", desc: "Move up" },
2266
+ { key: "Tab", desc: "Next section" },
2267
+ { key: "Shift+Tab", desc: "Previous section" }
2268
+ ]
2269
+ },
2270
+ {
2271
+ category: "View",
2272
+ items: [
2273
+ { key: "Enter", desc: "Toggle section / Open in browser" },
2274
+ { key: "Space", desc: "Toggle section / Multi-select" },
2275
+ { key: "/", desc: "Search" },
2276
+ { key: "?", desc: "Toggle help" },
2277
+ { key: "Esc", desc: "Close overlay / Back to normal" }
2278
+ ]
2279
+ },
2280
+ {
2281
+ category: "Actions",
2282
+ items: [
2283
+ { key: "p", desc: "Pick issue (assign + TickTick)" },
2284
+ { key: "a", desc: "Assign to self" },
2285
+ { key: "u", desc: "Unassign self" },
2286
+ { key: "c", desc: "Comment on issue" },
2287
+ { key: "m", desc: "Move status" },
2288
+ { key: "s", desc: "Open Slack thread" },
2289
+ { key: "n", desc: "Create new issue" }
2290
+ ]
2291
+ },
2292
+ {
2293
+ category: "Board",
2294
+ items: [
2295
+ { key: "r", desc: "Refresh data" },
2296
+ { key: "q", desc: "Quit" }
2297
+ ]
2298
+ }
2299
+ ];
2300
+ }
2301
+ });
2302
+
2303
+ // src/board/components/issue-row.tsx
2304
+ import { Box as Box8, Text as Text8 } from "ink";
2305
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2306
+ function truncate(s, max) {
2307
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2308
+ }
2309
+ function formatTargetDate(dateStr) {
2310
+ if (!dateStr) return { text: "", color: "gray" };
2311
+ const d = new Date(dateStr);
2312
+ const days = Math.ceil((d.getTime() - Date.now()) / 864e5);
2313
+ if (days < 0) return { text: `${Math.abs(days)}d overdue`, color: "red" };
2314
+ if (days === 0) return { text: "today", color: "yellow" };
2315
+ if (days === 1) return { text: "tomorrow", color: "white" };
2316
+ if (days <= 7) return { text: `in ${days}d`, color: "white" };
2317
+ return {
2318
+ text: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
2319
+ color: "gray"
2320
+ };
2321
+ }
2322
+ function timeAgo(dateStr) {
2323
+ const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
2324
+ if (seconds < 60) return "now";
2325
+ const minutes = Math.floor(seconds / 60);
2326
+ if (minutes < 60) return `${minutes}m`;
2327
+ const hours = Math.floor(minutes / 60);
2328
+ if (hours < 24) return `${hours}h`;
2329
+ const days = Math.floor(hours / 24);
2330
+ if (days < 30) return `${days}d`;
2331
+ const months = Math.floor(days / 30);
2332
+ return `${months}mo`;
2333
+ }
2334
+ function labelColor(name) {
2335
+ return LABEL_COLORS[name.toLowerCase()] ?? "cyan";
2336
+ }
2337
+ function IssueRow({ issue, selfLogin, isSelected }) {
2338
+ const assignees = issue.assignees ?? [];
2339
+ const isSelf = assignees.some((a) => a.login === selfLogin);
2340
+ const isUnassigned = assignees.length === 0;
2341
+ const assigneeColor = isSelf ? "green" : isUnassigned ? "gray" : "white";
2342
+ const assigneeText = isUnassigned ? "unassigned" : truncate(assignees.map((a) => a.login).join(", "), 14);
2343
+ const labels = (issue.labels ?? []).slice(0, 2);
2344
+ const target = formatTargetDate(issue.targetDate);
2345
+ const titleStr = truncate(issue.title, 42).padEnd(42);
2346
+ return /* @__PURE__ */ jsxs8(Box8, { children: [
2347
+ isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx8(Text8, { children: " " }),
2348
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
2349
+ "#",
2350
+ String(issue.number).padEnd(5)
2351
+ ] }),
2352
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2353
+ isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx8(Text8, { children: titleStr }),
2354
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2355
+ /* @__PURE__ */ jsx8(Box8, { width: LABEL_COL_WIDTH, children: labels.map((l, i) => /* @__PURE__ */ jsxs8(Text8, { children: [
2356
+ i > 0 ? " " : "",
2357
+ /* @__PURE__ */ jsxs8(Text8, { color: labelColor(l.name), children: [
2358
+ "[",
2359
+ truncate(l.name, 12),
2360
+ "]"
2361
+ ] })
2362
+ ] }, l.name)) }),
2363
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2364
+ /* @__PURE__ */ jsx8(Text8, { color: assigneeColor, children: assigneeText.padEnd(14) }),
2365
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2366
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", children: timeAgo(issue.updatedAt).padStart(4) }),
2367
+ target.text ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2368
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2369
+ /* @__PURE__ */ jsx8(Text8, { color: target.color, children: target.text })
2370
+ ] }) : null
2371
+ ] });
2372
+ }
2373
+ var LABEL_COLORS, LABEL_COL_WIDTH;
2374
+ var init_issue_row = __esm({
2375
+ "src/board/components/issue-row.tsx"() {
2376
+ "use strict";
2377
+ LABEL_COLORS = {
2378
+ bug: "red",
2379
+ enhancement: "green",
2380
+ feature: "green",
2381
+ documentation: "blue",
2382
+ "good first issue": "magenta",
2383
+ help: "yellow",
2384
+ question: "yellow",
2385
+ urgent: "red",
2386
+ wontfix: "gray"
2387
+ };
2388
+ LABEL_COL_WIDTH = 30;
2389
+ }
2390
+ });
2391
+
2392
+ // src/board/components/search-bar.tsx
2393
+ import { TextInput as TextInput3 } from "@inkjs/ui";
2394
+ import { Box as Box9, Text as Text9 } from "ink";
2395
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2396
+ function SearchBar({ defaultValue, onChange, onSubmit }) {
2397
+ return /* @__PURE__ */ jsxs9(Box9, { children: [
2398
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "/" }),
2399
+ /* @__PURE__ */ jsx9(
2400
+ TextInput3,
2401
+ {
2402
+ defaultValue,
2403
+ placeholder: "search...",
2404
+ onChange,
2405
+ onSubmit
2406
+ }
2407
+ )
2408
+ ] });
2409
+ }
2410
+ var init_search_bar = __esm({
2411
+ "src/board/components/search-bar.tsx"() {
2412
+ "use strict";
2413
+ }
2414
+ });
2415
+
2416
+ // src/board/components/status-picker.tsx
2417
+ import { Box as Box10, Text as Text10, useInput as useInput7 } from "ink";
2418
+ import { useState as useState8 } from "react";
2419
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2420
+ function StatusPicker({ options, currentStatus, onSelect, onCancel }) {
2421
+ const [selectedIdx, setSelectedIdx] = useState8(() => {
2422
+ const idx = options.findIndex((o) => o.name === currentStatus);
2423
+ return idx >= 0 ? idx : 0;
2424
+ });
2425
+ useInput7((input2, key) => {
2426
+ if (key.escape) return onCancel();
2427
+ if (key.return) {
2428
+ const opt = options[selectedIdx];
2429
+ if (opt) onSelect(opt.id);
2430
+ return;
2431
+ }
2432
+ if (input2 === "j" || key.downArrow) {
2433
+ setSelectedIdx((i) => Math.min(i + 1, options.length - 1));
2434
+ }
2435
+ if (input2 === "k" || key.upArrow) {
2436
+ setSelectedIdx((i) => Math.max(i - 1, 0));
2437
+ }
2438
+ });
2439
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2440
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: "Move to status:" }),
2441
+ options.map((opt, i) => {
2442
+ const isCurrent = opt.name === currentStatus;
2443
+ const isSelected = i === selectedIdx;
2444
+ const prefix = isSelected ? "> " : " ";
2445
+ const suffix = isCurrent ? " (current)" : "";
2446
+ return /* @__PURE__ */ jsxs10(
2447
+ Text10,
2448
+ {
2449
+ ...isSelected ? { color: "cyan" } : {},
2450
+ dimColor: isCurrent,
2451
+ children: [
2452
+ prefix,
2453
+ opt.name,
2454
+ suffix
2455
+ ]
2456
+ },
2457
+ opt.id
2458
+ );
2459
+ }),
2460
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
2461
+ ] });
2462
+ }
2463
+ var init_status_picker = __esm({
2464
+ "src/board/components/status-picker.tsx"() {
2465
+ "use strict";
2466
+ }
2467
+ });
2468
+
2469
+ // src/board/components/task-row.tsx
2470
+ import { Box as Box11, Text as Text11 } from "ink";
2471
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2472
+ function truncate2(s, max) {
2473
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2474
+ }
2475
+ function formatDue(dateStr) {
2476
+ if (!dateStr) return { text: "", color: "gray" };
2477
+ const d = new Date(dateStr);
2478
+ const days = Math.ceil((d.getTime() - Date.now()) / 864e5);
2479
+ if (days < 0) return { text: `${Math.abs(days)}d overdue`, color: "red" };
2480
+ if (days === 0) return { text: "today", color: "yellow" };
2481
+ if (days === 1) return { text: "tomorrow", color: "white" };
2482
+ if (days <= 7) return { text: `in ${days}d`, color: "white" };
2483
+ return {
2484
+ text: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
2485
+ color: "gray"
2486
+ };
2487
+ }
2488
+ function TaskRow({ task: task2, isSelected }) {
2489
+ const pri = PRIORITY_INDICATORS[task2.priority] ?? DEFAULT_PRIORITY;
2490
+ const due = formatDue(task2.dueDate);
2491
+ const titleStr = truncate2(task2.title, 45).padEnd(45);
2492
+ return /* @__PURE__ */ jsxs11(Box11, { children: [
2493
+ isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx11(Text11, { children: " " }),
2494
+ /* @__PURE__ */ jsx11(Text11, { color: pri.color, children: pri.text }),
2495
+ /* @__PURE__ */ jsx11(Text11, { children: " " }),
2496
+ isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx11(Text11, { children: titleStr }),
2497
+ /* @__PURE__ */ jsx11(Text11, { children: " " }),
2498
+ /* @__PURE__ */ jsx11(Text11, { color: due.color, children: due.text })
2499
+ ] });
2500
+ }
2501
+ var PRIORITY_INDICATORS, DEFAULT_PRIORITY;
2502
+ var init_task_row = __esm({
2503
+ "src/board/components/task-row.tsx"() {
2504
+ "use strict";
2505
+ init_types();
2506
+ PRIORITY_INDICATORS = {
2507
+ [5 /* High */]: { text: "[!]", color: "red" },
2508
+ [3 /* Medium */]: { text: "[~]", color: "yellow" },
2509
+ [1 /* Low */]: { text: "[\u2193]", color: "blue" },
2510
+ [0 /* None */]: { text: " ", color: "gray" }
2511
+ };
2512
+ DEFAULT_PRIORITY = { text: " ", color: "gray" };
2513
+ }
2514
+ });
2515
+
2516
+ // src/board/components/toast-container.tsx
2517
+ import { Spinner } from "@inkjs/ui";
2518
+ import { Box as Box12, Text as Text12 } from "ink";
2519
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2520
+ function ToastContainer({ toasts }) {
2521
+ if (toasts.length === 0) return null;
2522
+ return /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx12(Box12, { children: t.type === "loading" ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
2523
+ /* @__PURE__ */ jsx12(Spinner, { label: "" }),
2524
+ /* @__PURE__ */ jsxs12(Text12, { color: "cyan", children: [
2525
+ " ",
2526
+ t.message
2527
+ ] })
2528
+ ] }) : /* @__PURE__ */ jsxs12(Text12, { color: TYPE_COLORS[t.type], children: [
2529
+ TYPE_PREFIXES[t.type],
2530
+ " ",
2531
+ t.message,
2532
+ t.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "gray", children: t.retry ? " [r]etry [d]ismiss" : " [d]ismiss" }) : null
2533
+ ] }) }, t.id)) });
2534
+ }
2535
+ var TYPE_COLORS, TYPE_PREFIXES;
2536
+ var init_toast_container = __esm({
2537
+ "src/board/components/toast-container.tsx"() {
2538
+ "use strict";
2539
+ TYPE_COLORS = {
2540
+ info: "cyan",
2541
+ success: "green",
2542
+ error: "red",
2543
+ loading: "cyan"
2544
+ };
2545
+ TYPE_PREFIXES = {
2546
+ info: "\u2139",
2547
+ success: "\u2713",
2548
+ error: "\u2717"
2549
+ };
2550
+ }
2551
+ });
2552
+
2553
+ // src/board/components/dashboard.tsx
2554
+ import { execFileSync as execFileSync3 } from "child_process";
2555
+ import { Spinner as Spinner2 } from "@inkjs/ui";
2556
+ import { Box as Box13, Text as Text13, useApp, useInput as useInput8, useStdout } from "ink";
2557
+ import { useCallback as useCallback8, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef7, useState as useState9 } from "react";
2558
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2559
+ function isTerminalStatus(status) {
2560
+ return TERMINAL_STATUS_RE2.test(status);
2561
+ }
2562
+ function resolveStatusGroups(statusOptions, configuredGroups) {
2563
+ if (configuredGroups && configuredGroups.length > 0) {
2564
+ return configuredGroups.map((entry) => {
2565
+ const statuses = entry.split(",").map((s) => s.trim()).filter(Boolean);
2566
+ return { label: statuses[0] ?? entry, statuses };
2567
+ });
2568
+ }
2569
+ const nonTerminal = statusOptions.map((o) => o.name).filter((s) => !isTerminalStatus(s));
2570
+ if (nonTerminal.length > 0 && !nonTerminal.includes("Backlog")) {
2571
+ nonTerminal.push("Backlog");
2572
+ }
2573
+ const order = nonTerminal.length > 0 ? nonTerminal : ["In Progress", "Backlog"];
2574
+ return order.map((s) => ({ label: s, statuses: [s] }));
2575
+ }
2576
+ function issuePriorityRank(issue) {
2577
+ for (const label of issue.labels ?? []) {
2578
+ const rank = PRIORITY_RANK[label.name.toLowerCase()];
2579
+ if (rank != null) return rank;
2580
+ }
2581
+ return 99;
2582
+ }
2583
+ function groupByStatus(issues) {
2584
+ const groups = /* @__PURE__ */ new Map();
2585
+ for (const issue of issues) {
2586
+ const status = issue.projectStatus ?? "Backlog";
2587
+ const list = groups.get(status);
2588
+ if (list) {
2589
+ list.push(issue);
2590
+ } else {
2591
+ groups.set(status, [issue]);
2592
+ }
2593
+ }
2594
+ for (const [, list] of groups) {
2595
+ list.sort((a, b) => issuePriorityRank(a) - issuePriorityRank(b));
2596
+ }
2597
+ return groups;
2598
+ }
2599
+ function collectGroupIssues(statusGroup, byStatus) {
2600
+ const issues = [];
2601
+ for (const status of statusGroup.statuses) {
2602
+ const list = byStatus.get(status);
2603
+ if (list) issues.push(...list);
2604
+ }
2605
+ issues.sort((a, b) => issuePriorityRank(a) - issuePriorityRank(b));
2606
+ return issues;
2607
+ }
2608
+ function buildNavItems(repos, tasks, activityCount) {
2609
+ const items = [];
2610
+ if (activityCount > 0) {
2611
+ items.push({ id: "header:activity", section: "activity", type: "header" });
2612
+ }
2613
+ for (const rd of repos) {
2614
+ items.push({ id: `header:${rd.repo.shortName}`, section: rd.repo.shortName, type: "header" });
2615
+ const statusGroupDefs = resolveStatusGroups(rd.statusOptions, rd.repo.statusGroups);
2616
+ const byStatus = groupByStatus(rd.issues);
2617
+ const coveredStatuses = /* @__PURE__ */ new Set();
2618
+ for (const sg of statusGroupDefs) {
2619
+ const groupIssues = collectGroupIssues(sg, byStatus);
2620
+ if (groupIssues.length === 0) continue;
2621
+ const subId = `sub:${rd.repo.shortName}:${sg.label}`;
2622
+ items.push({ id: subId, section: rd.repo.shortName, type: "subHeader" });
2623
+ for (const issue of groupIssues) {
2624
+ items.push({
2625
+ id: `gh:${rd.repo.name}:${issue.number}`,
2626
+ section: rd.repo.shortName,
2627
+ type: "item",
2628
+ subSection: subId
2629
+ });
2630
+ }
2631
+ for (const s of sg.statuses) coveredStatuses.add(s);
2632
+ }
2633
+ for (const [status, issues] of byStatus) {
2634
+ if (!(coveredStatuses.has(status) || isTerminalStatus(status)) && issues.length > 0) {
2635
+ const subId = `sub:${rd.repo.shortName}:${status}`;
2636
+ items.push({ id: subId, section: rd.repo.shortName, type: "subHeader" });
2637
+ for (const issue of issues) {
2638
+ items.push({
2639
+ id: `gh:${rd.repo.name}:${issue.number}`,
2640
+ section: rd.repo.shortName,
2641
+ type: "item",
2642
+ subSection: subId
2643
+ });
2644
+ }
2645
+ }
2646
+ }
2647
+ }
2648
+ if (tasks.length > 0) {
2649
+ items.push({ id: "header:ticktick", section: "ticktick", type: "header" });
2650
+ for (const task2 of tasks) {
2651
+ items.push({ id: `tt:${task2.id}`, section: "ticktick", type: "item" });
2652
+ }
2653
+ }
2654
+ return items;
2655
+ }
2656
+ function buildFlatRows(repos, tasks, activity, isCollapsed) {
2657
+ const rows = [];
2658
+ if (activity.length > 0) {
2659
+ const collapsed = isCollapsed("activity");
2660
+ rows.push({
2661
+ type: "sectionHeader",
2662
+ key: "header:activity",
2663
+ navId: "header:activity",
2664
+ label: "Recent Activity (24h)",
2665
+ count: activity.length,
2666
+ countLabel: "events",
2667
+ isCollapsed: collapsed
2668
+ });
2669
+ if (!collapsed) {
2670
+ for (const [i, event] of activity.entries()) {
2671
+ rows.push({ type: "activity", key: `act:${i}`, navId: null, event });
2672
+ }
2673
+ }
2674
+ }
2675
+ for (const rd of repos) {
2676
+ const { repo, issues, error: repoError } = rd;
2677
+ const collapsed = isCollapsed(repo.shortName);
2678
+ rows.push({
2679
+ type: "sectionHeader",
2680
+ key: `header:${repo.shortName}`,
2681
+ navId: `header:${repo.shortName}`,
2682
+ label: repo.shortName,
2683
+ count: issues.length,
2684
+ countLabel: "issues",
2685
+ isCollapsed: collapsed
2686
+ });
2687
+ if (!collapsed) {
2688
+ if (repoError) {
2689
+ rows.push({ type: "error", key: `error:${repo.shortName}`, navId: null, text: repoError });
2690
+ } else if (issues.length === 0) {
2691
+ rows.push({
2692
+ type: "subHeader",
2693
+ key: `empty:${repo.shortName}`,
2694
+ navId: null,
2695
+ text: "No open issues"
2696
+ });
2697
+ } else {
2698
+ const statusGroupDefs = resolveStatusGroups(rd.statusOptions, rd.repo.statusGroups);
2699
+ const byStatus = groupByStatus(issues);
2700
+ const coveredStatuses = /* @__PURE__ */ new Set();
2701
+ let isFirstGroup = true;
2702
+ for (const sg of statusGroupDefs) {
2703
+ const groupIssues = collectGroupIssues(sg, byStatus);
2704
+ if (groupIssues.length === 0) continue;
2705
+ if (!isFirstGroup) {
2706
+ rows.push({ type: "gap", key: `gap:${repo.shortName}:${sg.label}`, navId: null });
2707
+ }
2708
+ isFirstGroup = false;
2709
+ const subId = `sub:${repo.shortName}:${sg.label}`;
2710
+ const subCollapsed = isCollapsed(subId);
2711
+ rows.push({
2712
+ type: "subHeader",
2713
+ key: subId,
2714
+ navId: subId,
2715
+ text: sg.label,
2716
+ count: groupIssues.length,
2717
+ isCollapsed: subCollapsed
2718
+ });
2719
+ if (!subCollapsed) {
2720
+ for (const issue of groupIssues) {
2721
+ rows.push({
2722
+ type: "issue",
2723
+ key: `gh:${repo.name}:${issue.number}`,
2724
+ navId: `gh:${repo.name}:${issue.number}`,
2725
+ issue,
2726
+ repoName: repo.name
2727
+ });
2728
+ }
2729
+ }
2730
+ for (const s of sg.statuses) coveredStatuses.add(s);
2731
+ }
2732
+ for (const [status, groupIssues] of byStatus) {
2733
+ if (!(coveredStatuses.has(status) || isTerminalStatus(status)) && groupIssues.length > 0) {
2734
+ if (!isFirstGroup) {
2735
+ rows.push({ type: "gap", key: `gap:${repo.shortName}:${status}`, navId: null });
2736
+ }
2737
+ isFirstGroup = false;
2738
+ const subId = `sub:${repo.shortName}:${status}`;
2739
+ const subCollapsed = isCollapsed(subId);
2740
+ rows.push({
2741
+ type: "subHeader",
2742
+ key: subId,
2743
+ navId: subId,
2744
+ text: status,
2745
+ count: groupIssues.length,
2746
+ isCollapsed: subCollapsed
2747
+ });
2748
+ if (!subCollapsed) {
2749
+ for (const issue of groupIssues) {
2750
+ rows.push({
2751
+ type: "issue",
2752
+ key: `gh:${repo.name}:${issue.number}`,
2753
+ navId: `gh:${repo.name}:${issue.number}`,
2754
+ issue,
2755
+ repoName: repo.name
2756
+ });
2757
+ }
2758
+ }
2759
+ }
2760
+ }
2761
+ }
2762
+ }
2763
+ }
2764
+ if (tasks.length > 0) {
2765
+ const collapsed = isCollapsed("ticktick");
2766
+ rows.push({
2767
+ type: "sectionHeader",
2768
+ key: "header:ticktick",
2769
+ navId: "header:ticktick",
2770
+ label: "Personal (TickTick)",
2771
+ count: tasks.length,
2772
+ countLabel: "tasks",
2773
+ isCollapsed: collapsed
2774
+ });
2775
+ if (!collapsed) {
2776
+ for (const task2 of tasks) {
2777
+ rows.push({ type: "task", key: `tt:${task2.id}`, navId: `tt:${task2.id}`, task: task2 });
2778
+ }
2779
+ }
2780
+ }
2781
+ return rows;
2782
+ }
2783
+ function timeAgo2(date) {
2784
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
2785
+ if (seconds < 10) return "just now";
2786
+ if (seconds < 60) return `${seconds}s ago`;
2787
+ const minutes = Math.floor(seconds / 60);
2788
+ return `${minutes}m ago`;
2789
+ }
2790
+ function openInBrowser(url) {
2791
+ try {
2792
+ execFileSync3("open", [url], { stdio: "ignore" });
2793
+ } catch {
2794
+ }
2795
+ }
2796
+ function findSelectedUrl(repos, selectedId) {
2797
+ if (!selectedId?.startsWith("gh:")) return null;
2798
+ for (const rd of repos) {
2799
+ for (const issue of rd.issues) {
2800
+ if (`gh:${rd.repo.name}:${issue.number}` === selectedId) return issue.url;
2801
+ }
2802
+ }
2803
+ return null;
2804
+ }
2805
+ function findSelectedIssueWithRepo(repos, selectedId) {
2806
+ if (!selectedId?.startsWith("gh:")) return null;
2807
+ for (const rd of repos) {
2808
+ for (const issue of rd.issues) {
2809
+ if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
2810
+ return { issue, repoName: rd.repo.name };
2811
+ }
2812
+ }
2813
+ return null;
2814
+ }
2815
+ function isHeaderId(id) {
2816
+ return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2817
+ }
2818
+ function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
2819
+ switch (row.type) {
2820
+ case "sectionHeader": {
2821
+ const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2822
+ const isSel = selectedId === row.navId;
2823
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
2824
+ /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "white", bold: true, children: [
2825
+ arrow,
2826
+ " ",
2827
+ row.label
2828
+ ] }),
2829
+ /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2830
+ " ",
2831
+ "(",
2832
+ row.count,
2833
+ " ",
2834
+ row.countLabel,
2835
+ ")"
2836
+ ] })
2837
+ ] });
2838
+ }
2839
+ case "subHeader": {
2840
+ if (row.navId) {
2841
+ const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2842
+ const isSel = selectedId === row.navId;
2843
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
2844
+ /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "gray", children: [
2845
+ " ",
2846
+ arrow,
2847
+ " ",
2848
+ row.text
2849
+ ] }),
2850
+ /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2851
+ " (",
2852
+ row.count,
2853
+ ")"
2854
+ ] })
2855
+ ] });
2856
+ }
2857
+ return /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2858
+ " ",
2859
+ row.text
2860
+ ] });
2861
+ }
2862
+ case "issue": {
2863
+ const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
2864
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
2865
+ checkbox2 ? /* @__PURE__ */ jsx13(Text13, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
2866
+ /* @__PURE__ */ jsx13(IssueRow, { issue: row.issue, selfLogin, isSelected: selectedId === row.navId })
2867
+ ] });
2868
+ }
2869
+ case "task": {
2870
+ const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
2871
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
2872
+ checkbox2 ? /* @__PURE__ */ jsx13(Text13, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
2873
+ /* @__PURE__ */ jsx13(TaskRow, { task: row.task, isSelected: selectedId === row.navId })
2874
+ ] });
2875
+ }
2876
+ case "activity": {
2877
+ const ago = timeAgo2(row.event.timestamp);
2878
+ return /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2879
+ " ",
2880
+ ago,
2881
+ ": ",
2882
+ /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2883
+ "@",
2884
+ row.event.actor
2885
+ ] }),
2886
+ " ",
2887
+ row.event.summary,
2888
+ " ",
2889
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2890
+ "(",
2891
+ row.event.repoShortName,
2892
+ ")"
2893
+ ] })
2894
+ ] });
2895
+ }
2896
+ case "error":
2897
+ return /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
2898
+ " Error: ",
2899
+ row.text
2900
+ ] });
2901
+ case "gap":
2902
+ return /* @__PURE__ */ jsx13(Text13, { children: "" });
2903
+ }
2904
+ }
2905
+ function Dashboard({ config: config2, options, activeProfile }) {
2906
+ const { exit } = useApp();
2907
+ const refreshMs = config2.board.refreshInterval * 1e3;
2908
+ const {
2909
+ status,
2910
+ data,
2911
+ error,
2912
+ lastRefresh,
2913
+ isRefreshing,
2914
+ consecutiveFailures,
2915
+ autoRefreshPaused,
2916
+ refresh,
2917
+ mutateData
2918
+ } = useData(config2, options, refreshMs);
2919
+ const allRepos = useMemo2(() => data?.repos ?? [], [data?.repos]);
2920
+ const allTasks = useMemo2(
2921
+ () => config2.ticktick.enabled ? data?.ticktick ?? [] : [],
2922
+ [data?.ticktick, config2.ticktick.enabled]
2923
+ );
2924
+ const allActivity = useMemo2(() => data?.activity ?? [], [data?.activity]);
2925
+ const ui = useUIState();
2926
+ const [searchQuery, setSearchQuery] = useState9("");
2927
+ const { toasts, toast, handleErrorAction } = useToast();
2928
+ const [, setTick] = useState9(0);
2929
+ useEffect3(() => {
2930
+ const id = setInterval(() => setTick((t) => t + 1), 1e4);
2931
+ return () => clearInterval(id);
2932
+ }, []);
2933
+ const repos = useMemo2(() => {
2934
+ if (!searchQuery) return allRepos;
2935
+ const q = searchQuery.toLowerCase();
2936
+ return allRepos.map((rd) => ({ ...rd, issues: rd.issues.filter((i) => i.title.toLowerCase().includes(q)) })).filter((rd) => rd.issues.length > 0);
2937
+ }, [allRepos, searchQuery]);
2938
+ const tasks = useMemo2(() => {
2939
+ if (!searchQuery) return allTasks;
2940
+ const q = searchQuery.toLowerCase();
2941
+ return allTasks.filter((t) => t.title.toLowerCase().includes(q));
2942
+ }, [allTasks, searchQuery]);
2943
+ const navItems = useMemo2(
2944
+ () => buildNavItems(repos, tasks, allActivity.length),
2945
+ [repos, tasks, allActivity.length]
2946
+ );
2947
+ const nav = useNavigation(navItems);
2948
+ const getRepoForId = useCallback8((id) => {
2949
+ if (id.startsWith("gh:")) {
2950
+ const parts = id.split(":");
2951
+ return parts.length >= 3 ? `${parts[1]}` : null;
2952
+ }
2953
+ if (id.startsWith("tt:")) return "ticktick";
2954
+ return null;
2955
+ }, []);
2956
+ const multiSelect = useMultiSelect(getRepoForId);
2957
+ useEffect3(() => {
2958
+ if (multiSelect.count === 0) return;
2959
+ const validIds = new Set(navItems.map((i) => i.id));
2960
+ multiSelect.prune(validIds);
2961
+ }, [navItems, multiSelect]);
2962
+ const actions = useActions({
2963
+ config: config2,
2964
+ repos,
2965
+ selectedId: nav.selectedId,
2966
+ toast,
2967
+ refresh,
2968
+ mutateData,
2969
+ onOverlayDone: ui.exitOverlay
2970
+ });
2971
+ const pendingPickRef = useRef7(null);
2972
+ const handleCreateIssueWithPrompt = useCallback8(
2973
+ (repo, title, labels) => {
2974
+ actions.handleCreateIssue(repo, title, labels).then((result) => {
2975
+ if (result) {
2976
+ pendingPickRef.current = result;
2977
+ ui.enterConfirmPick();
2978
+ }
2979
+ });
2980
+ },
2981
+ [actions, ui]
2982
+ );
2983
+ const handleConfirmPick = useCallback8(() => {
2984
+ const pending = pendingPickRef.current;
2985
+ pendingPickRef.current = null;
2986
+ ui.exitOverlay();
2987
+ if (!pending) return;
2988
+ const rc = config2.repos.find((r) => r.name === pending.repo);
2989
+ if (!rc) return;
2990
+ const t = toast.loading(`Picking ${rc.shortName}#${pending.issueNumber}...`);
2991
+ Promise.resolve().then(() => (init_pick(), pick_exports)).then(
2992
+ ({ pickIssue: pickIssue2 }) => pickIssue2(config2, { repo: rc, issueNumber: pending.issueNumber }).then((result) => {
2993
+ const msg = `Picked ${rc.shortName}#${pending.issueNumber} \u2014 assigned + synced to TickTick`;
2994
+ t.resolve(result.warning ? `${msg} (${result.warning})` : msg);
2995
+ refresh();
2996
+ }).catch((err) => {
2997
+ t.reject(`Pick failed: ${err instanceof Error ? err.message : String(err)}`);
2998
+ })
2999
+ );
3000
+ }, [config2, toast, refresh, ui]);
3001
+ const handleCancelPick = useCallback8(() => {
3002
+ pendingPickRef.current = null;
3003
+ ui.exitOverlay();
3004
+ }, [ui]);
3005
+ const [focusLabel, setFocusLabel] = useState9(null);
3006
+ const handleEnterFocus = useCallback8(() => {
3007
+ const id = nav.selectedId;
3008
+ if (!id || isHeaderId(id)) return;
3009
+ let label = "";
3010
+ if (id.startsWith("gh:")) {
3011
+ const found = findSelectedIssueWithRepo(repos, id);
3012
+ if (found) {
3013
+ const rc = config2.repos.find((r) => r.name === found.repoName);
3014
+ label = `${rc?.shortName ?? found.repoName}#${found.issue.number} \u2014 ${found.issue.title}`;
3015
+ }
3016
+ } else if (id.startsWith("tt:")) {
3017
+ const taskId = id.slice(3);
3018
+ const task2 = tasks.find((t) => t.id === taskId);
3019
+ if (task2) label = task2.title;
3020
+ }
3021
+ if (!label) return;
3022
+ setFocusLabel(label);
3023
+ ui.enterFocus();
3024
+ }, [nav.selectedId, repos, tasks, config2.repos, ui]);
3025
+ const handleFocusExit = useCallback8(() => {
3026
+ setFocusLabel(null);
3027
+ ui.exitToNormal();
3028
+ }, [ui]);
3029
+ const handleFocusEndAction = useCallback8(
3030
+ (action) => {
3031
+ switch (action) {
3032
+ case "restart":
3033
+ toast.info("Focus restarted!");
3034
+ setFocusLabel((prev) => prev);
3035
+ setFocusKey((k) => k + 1);
3036
+ break;
3037
+ case "break":
3038
+ toast.info("Break time! Step away for a few minutes.");
3039
+ setFocusLabel(null);
3040
+ ui.exitToNormal();
3041
+ break;
3042
+ case "done":
3043
+ toast.success("Focus session complete!");
3044
+ setFocusLabel(null);
3045
+ ui.exitToNormal();
3046
+ break;
3047
+ case "exit":
3048
+ setFocusLabel(null);
3049
+ ui.exitToNormal();
3050
+ break;
3051
+ }
3052
+ },
3053
+ [toast, ui]
3054
+ );
3055
+ const [focusKey, setFocusKey] = useState9(0);
3056
+ const { stdout } = useStdout();
3057
+ const [termSize, setTermSize] = useState9({
3058
+ cols: stdout?.columns ?? 80,
3059
+ rows: stdout?.rows ?? 24
3060
+ });
3061
+ useEffect3(() => {
3062
+ if (!stdout) return;
3063
+ const onResize = () => setTermSize({ cols: stdout.columns, rows: stdout.rows });
3064
+ stdout.on("resize", onResize);
3065
+ return () => {
3066
+ stdout.off("resize", onResize);
3067
+ };
3068
+ }, [stdout]);
3069
+ const showDetailPanel = termSize.cols >= 120;
3070
+ const detailPanelWidth = showDetailPanel ? Math.floor(termSize.cols * 0.35) : 0;
3071
+ const overlayBarRows = ui.state.mode === "search" || ui.state.mode === "overlay:comment" ? 1 : 0;
3072
+ const toastRows = toasts.length;
3073
+ const viewportHeight = Math.max(5, termSize.rows - CHROME_ROWS - overlayBarRows - toastRows);
3074
+ const flatRows = useMemo2(
3075
+ () => buildFlatRows(repos, tasks, allActivity, nav.isCollapsed),
3076
+ [repos, tasks, allActivity, nav.isCollapsed]
3077
+ );
3078
+ const scrollRef = useRef7(0);
3079
+ const selectedRowIdx = flatRows.findIndex((r) => r.navId === nav.selectedId);
3080
+ if (selectedRowIdx >= 0) {
3081
+ if (selectedRowIdx < scrollRef.current) {
3082
+ scrollRef.current = selectedRowIdx;
3083
+ } else if (selectedRowIdx >= scrollRef.current + viewportHeight) {
3084
+ scrollRef.current = selectedRowIdx - viewportHeight + 1;
3085
+ }
3086
+ }
3087
+ const maxOffset = Math.max(0, flatRows.length - viewportHeight);
3088
+ scrollRef.current = Math.max(0, Math.min(scrollRef.current, maxOffset));
3089
+ const visibleRows = flatRows.slice(scrollRef.current, scrollRef.current + viewportHeight);
3090
+ const hasMoreAbove = scrollRef.current > 0;
3091
+ const hasMoreBelow = scrollRef.current + viewportHeight < flatRows.length;
3092
+ const aboveCount = scrollRef.current;
3093
+ const belowCount = flatRows.length - scrollRef.current - viewportHeight;
3094
+ const selectedItem = useMemo2(() => {
3095
+ const id = nav.selectedId;
3096
+ if (!id || isHeaderId(id)) return { issue: null, task: null, repoName: null };
3097
+ if (id.startsWith("gh:")) {
3098
+ for (const rd of repos) {
3099
+ for (const issue of rd.issues) {
3100
+ if (`gh:${rd.repo.name}:${issue.number}` === id)
3101
+ return { issue, task: null, repoName: rd.repo.name };
3102
+ }
3103
+ }
3104
+ }
3105
+ if (id.startsWith("tt:")) {
3106
+ const taskId = id.slice(3);
3107
+ const task2 = tasks.find((t) => t.id === taskId);
3108
+ if (task2) return { issue: null, task: task2, repoName: null };
3109
+ }
3110
+ return { issue: null, task: null, repoName: null };
3111
+ }, [nav.selectedId, repos, tasks]);
3112
+ const selectedRepoStatusOptions = useMemo2(() => {
3113
+ const repoName = multiSelect.count > 0 ? multiSelect.constrainedRepo : selectedItem.repoName;
3114
+ if (!repoName || repoName === "ticktick") return [];
3115
+ const rd = repos.find((r) => r.repo.name === repoName);
3116
+ return rd?.statusOptions.filter((o) => !isTerminalStatus(o.name)) ?? [];
3117
+ }, [selectedItem.repoName, repos, multiSelect.count, multiSelect.constrainedRepo]);
3118
+ const handleOpen = useCallback8(() => {
3119
+ const url = findSelectedUrl(repos, nav.selectedId);
3120
+ if (url) openInBrowser(url);
3121
+ }, [repos, nav.selectedId]);
3122
+ const handleSlack = useCallback8(() => {
3123
+ const found = findSelectedIssueWithRepo(repos, nav.selectedId);
3124
+ if (!found?.issue.slackThreadUrl) return;
3125
+ openInBrowser(found.issue.slackThreadUrl);
3126
+ }, [repos, nav.selectedId]);
3127
+ const multiSelectType = useMemo2(() => {
3128
+ let hasGh = false;
3129
+ let hasTt = false;
3130
+ for (const id of multiSelect.selected) {
3131
+ if (id.startsWith("gh:")) hasGh = true;
3132
+ if (id.startsWith("tt:")) hasTt = true;
3133
+ }
3134
+ if (hasGh && hasTt) return "mixed";
3135
+ if (hasTt) return "ticktick";
3136
+ return "github";
3137
+ }, [multiSelect.selected]);
3138
+ const handleBulkAction = useCallback8(
3139
+ (action) => {
3140
+ const ids = multiSelect.selected;
3141
+ switch (action.type) {
3142
+ case "assign": {
3143
+ ui.exitOverlay();
3144
+ actions.handleBulkAssign(ids).then((failedIds) => {
3145
+ if (failedIds.length > 0) {
3146
+ multiSelect.clear();
3147
+ for (const id of failedIds) multiSelect.toggle(id);
3148
+ } else {
3149
+ multiSelect.clear();
3150
+ ui.clearMultiSelect();
3151
+ }
3152
+ });
3153
+ return;
3154
+ }
3155
+ case "unassign": {
3156
+ ui.exitOverlay();
3157
+ actions.handleBulkUnassign(ids).then((failedIds) => {
3158
+ if (failedIds.length > 0) {
3159
+ multiSelect.clear();
3160
+ for (const id of failedIds) multiSelect.toggle(id);
3161
+ } else {
3162
+ multiSelect.clear();
3163
+ ui.clearMultiSelect();
3164
+ }
3165
+ });
3166
+ return;
3167
+ }
3168
+ case "statusChange":
3169
+ ui.enterStatus();
3170
+ return;
3171
+ // status picker will call handleBulkStatusSelect on select
3172
+ case "complete":
3173
+ case "delete":
3174
+ toast.info(`Bulk ${action.type} not yet implemented for TickTick`);
3175
+ ui.exitOverlay();
3176
+ multiSelect.clear();
3177
+ return;
3178
+ }
3179
+ },
3180
+ [multiSelect, actions, ui, toast]
3181
+ );
3182
+ const handleBulkStatusSelect = useCallback8(
3183
+ (optionId) => {
3184
+ const ids = multiSelect.selected;
3185
+ ui.exitOverlay();
3186
+ actions.handleBulkStatusChange(ids, optionId).then((failedIds) => {
3187
+ if (failedIds.length > 0) {
3188
+ multiSelect.clear();
3189
+ for (const id of failedIds) multiSelect.toggle(id);
3190
+ } else {
3191
+ multiSelect.clear();
3192
+ ui.clearMultiSelect();
3193
+ }
3194
+ });
3195
+ },
3196
+ [multiSelect, actions, ui]
3197
+ );
3198
+ const handleInput = useCallback8(
3199
+ (input2, key) => {
3200
+ if (input2 === "?") {
3201
+ ui.toggleHelp();
3202
+ return;
3203
+ }
3204
+ if (key.escape && ui.state.mode !== "focus") {
3205
+ if (ui.state.mode === "multiSelect") {
3206
+ multiSelect.clear();
3207
+ }
3208
+ ui.exitOverlay();
3209
+ return;
3210
+ }
3211
+ if (ui.canNavigate) {
3212
+ if (input2 === "j" || key.downArrow) {
3213
+ nav.moveDown();
3214
+ return;
3215
+ }
3216
+ if (input2 === "k" || key.upArrow) {
3217
+ nav.moveUp();
3218
+ return;
3219
+ }
3220
+ if (key.tab) {
3221
+ if (ui.state.mode === "multiSelect") {
3222
+ multiSelect.clear();
3223
+ ui.clearMultiSelect();
3224
+ }
3225
+ key.shift ? nav.prevSection() : nav.nextSection();
3226
+ return;
3227
+ }
3228
+ }
3229
+ if (ui.state.mode === "multiSelect") {
3230
+ if (input2 === " ") {
3231
+ const id = nav.selectedId;
3232
+ if (id && !isHeaderId(id)) {
3233
+ multiSelect.toggle(id);
3234
+ }
3235
+ return;
3236
+ }
3237
+ if (key.return) {
3238
+ if (multiSelect.count > 0) {
3239
+ ui.enterBulkAction();
3240
+ }
3241
+ return;
3242
+ }
3243
+ if (input2 === "m" && multiSelect.count > 0) {
3244
+ ui.enterBulkAction();
3245
+ return;
3246
+ }
3247
+ return;
3248
+ }
3249
+ if (input2 === "d") {
3250
+ if (handleErrorAction("dismiss")) return;
3251
+ }
3252
+ if (input2 === "r" && handleErrorAction("retry")) return;
3253
+ if (ui.canAct) {
3254
+ if (input2 === "/") {
3255
+ multiSelect.clear();
3256
+ ui.enterSearch();
3257
+ return;
3258
+ }
3259
+ if (input2 === "q") {
3260
+ exit();
3261
+ return;
3262
+ }
3263
+ if (input2 === "r" || input2 === "R") {
3264
+ multiSelect.clear();
3265
+ refresh();
3266
+ return;
3267
+ }
3268
+ if (input2 === "s") {
3269
+ handleSlack();
3270
+ return;
3271
+ }
3272
+ if (input2 === "p") {
3273
+ actions.handlePick();
3274
+ return;
3275
+ }
3276
+ if (input2 === "a") {
3277
+ actions.handleAssign();
3278
+ return;
3279
+ }
3280
+ if (input2 === "u") {
3281
+ actions.handleUnassign();
3282
+ return;
3283
+ }
3284
+ if (input2 === "c") {
3285
+ if (selectedItem.issue) {
3286
+ multiSelect.clear();
3287
+ ui.enterComment();
3288
+ }
3289
+ return;
3290
+ }
3291
+ if (input2 === "m") {
3292
+ if (selectedItem.issue && selectedRepoStatusOptions.length > 0) {
3293
+ multiSelect.clear();
3294
+ ui.enterStatus();
3295
+ } else if (selectedItem.issue) {
3296
+ toast.info("Issue not in a project board");
3297
+ }
3298
+ return;
3299
+ }
3300
+ if (input2 === "n") {
3301
+ multiSelect.clear();
3302
+ ui.enterCreate();
3303
+ return;
3304
+ }
3305
+ if (input2 === "f") {
3306
+ handleEnterFocus();
3307
+ return;
3308
+ }
3309
+ if (input2 === " ") {
3310
+ const id = nav.selectedId;
3311
+ if (id && !isHeaderId(id)) {
3312
+ multiSelect.toggle(id);
3313
+ ui.enterMultiSelect();
3314
+ } else if (isHeaderId(nav.selectedId)) {
3315
+ nav.toggleSection();
3316
+ }
3317
+ return;
3318
+ }
3319
+ if (key.return) {
3320
+ if (isHeaderId(nav.selectedId)) {
3321
+ nav.toggleSection();
3322
+ return;
3323
+ }
3324
+ handleOpen();
3325
+ return;
3326
+ }
3327
+ }
3328
+ },
3329
+ [
3330
+ ui,
3331
+ nav,
3332
+ exit,
3333
+ refresh,
3334
+ handleSlack,
3335
+ handleOpen,
3336
+ actions,
3337
+ selectedItem.issue,
3338
+ selectedRepoStatusOptions.length,
3339
+ toast,
3340
+ nav.selectedId,
3341
+ multiSelect,
3342
+ handleEnterFocus,
3343
+ handleErrorAction
3344
+ ]
3345
+ );
3346
+ const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus";
3347
+ useInput8(handleInput, { isActive: inputActive });
3348
+ const handleSearchEscape = useCallback8(
3349
+ (_input, key) => {
3350
+ if (key.escape) {
3351
+ ui.exitOverlay();
3352
+ setSearchQuery("");
3353
+ }
3354
+ },
3355
+ [ui]
3356
+ );
3357
+ useInput8(handleSearchEscape, { isActive: ui.state.mode === "search" });
3358
+ if (status === "loading" && !data) {
3359
+ return /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx13(Spinner2, { label: "Loading dashboard..." }) });
3360
+ }
3361
+ const now = data?.fetchedAt ?? /* @__PURE__ */ new Date();
3362
+ const dateStr = now.toLocaleDateString("en-US", {
3363
+ month: "short",
3364
+ day: "numeric",
3365
+ year: "numeric"
3366
+ });
3367
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
3368
+ /* @__PURE__ */ jsxs13(Box13, { children: [
3369
+ /* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: "HOG BOARD" }),
3370
+ activeProfile ? /* @__PURE__ */ jsxs13(Text13, { color: "yellow", children: [
3371
+ " [",
3372
+ activeProfile,
3373
+ "]"
3374
+ ] }) : null,
3375
+ /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
3376
+ " ",
3377
+ "\u2014",
3378
+ " ",
3379
+ dateStr
3380
+ ] }),
3381
+ /* @__PURE__ */ jsx13(Text13, { children: " " }),
3382
+ isRefreshing ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3383
+ /* @__PURE__ */ jsx13(Spinner2, { label: "" }),
3384
+ /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: " Refreshing..." })
3385
+ ] }) : lastRefresh ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3386
+ /* @__PURE__ */ jsxs13(Text13, { color: refreshAgeColor(lastRefresh), children: [
3387
+ "Updated ",
3388
+ timeAgo2(lastRefresh)
3389
+ ] }),
3390
+ consecutiveFailures > 0 ? /* @__PURE__ */ jsx13(Text13, { color: "red", children: " (!)" }) : null
3391
+ ] }) : null,
3392
+ autoRefreshPaused ? /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: " Auto-refresh paused \u2014 press r to retry" }) : null
3393
+ ] }),
3394
+ error ? /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
3395
+ "Error: ",
3396
+ error
3397
+ ] }) : null,
3398
+ ui.state.helpVisible ? /* @__PURE__ */ jsx13(HelpOverlay, { currentMode: ui.state.mode, onClose: ui.toggleHelp }) : null,
3399
+ ui.state.mode === "overlay:status" && selectedRepoStatusOptions.length > 0 ? /* @__PURE__ */ jsx13(
3400
+ StatusPicker,
3401
+ {
3402
+ options: selectedRepoStatusOptions,
3403
+ currentStatus: multiSelect.count > 0 ? void 0 : selectedItem.issue?.projectStatus,
3404
+ onSelect: multiSelect.count > 0 ? handleBulkStatusSelect : actions.handleStatusChange,
3405
+ onCancel: ui.exitOverlay
3406
+ }
3407
+ ) : null,
3408
+ ui.state.mode === "overlay:create" ? /* @__PURE__ */ jsx13(
3409
+ CreateIssueForm,
3410
+ {
3411
+ repos: config2.repos,
3412
+ defaultRepo: selectedItem.repoName,
3413
+ onSubmit: handleCreateIssueWithPrompt,
3414
+ onCancel: ui.exitOverlay
3415
+ }
3416
+ ) : null,
3417
+ ui.state.mode === "overlay:confirmPick" ? /* @__PURE__ */ jsx13(
3418
+ ConfirmPrompt,
3419
+ {
3420
+ message: "Pick this issue?",
3421
+ onConfirm: handleConfirmPick,
3422
+ onCancel: handleCancelPick
3423
+ }
3424
+ ) : null,
3425
+ ui.state.mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx13(
3426
+ BulkActionMenu,
3427
+ {
3428
+ count: multiSelect.count,
3429
+ selectionType: multiSelectType,
3430
+ onSelect: handleBulkAction,
3431
+ onCancel: ui.exitOverlay
3432
+ }
3433
+ ) : null,
3434
+ ui.state.mode === "focus" && focusLabel ? /* @__PURE__ */ jsx13(
3435
+ FocusMode,
3436
+ {
3437
+ label: focusLabel,
3438
+ durationSec: config2.board.focusDuration ?? 1500,
3439
+ onExit: handleFocusExit,
3440
+ onEndAction: handleFocusEndAction
3441
+ },
3442
+ focusKey
3443
+ ) : null,
3444
+ !ui.state.helpVisible && ui.state.mode !== "overlay:status" && ui.state.mode !== "overlay:create" && ui.state.mode !== "overlay:bulkAction" && ui.state.mode !== "overlay:confirmPick" && ui.state.mode !== "focus" ? /* @__PURE__ */ jsxs13(Box13, { height: viewportHeight, children: [
3445
+ /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", flexGrow: 1, children: [
3446
+ hasMoreAbove ? /* @__PURE__ */ jsxs13(Text13, { color: "gray", dimColor: true, children: [
3447
+ " ",
3448
+ "\u25B2",
3449
+ " ",
3450
+ aboveCount,
3451
+ " more above"
3452
+ ] }) : null,
3453
+ visibleRows.map((row) => /* @__PURE__ */ jsx13(
3454
+ RowRenderer,
3455
+ {
3456
+ row,
3457
+ selectedId: nav.selectedId,
3458
+ selfLogin: config2.board.assignee,
3459
+ isMultiSelected: ui.state.mode === "multiSelect" && row.navId ? multiSelect.isSelected(row.navId) : void 0
3460
+ },
3461
+ row.key
3462
+ )),
3463
+ hasMoreBelow ? /* @__PURE__ */ jsxs13(Text13, { color: "gray", dimColor: true, children: [
3464
+ " ",
3465
+ "\u25BC",
3466
+ " ",
3467
+ belowCount,
3468
+ " more below"
3469
+ ] }) : null
3470
+ ] }),
3471
+ showDetailPanel ? /* @__PURE__ */ jsx13(Box13, { marginLeft: 1, width: detailPanelWidth, children: /* @__PURE__ */ jsx13(
3472
+ DetailPanel,
3473
+ {
3474
+ issue: selectedItem.issue,
3475
+ task: selectedItem.task,
3476
+ width: detailPanelWidth
3477
+ }
3478
+ ) }) : null
3479
+ ] }) : null,
3480
+ ui.state.mode === "search" ? /* @__PURE__ */ jsx13(SearchBar, { defaultValue: searchQuery, onChange: setSearchQuery, onSubmit: ui.exitOverlay }) : null,
3481
+ ui.state.mode === "overlay:comment" && selectedItem.issue ? /* @__PURE__ */ jsx13(
3482
+ CommentInput,
3483
+ {
3484
+ issueNumber: selectedItem.issue.number,
3485
+ onSubmit: actions.handleComment,
3486
+ onCancel: ui.exitOverlay
3487
+ }
3488
+ ) : null,
3489
+ /* @__PURE__ */ jsx13(ToastContainer, { toasts }),
3490
+ /* @__PURE__ */ jsx13(Box13, { children: ui.state.mode === "multiSelect" ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3491
+ /* @__PURE__ */ jsxs13(Text13, { color: "cyan", bold: true, children: [
3492
+ multiSelect.count,
3493
+ " selected"
3494
+ ] }),
3495
+ /* @__PURE__ */ jsx13(Text13, { color: "gray", children: " Space:toggle Enter:actions Esc:cancel" })
3496
+ ] }) : ui.state.mode === "focus" ? /* @__PURE__ */ jsx13(Text13, { color: "magenta", bold: true, children: "Focus mode \u2014 Esc to exit" }) : /* @__PURE__ */ jsxs13(Fragment4, { children: [
3497
+ /* @__PURE__ */ jsx13(Text13, { color: "gray", children: "j/k:nav Tab:section Enter:open Space:select /:search p:pick c:comment m:status a/u:assign s:slack n:new f:focus ?:help q:quit" }),
3498
+ searchQuery && ui.state.mode !== "search" ? /* @__PURE__ */ jsxs13(Text13, { color: "yellow", children: [
3499
+ ' filter: "',
3500
+ searchQuery,
3501
+ '"'
3502
+ ] }) : null
3503
+ ] }) })
3504
+ ] });
3505
+ }
3506
+ var TERMINAL_STATUS_RE2, PRIORITY_RANK, CHROME_ROWS;
3507
+ var init_dashboard = __esm({
3508
+ "src/board/components/dashboard.tsx"() {
3509
+ "use strict";
3510
+ init_use_actions();
3511
+ init_use_data();
3512
+ init_use_multi_select();
3513
+ init_use_navigation();
3514
+ init_use_toast();
3515
+ init_use_ui_state();
3516
+ init_bulk_action_menu();
3517
+ init_comment_input();
3518
+ init_confirm_prompt();
3519
+ init_create_issue_form();
3520
+ init_detail_panel();
3521
+ init_focus_mode();
3522
+ init_help_overlay();
3523
+ init_issue_row();
3524
+ init_search_bar();
3525
+ init_status_picker();
3526
+ init_task_row();
3527
+ init_toast_container();
3528
+ TERMINAL_STATUS_RE2 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
3529
+ PRIORITY_RANK = {
3530
+ "priority:critical": 0,
3531
+ "priority:high": 1,
3532
+ "priority:medium": 2,
3533
+ "priority:low": 3
3534
+ };
3535
+ CHROME_ROWS = 4;
3536
+ }
3537
+ });
3538
+
3539
+ // src/board/live.tsx
3540
+ var live_exports = {};
3541
+ __export(live_exports, {
3542
+ runLiveDashboard: () => runLiveDashboard
3543
+ });
3544
+ import { render } from "ink";
3545
+ import { jsx as jsx14 } from "react/jsx-runtime";
3546
+ async function runLiveDashboard(config2, options, activeProfile) {
3547
+ const { waitUntilExit } = render(
3548
+ /* @__PURE__ */ jsx14(Dashboard, { config: config2, options, activeProfile: activeProfile ?? null })
3549
+ );
3550
+ await waitUntilExit();
3551
+ }
3552
+ var init_live = __esm({
3553
+ "src/board/live.tsx"() {
3554
+ "use strict";
3555
+ init_dashboard();
3556
+ }
3557
+ });
3558
+
3559
+ // src/board/fetch.ts
3560
+ var fetch_exports = {};
3561
+ __export(fetch_exports, {
3562
+ SLACK_URL_RE: () => SLACK_URL_RE2,
3563
+ extractSlackUrl: () => extractSlackUrl,
3564
+ fetchDashboard: () => fetchDashboard,
3565
+ fetchRecentActivity: () => fetchRecentActivity
3566
+ });
3567
+ import { execFileSync as execFileSync4 } from "child_process";
3568
+ function extractSlackUrl(body) {
3569
+ if (!body) return void 0;
3570
+ const match = body.match(SLACK_URL_RE2);
3571
+ return match?.[0];
3572
+ }
3573
+ function formatError2(err) {
3574
+ return err instanceof Error ? err.message : String(err);
3575
+ }
3576
+ function fetchRecentActivity(repoName, shortName) {
3577
+ try {
3578
+ const output = execFileSync4(
3579
+ "gh",
3580
+ [
3581
+ "api",
3582
+ `repos/${repoName}/events`,
3583
+ "--paginate",
3584
+ "-q",
3585
+ '.[] | 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}'
3586
+ ],
3587
+ { encoding: "utf-8", timeout: 15e3 }
3588
+ );
3589
+ const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
3590
+ const events = [];
3591
+ for (const line of output.trim().split("\n")) {
3592
+ if (!line.trim()) continue;
3593
+ try {
3594
+ const ev = JSON.parse(line);
3595
+ const timestamp = new Date(ev.created_at);
3596
+ if (timestamp.getTime() < cutoff) continue;
3597
+ if (!ev.number) continue;
3598
+ let eventType;
3599
+ let summary;
3600
+ if (ev.type === "IssueCommentEvent") {
3601
+ eventType = "comment";
3602
+ const preview = ev.body ? ev.body.slice(0, 60).replace(/\n/g, " ") : "";
3603
+ summary = `commented on #${ev.number}${preview ? ` \u2014 "${preview}${(ev.body?.length ?? 0) > 60 ? "..." : ""}"` : ""}`;
3604
+ } else if (ev.type === "IssuesEvent") {
3605
+ switch (ev.action) {
3606
+ case "opened":
3607
+ eventType = "opened";
3608
+ summary = `opened #${ev.number}: ${ev.title ?? ""}`;
3609
+ break;
3610
+ case "closed":
3611
+ eventType = "closed";
3612
+ summary = `closed #${ev.number}`;
3613
+ break;
3614
+ case "assigned":
3615
+ eventType = "assignment";
3616
+ summary = `assigned #${ev.number}`;
3617
+ break;
3618
+ case "labeled":
3619
+ eventType = "labeled";
3620
+ summary = `labeled #${ev.number}`;
3621
+ break;
3622
+ default:
3623
+ continue;
3624
+ }
3625
+ } else {
3626
+ continue;
3627
+ }
3628
+ events.push({
3629
+ type: eventType,
3630
+ repoShortName: shortName,
3631
+ issueNumber: ev.number,
3632
+ actor: ev.actor,
3633
+ summary,
3634
+ timestamp
3635
+ });
3636
+ } catch {
3637
+ }
3638
+ }
3639
+ return events.slice(0, 15);
3640
+ } catch {
3641
+ return [];
3642
+ }
3643
+ }
3644
+ async function fetchDashboard(config2, options = {}) {
3645
+ const repos = options.repoFilter ? config2.repos.filter(
3646
+ (r) => r.shortName === options.repoFilter || r.name === options.repoFilter
3647
+ ) : config2.repos;
3648
+ const repoData = repos.map((repo) => {
3649
+ try {
3650
+ const fetchOpts = {};
3651
+ if (options.mineOnly) {
3652
+ fetchOpts.assignee = config2.board.assignee;
3653
+ }
3654
+ const issues = fetchRepoIssues(repo.name, fetchOpts);
3655
+ let statusOptions = [];
3656
+ try {
3657
+ const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);
3658
+ for (const issue of issues) {
3659
+ const e = enrichMap.get(issue.number);
3660
+ if (e?.targetDate) issue.targetDate = e.targetDate;
3661
+ if (e?.projectStatus) issue.projectStatus = e.projectStatus;
3662
+ }
3663
+ statusOptions = fetchProjectStatusOptions(
3664
+ repo.name,
3665
+ repo.projectNumber,
3666
+ repo.statusFieldId
3667
+ );
3668
+ } catch {
3669
+ }
3670
+ for (const issue of issues) {
3671
+ const slackUrl = extractSlackUrl(issue.body);
3672
+ if (slackUrl) issue.slackThreadUrl = slackUrl;
3673
+ }
3674
+ return { repo, issues, statusOptions, error: null };
3675
+ } catch (err) {
3676
+ return { repo, issues: [], statusOptions: [], error: formatError2(err) };
3677
+ }
3678
+ });
3679
+ let ticktick = [];
3680
+ let ticktickError = null;
3681
+ if (config2.ticktick.enabled) {
3682
+ try {
3683
+ const auth = requireAuth();
3684
+ const api = new TickTickClient(auth.accessToken);
3685
+ const cfg = getConfig();
3686
+ if (cfg.defaultProjectId) {
3687
+ const tasks = await api.listTasks(cfg.defaultProjectId);
3688
+ ticktick = tasks.filter((t) => t.status !== 2 /* Completed */);
3689
+ }
3690
+ } catch (err) {
3691
+ ticktickError = formatError2(err);
3692
+ }
3693
+ }
3694
+ const activity = [];
3695
+ for (const repo of repos) {
3696
+ const events = fetchRecentActivity(repo.name, repo.shortName);
3697
+ activity.push(...events);
3698
+ }
3699
+ activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
3700
+ return {
3701
+ repos: repoData,
3702
+ ticktick,
3703
+ ticktickError,
3704
+ activity: activity.slice(0, 15),
3705
+ fetchedAt: /* @__PURE__ */ new Date()
3706
+ };
3707
+ }
3708
+ var SLACK_URL_RE2;
3709
+ var init_fetch = __esm({
3710
+ "src/board/fetch.ts"() {
3711
+ "use strict";
3712
+ init_api();
3713
+ init_config();
3714
+ init_github();
3715
+ init_types();
3716
+ SLACK_URL_RE2 = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/i;
3717
+ }
3718
+ });
3719
+
3720
+ // src/board/theme.ts
3721
+ import chalk from "chalk";
3722
+ function getTheme() {
3723
+ return darkTheme;
3724
+ }
3725
+ var darkTheme;
3726
+ var init_theme = __esm({
3727
+ "src/board/theme.ts"() {
3728
+ "use strict";
3729
+ darkTheme = {
3730
+ text: {
3731
+ primary: chalk.white,
3732
+ secondary: chalk.gray,
3733
+ muted: chalk.dim,
3734
+ success: chalk.green,
3735
+ warning: chalk.yellow,
3736
+ error: chalk.red,
3737
+ accent: chalk.cyan
3738
+ },
3739
+ border: {
3740
+ primary: chalk.gray,
3741
+ muted: chalk.dim,
3742
+ focus: chalk.cyan
3743
+ },
3744
+ priority: {
3745
+ high: chalk.red,
3746
+ medium: chalk.yellow,
3747
+ low: chalk.blue,
3748
+ none: chalk.gray
3749
+ },
3750
+ assignee: {
3751
+ self: chalk.greenBright,
3752
+ others: chalk.white,
3753
+ unassigned: chalk.dim
3754
+ }
3755
+ };
3756
+ }
3757
+ });
3758
+
3759
+ // src/board/format-static.ts
3760
+ var format_static_exports = {};
3761
+ __export(format_static_exports, {
3762
+ renderBoardJson: () => renderBoardJson,
3763
+ renderStaticBoard: () => renderStaticBoard
3764
+ });
3765
+ function truncate3(s, max) {
3766
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
3767
+ }
3768
+ function issueAssignee(issue, selfLogin) {
3769
+ const assignees = issue.assignees ?? [];
3770
+ if (assignees.length === 0) return theme.assignee.unassigned("unassigned");
3771
+ const names = assignees.map((a) => a.login);
3772
+ const isSelf = names.includes(selfLogin);
3773
+ const display = names.join(", ");
3774
+ return isSelf ? theme.assignee.self(display) : theme.assignee.others(display);
3775
+ }
3776
+ function formatIssueLine(issue, selfLogin, maxTitle) {
3777
+ const num = theme.text.accent(`#${String(issue.number).padEnd(5)}`);
3778
+ const title = truncate3(issue.title, maxTitle);
3779
+ const assignee = issueAssignee(issue, selfLogin);
3780
+ return ` ${num} ${title.padEnd(maxTitle)} ${assignee}`;
3781
+ }
3782
+ function formatTaskLine(task2, maxTitle) {
3783
+ const pri = task2.priority === 5 /* High */ ? theme.priority.high("[!]") : task2.priority === 3 /* Medium */ ? theme.priority.medium("[~]") : " ";
3784
+ const title = truncate3(task2.title, maxTitle);
3785
+ const due = task2.dueDate ? formatDueDate(task2.dueDate) : "";
3786
+ return ` ${pri} ${title.padEnd(maxTitle)} ${theme.text.secondary(due)}`;
3787
+ }
3788
+ function formatDueDate(dateStr) {
3789
+ const d = new Date(dateStr);
3790
+ const now = /* @__PURE__ */ new Date();
3791
+ const days = Math.ceil((d.getTime() - now.getTime()) / 864e5);
3792
+ if (days < 0) return theme.text.error(`${Math.abs(days)}d overdue`);
3793
+ if (days === 0) return theme.text.warning("today");
3794
+ if (days === 1) return "tomorrow";
3795
+ if (days <= 7) return `in ${days}d`;
3796
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
3797
+ }
3798
+ function printSection2(title, content) {
3799
+ const line = theme.border.primary("\u2500".repeat(Math.max(0, title.length + 4)));
3800
+ console.log(`
3801
+ ${theme.text.primary(title)}`);
3802
+ console.log(line);
3803
+ console.log(content);
3804
+ }
3805
+ function renderRepoSection(data, selfLogin, backlogOnly) {
3806
+ if (data.error) {
3807
+ return ` ${theme.text.error(`Error: ${data.error}`)}`;
3808
+ }
3809
+ if (data.issues.length === 0) {
3810
+ return ` ${theme.text.muted("No open issues")}`;
3811
+ }
3812
+ const assigned = backlogOnly ? [] : data.issues.filter((i) => (i.assignees ?? []).length > 0);
3813
+ const backlog = data.issues.filter((i) => (i.assignees ?? []).length === 0);
3814
+ const lines = [];
3815
+ const maxTitle = 45;
3816
+ if (assigned.length > 0) {
3817
+ lines.push(` ${theme.text.secondary("In Progress")}`);
3818
+ for (const issue of assigned) {
3819
+ lines.push(formatIssueLine(issue, selfLogin, maxTitle));
3820
+ }
3821
+ }
3822
+ if (backlog.length > 0) {
3823
+ if (assigned.length > 0) lines.push("");
3824
+ lines.push(` ${theme.text.secondary("Backlog (unassigned)")}`);
3825
+ for (const issue of backlog) {
3826
+ lines.push(formatIssueLine(issue, selfLogin, maxTitle));
3827
+ }
3828
+ }
3829
+ return lines.join("\n");
3830
+ }
3831
+ function renderTickTickSection(tasks, error) {
3832
+ if (error) {
3833
+ return ` ${theme.text.error(`Error: ${error}`)}`;
3834
+ }
3835
+ if (tasks.length === 0) {
3836
+ return ` ${theme.text.muted("No active tasks")}`;
3837
+ }
3838
+ const maxTitle = 45;
3839
+ const sorted = [...tasks].sort((a, b) => {
3840
+ if (a.dueDate && !b.dueDate) return -1;
3841
+ if (!a.dueDate && b.dueDate) return 1;
3842
+ if (a.dueDate && b.dueDate) return a.dueDate.localeCompare(b.dueDate);
3843
+ return b.priority - a.priority;
3844
+ });
3845
+ return sorted.map((t) => formatTaskLine(t, maxTitle)).join("\n");
3846
+ }
3847
+ function renderStaticBoard(data, selfLogin, backlogOnly) {
3848
+ const now = data.fetchedAt.toLocaleTimeString("en-US", {
3849
+ hour: "2-digit",
3850
+ minute: "2-digit"
3851
+ });
3852
+ const date = data.fetchedAt.toLocaleDateString("en-US", {
3853
+ month: "short",
3854
+ day: "numeric",
3855
+ year: "numeric"
3856
+ });
3857
+ console.log(`
3858
+ ${theme.text.accent("HOG BOARD")} ${theme.text.muted(`\u2014 ${date} ${now}`)}`);
3859
+ for (const rd of data.repos) {
3860
+ const issueCount = rd.issues.length;
3861
+ const label = `${rd.repo.shortName} ${theme.text.muted(`(${issueCount} issues)`)}`;
3862
+ printSection2(label, renderRepoSection(rd, selfLogin, backlogOnly));
3863
+ }
3864
+ if (!backlogOnly) {
3865
+ const taskCount = data.ticktick.length;
3866
+ const dueToday = data.ticktick.filter((t) => {
3867
+ if (!t.dueDate) return false;
3868
+ const days = Math.ceil((new Date(t.dueDate).getTime() - Date.now()) / 864e5);
3869
+ return days <= 0;
3870
+ }).length;
3871
+ const label = dueToday > 0 ? `Personal (TickTick) ${theme.text.warning(`${dueToday} due today`)} / ${taskCount} total` : `Personal (TickTick) ${theme.text.muted(`${taskCount} tasks`)}`;
3872
+ printSection2(label, renderTickTickSection(data.ticktick, data.ticktickError));
3873
+ }
3874
+ console.log("");
3875
+ }
3876
+ function renderBoardJson(data, selfLogin) {
3877
+ return {
3878
+ ok: true,
3879
+ data: {
3880
+ repos: data.repos.map((rd) => ({
3881
+ name: rd.repo.name,
3882
+ shortName: rd.repo.shortName,
3883
+ error: rd.error,
3884
+ issues: rd.issues.map((i) => ({
3885
+ number: i.number,
3886
+ title: i.title,
3887
+ url: i.url,
3888
+ state: i.state,
3889
+ assignee: (i.assignees ?? [])[0]?.login ?? null,
3890
+ assignees: (i.assignees ?? []).map((a) => a.login),
3891
+ labels: i.labels.map((l) => l.name),
3892
+ updatedAt: i.updatedAt,
3893
+ isMine: (i.assignees ?? []).some((a) => a.login === selfLogin)
3894
+ }))
3895
+ })),
3896
+ ticktick: {
3897
+ error: data.ticktickError,
3898
+ tasks: data.ticktick.map((t) => ({
3899
+ id: t.id,
3900
+ title: t.title,
3901
+ priority: t.priority,
3902
+ dueDate: t.dueDate,
3903
+ tags: t.tags
3904
+ }))
3905
+ },
3906
+ fetchedAt: data.fetchedAt.toISOString()
3907
+ }
3908
+ };
3909
+ }
3910
+ var theme;
3911
+ var init_format_static = __esm({
3912
+ "src/board/format-static.ts"() {
3913
+ "use strict";
3914
+ init_types();
3915
+ init_theme();
3916
+ theme = getTheme();
3917
+ }
3918
+ });
3919
+
3920
+ // src/cli.ts
3921
+ init_api();
3922
+ init_config();
3923
+ import { Command } from "commander";
3924
+
3925
+ // src/init.ts
3926
+ import { execFileSync } from "child_process";
3927
+ import { existsSync as existsSync2 } from "fs";
3928
+ import { checkbox, confirm, input, select } from "@inquirer/prompts";
3929
+
3930
+ // src/auth.ts
3931
+ import { createServer } from "http";
3932
+ var AUTH_URL = "https://ticktick.com/oauth/authorize";
3933
+ var TOKEN_URL = "https://ticktick.com/oauth/token";
3934
+ var REDIRECT_URI = "http://localhost:8080";
3935
+ var SCOPE = "tasks:write tasks:read";
3936
+ function getAuthorizationUrl(clientId) {
3937
+ const params = new URLSearchParams({
3938
+ scope: SCOPE,
3939
+ client_id: clientId,
3940
+ state: "hog",
3941
+ redirect_uri: REDIRECT_URI,
3942
+ response_type: "code"
3943
+ });
3944
+ return `${AUTH_URL}?${params}`;
3945
+ }
3946
+ async function waitForAuthCode() {
3947
+ return new Promise((resolve, reject) => {
3948
+ const server = createServer((req, res) => {
3949
+ const url = new URL(req.url ?? "", REDIRECT_URI);
3950
+ const code = url.searchParams.get("code");
3951
+ if (code) {
3952
+ res.writeHead(200, { "Content-Type": "text/html" });
3953
+ res.end(
3954
+ "<html><body><h1>Heart of Gold authenticated!</h1><p>You can close this window.</p></body></html>"
3955
+ );
3956
+ server.close();
3957
+ resolve(code);
3958
+ } else {
3959
+ res.writeHead(400, { "Content-Type": "text/plain" });
3960
+ res.end("Missing authorization code");
3961
+ server.close();
3962
+ reject(new Error("No authorization code received"));
3963
+ }
3964
+ });
3965
+ server.listen(8080, () => {
3966
+ });
3967
+ server.on("error", reject);
3968
+ setTimeout(() => {
3969
+ server.close();
3970
+ reject(new Error("Authorization timed out (2 min)"));
3971
+ }, 12e4);
3972
+ });
3973
+ }
3974
+ async function exchangeCodeForToken(clientId, clientSecret, code) {
3975
+ const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
3976
+ const res = await fetch(TOKEN_URL, {
3977
+ method: "POST",
3978
+ headers: {
3979
+ Authorization: `Basic ${credentials}`,
3980
+ "Content-Type": "application/x-www-form-urlencoded"
3981
+ },
3982
+ body: new URLSearchParams({
3983
+ grant_type: "authorization_code",
3984
+ code,
3985
+ redirect_uri: REDIRECT_URI
3986
+ })
3987
+ });
3988
+ if (!res.ok) {
3989
+ const text = await res.text();
3990
+ throw new Error(`Token exchange failed: ${text}`);
3991
+ }
3992
+ const data = await res.json();
3993
+ return data.access_token;
3994
+ }
3995
+
3996
+ // src/init.ts
3997
+ init_config();
3998
+ function ghJson(args) {
3999
+ const output = execFileSync("gh", args, { encoding: "utf-8", timeout: 3e4 }).trim();
4000
+ return JSON.parse(output);
4001
+ }
4002
+ function isGhAuthenticated() {
4003
+ try {
4004
+ execFileSync("gh", ["auth", "status"], { encoding: "utf-8", timeout: 1e4 });
4005
+ return true;
4006
+ } catch {
4007
+ return false;
4008
+ }
4009
+ }
4010
+ function getGitHubLogin() {
4011
+ const user = ghJson(["api", "user"]);
4012
+ return user.login;
4013
+ }
4014
+ function listRepos() {
4015
+ return ghJson(["repo", "list", "--json", "nameWithOwner,name,owner", "--limit", "100"]);
4016
+ }
4017
+ function listOrgProjects(owner) {
4018
+ try {
4019
+ return ghJson(["project", "list", "--owner", owner, "--format", "json"]);
4020
+ } catch {
4021
+ return [];
4022
+ }
4023
+ }
4024
+ function listProjectFields(owner, projectNumber) {
4025
+ try {
4026
+ return ghJson([
4027
+ "project",
4028
+ "field-list",
4029
+ String(projectNumber),
4030
+ "--owner",
4031
+ owner,
4032
+ "--format",
4033
+ "json"
4034
+ ]);
4035
+ } catch {
4036
+ return [];
4037
+ }
4038
+ }
4039
+ function detectStatusFieldId(owner, projectNumber) {
4040
+ const fields = listProjectFields(owner, projectNumber);
4041
+ const statusField = fields.find(
4042
+ (f) => f.name === "Status" && f.type === "ProjectV2SingleSelectField"
4043
+ );
4044
+ return statusField?.id ?? null;
4045
+ }
4046
+ async function runInit(opts = {}) {
4047
+ try {
4048
+ await runWizard(opts);
4049
+ } catch (error) {
4050
+ if (error instanceof Error && error.message.includes("User force closed")) {
4051
+ console.log("\nSetup cancelled. No changes were made.");
4052
+ return;
4053
+ }
4054
+ throw error;
4055
+ }
4056
+ }
4057
+ async function runWizard(opts) {
4058
+ console.log("\n\u{1F417} hog init \u2014 Setup Wizard\n");
4059
+ const configExists = existsSync2(`${CONFIG_DIR}/config.json`);
4060
+ if (configExists && !opts.force) {
4061
+ const overwrite = await confirm({
4062
+ message: "Config already exists. Overwrite?",
4063
+ default: false
4064
+ });
4065
+ if (!overwrite) {
4066
+ console.log("Setup cancelled.");
4067
+ return;
4068
+ }
4069
+ }
4070
+ console.log("Checking GitHub CLI authentication...");
4071
+ if (!isGhAuthenticated()) {
4072
+ console.error(
4073
+ "\nGitHub CLI is not authenticated. Run:\n\n gh auth login\n\nThen re-run `hog init`."
4074
+ );
4075
+ process.exit(1);
4076
+ }
4077
+ console.log(" GitHub CLI authenticated.\n");
4078
+ const login = getGitHubLogin();
4079
+ console.log(` Detected GitHub user: ${login}
4080
+ `);
4081
+ const allRepos = listRepos();
4082
+ if (allRepos.length === 0) {
4083
+ console.error("No repositories found. Check your GitHub CLI access.");
4084
+ process.exit(1);
4085
+ }
4086
+ const selectedRepoNames = await checkbox({
4087
+ message: "Select repositories to track:",
4088
+ choices: allRepos.map((r) => ({
4089
+ name: r.nameWithOwner,
4090
+ value: r.nameWithOwner
4091
+ }))
4092
+ });
4093
+ if (selectedRepoNames.length === 0) {
4094
+ console.log("No repos selected. You can add repos later with `hog config repos:add`.");
4095
+ }
4096
+ const repos = [];
4097
+ for (const repoName of selectedRepoNames) {
4098
+ console.log(`
4099
+ Configuring ${repoName}...`);
4100
+ const [owner, name] = repoName.split("/");
4101
+ const projects = listOrgProjects(owner);
4102
+ let projectNumber;
4103
+ if (projects.length === 0) {
4104
+ console.log(" No GitHub Projects found. Enter project number manually.");
4105
+ const num = await input({ message: ` Project number for ${repoName}:` });
4106
+ projectNumber = Number.parseInt(num, 10);
4107
+ } else {
4108
+ projectNumber = await select({
4109
+ message: ` Select project for ${repoName}:`,
4110
+ choices: projects.map((p) => ({
4111
+ name: `#${p.number} \u2014 ${p.title}`,
4112
+ value: p.number
4113
+ }))
4114
+ });
4115
+ }
4116
+ console.log(" Detecting status field...");
4117
+ let statusFieldId = detectStatusFieldId(owner, projectNumber);
4118
+ if (statusFieldId) {
4119
+ console.log(` Found status field: ${statusFieldId}`);
4120
+ } else {
4121
+ console.log(" Could not auto-detect status field.");
4122
+ statusFieldId = await input({
4123
+ message: " Enter status field ID manually:"
4124
+ });
4125
+ }
4126
+ const completionType = await select({
4127
+ message: ` When a task is completed in TickTick, what should happen on GitHub?`,
4128
+ choices: [
4129
+ { name: "Close the issue", value: "closeIssue" },
4130
+ { name: "Add a label (e.g. review:pending)", value: "addLabel" },
4131
+ { name: "Update project status column", value: "updateProjectStatus" }
4132
+ ]
4133
+ });
4134
+ let completionAction;
4135
+ if (completionType === "addLabel") {
4136
+ const label = await input({
4137
+ message: " Label to add:",
4138
+ default: "review:pending"
4139
+ });
4140
+ completionAction = { type: "addLabel", label };
4141
+ } else if (completionType === "updateProjectStatus") {
4142
+ const optionId = await input({
4143
+ message: " Status option ID to set:"
4144
+ });
4145
+ completionAction = { type: "updateProjectStatus", optionId };
4146
+ } else {
4147
+ completionAction = { type: "closeIssue" };
4148
+ }
4149
+ const shortName = await input({
4150
+ message: ` Short name for ${repoName}:`,
4151
+ default: name
4152
+ });
4153
+ repos.push({
4154
+ name: repoName,
4155
+ shortName,
4156
+ projectNumber,
4157
+ statusFieldId,
4158
+ completionAction
4159
+ });
4160
+ }
4161
+ const enableTickTick = await confirm({
4162
+ message: "Enable TickTick integration?",
4163
+ default: false
4164
+ });
4165
+ let ticktickAuth = false;
4166
+ if (enableTickTick) {
4167
+ const hasAuth = existsSync2(`${CONFIG_DIR}/auth.json`);
4168
+ if (hasAuth) {
4169
+ console.log(" TickTick auth already configured.");
4170
+ ticktickAuth = true;
4171
+ } else {
4172
+ const setupNow = await confirm({
4173
+ message: " Set up TickTick OAuth now?",
4174
+ default: true
4175
+ });
4176
+ if (setupNow) {
4177
+ const clientId = await input({ message: " TickTick OAuth client ID:" });
4178
+ const clientSecret = await input({ message: " TickTick OAuth client secret:" });
4179
+ const url = getAuthorizationUrl(clientId);
4180
+ console.log(`
4181
+ Open this URL to authorize:
4182
+
4183
+ ${url}
4184
+ `);
4185
+ try {
4186
+ const { exec } = await import("child_process");
4187
+ exec(`open "${url}"`);
4188
+ } catch {
4189
+ }
4190
+ console.log(" Waiting for authorization...");
4191
+ const code = await waitForAuthCode();
4192
+ const accessToken = await exchangeCodeForToken(clientId, clientSecret, code);
4193
+ saveAuth({ accessToken, clientId, clientSecret });
4194
+ console.log(" TickTick authenticated successfully.");
4195
+ ticktickAuth = true;
4196
+ }
4197
+ }
4198
+ }
4199
+ console.log("\nBoard settings:");
4200
+ const refreshInterval = await input({
4201
+ message: " Refresh interval (seconds):",
4202
+ default: "60"
4203
+ });
4204
+ const backlogLimit = await input({
4205
+ message: " Backlog limit (max issues per repo):",
4206
+ default: "20"
4207
+ });
4208
+ const focusDuration = await input({
4209
+ message: " Focus timer duration (seconds):",
4210
+ default: "1500"
4211
+ });
4212
+ const existingConfig = configExists ? loadFullConfig() : void 0;
4213
+ const config2 = {
4214
+ version: 3,
4215
+ defaultProjectId: existingConfig?.defaultProjectId,
4216
+ defaultProjectName: existingConfig?.defaultProjectName,
4217
+ repos,
4218
+ board: {
4219
+ refreshInterval: Number.parseInt(refreshInterval, 10) || 60,
4220
+ backlogLimit: Number.parseInt(backlogLimit, 10) || 20,
4221
+ assignee: login,
4222
+ focusDuration: Number.parseInt(focusDuration, 10) || 1500
4223
+ },
4224
+ ticktick: { enabled: enableTickTick && ticktickAuth },
4225
+ profiles: existingConfig?.profiles ?? {}
4226
+ };
4227
+ saveFullConfig(config2);
4228
+ console.log(`
4229
+ Config written to ${CONFIG_DIR}/config.json`);
4230
+ console.log("\nSetup complete! Try:\n");
4231
+ console.log(" hog board --live # Interactive dashboard");
4232
+ console.log(" hog task list # List TickTick tasks");
4233
+ console.log(" hog config show # View configuration\n");
4234
+ }
4235
+
4236
+ // src/output.ts
4237
+ init_types();
4238
+ var isTTY = process.stdout.isTTY ?? false;
4239
+ var forceFormat = null;
4240
+ function setFormat(format) {
4241
+ forceFormat = format;
4242
+ }
4243
+ function useJson() {
4244
+ if (forceFormat === "json") return true;
4245
+ if (forceFormat === "human") return false;
4246
+ return !isTTY;
4247
+ }
4248
+ function jsonOut(data) {
4249
+ console.log(JSON.stringify(data));
4250
+ }
4251
+ var PRIORITY_LABELS = {
4252
+ [0 /* None */]: "",
4253
+ [1 /* Low */]: "[low]",
4254
+ [3 /* Medium */]: "[med]",
4255
+ [5 /* High */]: "[HIGH]"
4256
+ };
4257
+ function formatDate(dateStr) {
4258
+ if (!dateStr) return "";
4259
+ const d = new Date(dateStr);
4260
+ const now = /* @__PURE__ */ new Date();
4261
+ const days = Math.ceil((d.getTime() - now.getTime()) / 864e5);
4262
+ if (days < 0) return `${Math.abs(days)}d ago`;
4263
+ if (days === 0) return "today";
4264
+ if (days === 1) return "tomorrow";
4265
+ if (days <= 7) return `in ${days}d`;
4266
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
4267
+ }
4268
+ function taskLine(t) {
4269
+ const parts = [];
4270
+ const pri = PRIORITY_LABELS[t.priority] ?? "";
4271
+ if (pri) parts.push(pri);
4272
+ parts.push(t.title);
4273
+ if (t.dueDate) parts.push(` ${formatDate(t.dueDate)}`);
4274
+ if (t.tags.length > 0) parts.push(` #${t.tags.join(" #")}`);
4275
+ return ` ${t.id} ${parts.join(" ")}`;
4276
+ }
4277
+ function printTasks(tasks) {
4278
+ if (useJson()) {
4279
+ jsonOut(tasks);
4280
+ return;
4281
+ }
4282
+ if (tasks.length === 0) {
4283
+ console.log(" No tasks.");
4284
+ return;
4285
+ }
4286
+ for (const t of tasks) {
4287
+ console.log(taskLine(t));
4288
+ }
4289
+ }
4290
+ function printTask(task2) {
4291
+ if (useJson()) {
4292
+ jsonOut(task2);
4293
+ return;
4294
+ }
4295
+ console.log(` ID: ${task2.id}`);
4296
+ console.log(` Title: ${task2.title}`);
4297
+ if (task2.content) console.log(` Content: ${task2.content}`);
4298
+ console.log(` Priority: ${PRIORITY_LABELS[task2.priority] ?? "none"}`);
4299
+ if (task2.dueDate) console.log(` Due: ${formatDate(task2.dueDate)}`);
4300
+ if (task2.startDate) console.log(` Start: ${formatDate(task2.startDate)}`);
4301
+ if (task2.tags.length > 0) console.log(` Tags: ${task2.tags.join(", ")}`);
4302
+ console.log(` Project: ${task2.projectId}`);
4303
+ console.log(` Status: ${task2.status === 2 ? "completed" : "active"}`);
4304
+ }
4305
+ function printProjects(projects) {
4306
+ if (useJson()) {
4307
+ jsonOut(projects);
4308
+ return;
4309
+ }
4310
+ if (projects.length === 0) {
4311
+ console.log(" No projects.");
4312
+ return;
4313
+ }
4314
+ for (const p of projects) {
4315
+ const closed = p.closed ? " (closed)" : "";
4316
+ console.log(` ${p.id} ${p.name}${closed}`);
4317
+ }
4318
+ }
4319
+ function printSuccess(message, data) {
4320
+ if (useJson()) {
4321
+ jsonOut({ ok: true, message, ...data });
4322
+ return;
4323
+ }
4324
+ console.log(message);
4325
+ }
4326
+ function printSection(prefix, label, icon, items) {
4327
+ if (items.length === 0) return;
4328
+ console.log(`${prefix}${label} ${items.length} task(s):`);
4329
+ for (const key of items) console.log(` ${icon} ${key}`);
4330
+ }
4331
+ function printSyncResult(result, dryRun) {
4332
+ if (useJson()) {
4333
+ jsonOut({ ok: true, dryRun, ...result });
4334
+ return;
4335
+ }
4336
+ const prefix = dryRun ? "[dry-run] " : "";
4337
+ printSection(prefix, "Created", "+", result.created);
4338
+ printSection(prefix, "Updated", "~", result.updated);
4339
+ printSection(prefix, "Completed", "\u2713", result.completed);
4340
+ printSection(prefix, "GitHub updated", "\u2192", result.ghUpdated);
4341
+ printSection("", "Errors", "\u2717", result.errors);
4342
+ const total = result.created.length + result.updated.length + result.completed.length + result.ghUpdated.length;
4343
+ if (total === 0 && result.errors.length === 0) {
4344
+ console.log(`${prefix}Everything in sync.`);
4345
+ }
4346
+ }
4347
+ function printSyncStatus(state, repos) {
4348
+ if (useJson()) {
4349
+ jsonOut({ repos, lastSyncAt: state.lastSyncAt ?? null, mappings: state.mappings });
4350
+ return;
4351
+ }
4352
+ console.log(` Repos: ${repos.join(", ")}`);
4353
+ console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
4354
+ console.log(` Active mappings: ${state.mappings.length}`);
4355
+ if (state.mappings.length > 0) {
4356
+ for (const m of state.mappings) {
4357
+ console.log(` ${m.githubRepo}#${m.githubIssueNumber} \u2192 ${m.ticktickTaskId}`);
4358
+ }
4359
+ }
4360
+ }
4361
+
4362
+ // src/sync.ts
4363
+ init_api();
4364
+ init_config();
4365
+ init_github();
4366
+ init_sync_state();
4367
+ init_types();
4368
+ function emptySyncResult() {
4369
+ return { created: [], updated: [], completed: [], ghUpdated: [], errors: [] };
4370
+ }
4371
+ function formatError(err) {
4372
+ return err instanceof Error ? err.message : String(err);
4373
+ }
4374
+ function repoShortName(repo) {
4375
+ return repo.split("/")[1] ?? repo;
4376
+ }
4377
+ function issueTaskTitle(issue) {
4378
+ return issue.title;
4379
+ }
4380
+ function issueTaskContent(issue, projectFields) {
4381
+ const lines = [`GitHub: ${issue.url}`];
4382
+ if (projectFields.status) lines.push(`Status: ${projectFields.status}`);
4383
+ return lines.join("\n");
4384
+ }
4385
+ function mapPriority(labels) {
4386
+ for (const label of labels) {
4387
+ if (label.name === "priority:critical" || label.name === "priority:high") return 5 /* High */;
4388
+ if (label.name === "priority:medium") return 3 /* Medium */;
4389
+ if (label.name === "priority:low") return 1 /* Low */;
4390
+ }
4391
+ return 0 /* None */;
4392
+ }
4393
+ function buildCreateInput(repo, issue, projectFields) {
4394
+ const input2 = {
4395
+ title: issueTaskTitle(issue),
4396
+ content: issueTaskContent(issue, projectFields),
4397
+ priority: mapPriority(issue.labels),
4398
+ tags: ["github", repoShortName(repo)]
4399
+ };
4400
+ if (projectFields.targetDate) {
4401
+ input2.dueDate = projectFields.targetDate;
4402
+ input2.isAllDay = true;
4403
+ }
4404
+ return input2;
4405
+ }
4406
+ function buildUpdateInput(repo, issue, projectFields, mapping) {
4407
+ const input2 = {
4408
+ id: mapping.ticktickTaskId,
4409
+ projectId: mapping.ticktickProjectId,
4410
+ title: issueTaskTitle(issue),
4411
+ content: issueTaskContent(issue, projectFields),
4412
+ priority: mapPriority(issue.labels),
4413
+ tags: ["github", repoShortName(repo)]
4414
+ };
4415
+ if (projectFields.targetDate) {
4416
+ input2.dueDate = projectFields.targetDate;
4417
+ }
4418
+ return input2;
4419
+ }
4420
+ async function syncGitHubToTickTick(config2, state, api, result, dryRun) {
4421
+ const openIssueKeys = /* @__PURE__ */ new Set();
4422
+ const failedRepos = /* @__PURE__ */ new Set();
4423
+ for (const repoConfig of config2.repos) {
4424
+ let issues;
4425
+ try {
4426
+ issues = fetchAssignedIssues(repoConfig.name, config2.board.assignee);
4427
+ } catch (err) {
4428
+ result.errors.push(`Failed to fetch issues from ${repoConfig.name}: ${formatError(err)}`);
4429
+ failedRepos.add(repoConfig.name);
4430
+ continue;
4431
+ }
4432
+ for (const issue of issues) {
4433
+ const key = `${repoConfig.name}#${issue.number}`;
4434
+ openIssueKeys.add(key);
4435
+ await syncSingleIssue(state, api, result, dryRun, repoConfig, issue, key);
4436
+ }
4437
+ }
4438
+ return { openIssueKeys, failedRepos };
4439
+ }
4440
+ async function syncSingleIssue(state, api, result, dryRun, repoConfig, issue, key) {
4441
+ try {
4442
+ const existing = findMapping(state, repoConfig.name, issue.number);
4443
+ if (existing && existing.githubUpdatedAt === issue.updatedAt) return;
4444
+ const projectFields = fetchProjectFields(
4445
+ repoConfig.name,
4446
+ issue.number,
4447
+ repoConfig.projectNumber
4448
+ );
4449
+ if (!existing) {
4450
+ await createTickTickTask(
4451
+ state,
4452
+ api,
4453
+ result,
4454
+ dryRun,
4455
+ repoConfig.name,
4456
+ issue,
4457
+ projectFields,
4458
+ key
4459
+ );
4460
+ } else {
4461
+ await updateTickTickTask(
4462
+ state,
4463
+ api,
4464
+ result,
4465
+ dryRun,
4466
+ repoConfig.name,
4467
+ issue,
4468
+ projectFields,
4469
+ existing,
4470
+ key
4471
+ );
4472
+ }
4473
+ } catch (err) {
4474
+ result.errors.push(`${key}: ${formatError(err)}`);
4475
+ }
4476
+ }
4477
+ async function createTickTickTask(state, api, result, dryRun, repo, issue, projectFields, key) {
4478
+ if (dryRun) {
4479
+ result.created.push(key);
4480
+ return;
4481
+ }
4482
+ const input2 = buildCreateInput(repo, issue, projectFields);
4483
+ const task2 = await api.createTask(input2);
4484
+ upsertMapping(state, {
4485
+ githubRepo: repo,
4486
+ githubIssueNumber: issue.number,
4487
+ githubUrl: issue.url,
4488
+ ticktickTaskId: task2.id,
4489
+ ticktickProjectId: task2.projectId,
4490
+ githubUpdatedAt: issue.updatedAt,
4491
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
4492
+ });
4493
+ result.created.push(key);
4494
+ }
4495
+ async function updateTickTickTask(state, api, result, dryRun, repo, issue, projectFields, existing, key) {
4496
+ if (dryRun) {
4497
+ result.updated.push(key);
4498
+ return;
4499
+ }
4500
+ const input2 = buildUpdateInput(repo, issue, projectFields, existing);
4501
+ await api.updateTask(input2);
4502
+ upsertMapping(state, {
4503
+ ...existing,
4504
+ githubUpdatedAt: issue.updatedAt,
4505
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
4506
+ });
4507
+ result.updated.push(key);
4508
+ }
4509
+ async function syncClosedIssues(state, api, result, dryRun, openIssueKeys, failedRepos) {
4510
+ for (const mapping of [...state.mappings]) {
4511
+ if (failedRepos.has(mapping.githubRepo)) continue;
4512
+ const key = `${mapping.githubRepo}#${mapping.githubIssueNumber}`;
4513
+ if (openIssueKeys.has(key)) continue;
4514
+ try {
4515
+ if (dryRun) {
4516
+ result.completed.push(key);
4517
+ continue;
4518
+ }
4519
+ await api.completeTask(mapping.ticktickProjectId, mapping.ticktickTaskId);
4520
+ removeMapping(state, mapping.githubRepo, mapping.githubIssueNumber);
4521
+ result.completed.push(key);
4522
+ } catch (err) {
4523
+ result.errors.push(`Complete ${key}: ${formatError(err)}`);
4524
+ }
4525
+ }
4526
+ }
4527
+ async function syncCompletedTasksToGitHub(config2, state, api, result, dryRun) {
4528
+ for (const mapping of [...state.mappings]) {
4529
+ const key = `${mapping.githubRepo}#${mapping.githubIssueNumber}`;
4530
+ try {
4531
+ await processCompletedMapping(config2, state, api, result, dryRun, mapping, key);
4532
+ } catch (err) {
4533
+ result.errors.push(`GH update ${key}: ${formatError(err)}`);
4534
+ }
4535
+ }
4536
+ }
4537
+ async function processCompletedMapping(config2, state, api, result, dryRun, mapping, key) {
4538
+ let task2;
4539
+ try {
4540
+ task2 = await api.getTask(mapping.ticktickProjectId, mapping.ticktickTaskId);
4541
+ } catch {
4542
+ return;
4543
+ }
4544
+ if (task2.status !== 2 /* Completed */) return;
4545
+ if (dryRun) {
4546
+ result.ghUpdated.push(key);
4547
+ return;
4548
+ }
4549
+ const repo = mapping.githubRepo;
4550
+ const repoConfig = config2.repos.find((r) => r.name === repo);
4551
+ if (repoConfig) {
4552
+ const action = repoConfig.completionAction;
4553
+ switch (action.type) {
4554
+ case "addLabel":
4555
+ addLabel(repo, mapping.githubIssueNumber, action.label);
4556
+ break;
4557
+ case "updateProjectStatus":
4558
+ updateProjectItemStatus(repo, mapping.githubIssueNumber, {
4559
+ projectNumber: repoConfig.projectNumber,
4560
+ statusFieldId: repoConfig.statusFieldId,
4561
+ optionId: action.optionId
4562
+ });
4563
+ break;
4564
+ case "closeIssue":
4565
+ break;
4566
+ }
4567
+ }
4568
+ removeMapping(state, repo, mapping.githubIssueNumber);
4569
+ result.ghUpdated.push(key);
4570
+ }
4571
+ async function runSync(options = {}) {
4572
+ const { dryRun = false } = options;
4573
+ const result = emptySyncResult();
4574
+ const config2 = loadFullConfig();
4575
+ const auth = requireAuth();
4576
+ const api = new TickTickClient(auth.accessToken);
4577
+ const state = loadSyncState();
4578
+ const { openIssueKeys, failedRepos } = await syncGitHubToTickTick(
4579
+ config2,
4580
+ state,
4581
+ api,
4582
+ result,
4583
+ dryRun
4584
+ );
4585
+ await syncClosedIssues(state, api, result, dryRun, openIssueKeys, failedRepos);
4586
+ await syncCompletedTasksToGitHub(config2, state, api, result, dryRun);
4587
+ if (!dryRun) {
4588
+ state.lastSyncAt = (/* @__PURE__ */ new Date()).toISOString();
4589
+ saveSyncState(state);
4590
+ }
4591
+ return result;
4592
+ }
4593
+ function getSyncStatus() {
4594
+ const config2 = loadFullConfig();
4595
+ return { state: loadSyncState(), repos: config2.repos.map((r) => r.name) };
4596
+ }
4597
+
4598
+ // src/cli.ts
4599
+ init_types();
4600
+ var major = Number(process.versions.node.split(".")[0]);
4601
+ if (major < 22) {
4602
+ console.error(
4603
+ `hog requires Node.js >= 22 (current: ${process.version}). Install from https://nodejs.org/`
4604
+ );
4605
+ process.exit(1);
4606
+ }
4607
+ var PRIORITY_MAP = {
4608
+ none: 0 /* None */,
4609
+ low: 1 /* Low */,
4610
+ medium: 3 /* Medium */,
4611
+ med: 3 /* Medium */,
4612
+ high: 5 /* High */
4613
+ };
4614
+ function parsePriority(value) {
4615
+ const p = PRIORITY_MAP[value.toLowerCase()];
4616
+ if (p === void 0) {
4617
+ console.error(`Invalid priority: ${value}. Use: none, low, medium, high`);
4618
+ process.exit(1);
4619
+ }
4620
+ return p;
4621
+ }
4622
+ function createClient() {
4623
+ const auth = requireAuth();
4624
+ return new TickTickClient(auth.accessToken);
4625
+ }
4626
+ function resolveProjectId(projectId) {
4627
+ if (projectId) return projectId;
4628
+ const config2 = getConfig();
4629
+ if (config2.defaultProjectId) return config2.defaultProjectId;
4630
+ console.error("No project selected. Run `hog task use-project <id>` or pass --project.");
4631
+ process.exit(1);
4632
+ }
4633
+ var program = new Command();
4634
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.1.1").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
4635
+ const opts = thisCommand.opts();
4636
+ if (opts.json) setFormat("json");
4637
+ if (opts.human) setFormat("human");
4638
+ });
4639
+ program.command("init").description("Interactive setup wizard").option("--force", "Overwrite existing config without prompt").action(async (opts) => {
4640
+ await runInit({ force: opts.force ?? false });
4641
+ });
4642
+ var task = program.command("task").description("Manage TickTick tasks");
4643
+ task.command("add <title>").description("Create a new task").option("-p, --priority <level>", "Priority: none, low, medium, high").option("-d, --date <date>", "Due date (ISO 8601)").option("--start <date>", "Start date (ISO 8601)").option("-c, --content <text>", "Task description/content").option("-t, --tags <tags>", "Comma-separated tags").option("--all-day", "Mark as all-day task").option("--project <id>", "Project ID (overrides default)").action(async (title, opts) => {
4644
+ const api = createClient();
4645
+ const input2 = {
4646
+ title,
4647
+ projectId: resolveProjectId(opts.project)
4648
+ };
4649
+ if (opts.priority) input2.priority = parsePriority(opts.priority);
4650
+ if (opts.date) input2.dueDate = opts.date;
4651
+ if (opts.start) input2.startDate = opts.start;
4652
+ if (opts.content) input2.content = opts.content;
4653
+ if (opts.tags) input2.tags = opts.tags.split(",").map((t) => t.trim());
4654
+ if (opts.allDay) input2.isAllDay = true;
4655
+ const created = await api.createTask(input2);
4656
+ printSuccess(`Created: ${created.title}`, {
4657
+ task: created
4658
+ });
4659
+ });
4660
+ task.command("list").description("List tasks in a project").option("--project <id>", "Project ID (overrides default)").option("--all", "Include completed tasks").option("-p, --priority <level>", "Filter by minimum priority").option("-t, --tag <tag>", "Filter by tag").action(async (opts) => {
4661
+ const api = createClient();
4662
+ const projectId = resolveProjectId(opts.project);
4663
+ let tasks = await api.listTasks(projectId);
4664
+ if (!opts.all) {
4665
+ tasks = tasks.filter((t) => t.status !== 2);
4666
+ }
4667
+ if (opts.priority) {
4668
+ const minPri = parsePriority(opts.priority);
4669
+ tasks = tasks.filter((t) => t.priority >= minPri);
4670
+ }
4671
+ if (opts.tag) {
4672
+ const tag = opts.tag;
4673
+ tasks = tasks.filter((t) => t.tags.includes(tag));
4674
+ }
4675
+ printTasks(tasks);
4676
+ });
4677
+ task.command("show <taskId>").description("Show task details").option("--project <id>", "Project ID (overrides default)").action(async (taskId, opts) => {
4678
+ const api = createClient();
4679
+ const projectId = resolveProjectId(opts.project);
4680
+ const t = await api.getTask(projectId, taskId);
4681
+ printTask(t);
4682
+ });
4683
+ task.command("complete <taskId>").description("Mark a task as completed").option("--project <id>", "Project ID (overrides default)").action(async (taskId, opts) => {
4684
+ const api = createClient();
4685
+ const projectId = resolveProjectId(opts.project);
4686
+ await api.completeTask(projectId, taskId);
4687
+ printSuccess(`Completed task ${taskId}`, { taskId });
4688
+ });
4689
+ task.command("update <taskId>").description("Update a task").option("--title <title>", "New title").option("-p, --priority <level>", "New priority").option("-d, --date <date>", "New due date (ISO 8601)").option("-c, --content <text>", "New content").option("-t, --tags <tags>", "New comma-separated tags").option("--project <id>", "Project ID (overrides default)").action(async (taskId, opts) => {
4690
+ const api = createClient();
4691
+ const projectId = resolveProjectId(opts.project);
4692
+ const input2 = { id: taskId, projectId };
4693
+ if (opts.title) input2.title = opts.title;
4694
+ if (opts.priority) input2.priority = parsePriority(opts.priority);
4695
+ if (opts.date) input2.dueDate = opts.date;
4696
+ if (opts.content) input2.content = opts.content;
4697
+ if (opts.tags) input2.tags = opts.tags.split(",").map((t) => t.trim());
4698
+ const updated = await api.updateTask(input2);
4699
+ printSuccess(`Updated: ${updated.title}`, {
4700
+ task: updated
4701
+ });
4702
+ });
4703
+ task.command("delete <taskId>").description("Delete a task").option("--project <id>", "Project ID (overrides default)").action(async (taskId, opts) => {
4704
+ const api = createClient();
4705
+ const projectId = resolveProjectId(opts.project);
4706
+ await api.deleteTask(projectId, taskId);
4707
+ printSuccess(`Deleted task ${taskId}`, { taskId });
4708
+ });
4709
+ task.command("projects").description("List all projects").action(async () => {
4710
+ const api = createClient();
4711
+ const projects = await api.listProjects();
4712
+ printProjects(projects);
4713
+ });
4714
+ task.command("use-project <projectId>").description("Set the default project for task commands").action(async (projectId) => {
4715
+ const api = createClient();
4716
+ try {
4717
+ const project = await api.getProject(projectId);
4718
+ saveConfig({ defaultProjectId: project.id, defaultProjectName: project.name });
4719
+ printSuccess(`Default project: ${project.name} (${project.id})`, {
4720
+ projectId: project.id,
4721
+ projectName: project.name
4722
+ });
4723
+ } catch {
4724
+ saveConfig({ defaultProjectId: projectId });
4725
+ printSuccess(`Default project: ${projectId}`, { projectId });
4726
+ }
4727
+ });
4728
+ var sync = program.command("sync").description("Sync GitHub issues with TickTick");
4729
+ sync.command("run", { isDefault: true }).description("Run GitHub-TickTick sync").option("--dry-run", "Preview changes without applying them").action(async (opts) => {
4730
+ const dryRun = opts.dryRun ?? false;
4731
+ const result = await runSync({ dryRun });
4732
+ printSyncResult(result, dryRun);
4733
+ });
4734
+ sync.command("status").description("Show sync status and mappings").action(() => {
4735
+ const { state, repos } = getSyncStatus();
4736
+ printSyncStatus(state, repos);
4737
+ });
4738
+ program.command("board").description("Show unified task dashboard").option("--repo <name>", "Filter by repo (short name or full)").option("--mine", "Show only my assigned issues and tasks").option("--backlog", "Show only unassigned issues").option("--live", "Persistent TUI with auto-refresh and keyboard navigation").option("--profile <name>", "Use a named board profile").action(async (opts) => {
4739
+ const rawCfg = loadFullConfig();
4740
+ const { resolved: cfg, activeProfile } = resolveProfile(rawCfg, opts.profile);
4741
+ const jsonMode = useJson();
4742
+ const fetchOptions = {
4743
+ repoFilter: opts.repo,
4744
+ mineOnly: opts.mine ?? false,
4745
+ backlogOnly: opts.backlog ?? false
4746
+ };
4747
+ if (opts.live) {
4748
+ const { runLiveDashboard: runLiveDashboard2 } = await Promise.resolve().then(() => (init_live(), live_exports));
4749
+ await runLiveDashboard2(cfg, fetchOptions, activeProfile);
4750
+ return;
4751
+ }
4752
+ const { fetchDashboard: fetchDashboard2 } = await Promise.resolve().then(() => (init_fetch(), fetch_exports));
4753
+ const data = await fetchDashboard2(cfg, fetchOptions);
4754
+ if (jsonMode) {
4755
+ const { renderBoardJson: renderBoardJson2 } = await Promise.resolve().then(() => (init_format_static(), format_static_exports));
4756
+ jsonOut(renderBoardJson2(data, cfg.board.assignee));
4757
+ } else {
4758
+ const { renderStaticBoard: renderStaticBoard2 } = await Promise.resolve().then(() => (init_format_static(), format_static_exports));
4759
+ renderStaticBoard2(data, cfg.board.assignee, opts.backlog ?? false);
4760
+ }
4761
+ });
4762
+ program.command("pick <issueRef>").description("Pick up an issue: assign to self + sync to TickTick (e.g., hog pick aibility/145)").action(async (issueRef) => {
4763
+ const cfg = loadFullConfig();
4764
+ const { parseIssueRef: parseIssueRef2, pickIssue: pickIssue2 } = await Promise.resolve().then(() => (init_pick(), pick_exports));
4765
+ const ref = parseIssueRef2(issueRef, cfg);
4766
+ const result = await pickIssue2(cfg, ref);
4767
+ if (useJson()) {
4768
+ jsonOut({
4769
+ ok: result.success,
4770
+ data: {
4771
+ issue: result.issue,
4772
+ ticktickTask: result.ticktickTask ?? null,
4773
+ warning: result.warning ?? null
4774
+ }
4775
+ });
4776
+ } else {
4777
+ console.log(`Picked ${ref.repo.shortName}#${ref.issueNumber}: ${result.issue.title}`);
4778
+ console.log(` GitHub: assigned to @me`);
4779
+ if (result.ticktickTask) {
4780
+ console.log(` TickTick: task created`);
4781
+ }
4782
+ if (result.warning) {
4783
+ console.log(` Warning: ${result.warning}`);
4784
+ }
4785
+ }
4786
+ });
4787
+ var config = program.command("config").description("Manage hog configuration");
4788
+ config.command("show").description("Show full configuration").action(() => {
4789
+ const cfg = loadFullConfig();
4790
+ if (useJson()) {
4791
+ jsonOut({ ok: true, data: cfg });
4792
+ } else {
4793
+ console.log("Version:", cfg.version);
4794
+ console.log("Default project:", cfg.defaultProjectId ?? "(none)");
4795
+ console.log("Assignee:", cfg.board.assignee);
4796
+ console.log("Refresh interval:", `${cfg.board.refreshInterval}s`);
4797
+ console.log("Backlog limit:", cfg.board.backlogLimit);
4798
+ console.log("TickTick:", cfg.ticktick.enabled ? "enabled" : "disabled");
4799
+ console.log("\nRepos:");
4800
+ for (const repo of cfg.repos) {
4801
+ console.log(` ${repo.shortName} \u2192 ${repo.name} (project #${repo.projectNumber})`);
4802
+ console.log(` completion: ${repo.completionAction.type}`);
4803
+ }
4804
+ }
4805
+ });
4806
+ config.command("repos").description("List configured repositories").action(() => {
4807
+ const cfg = loadFullConfig();
4808
+ if (useJson()) {
4809
+ jsonOut({ ok: true, data: cfg.repos });
4810
+ } else {
4811
+ if (cfg.repos.length === 0) {
4812
+ console.log("No repos configured. Run: hog config repos add <owner/repo>");
4813
+ return;
4814
+ }
4815
+ for (const repo of cfg.repos) {
4816
+ console.log(` ${repo.shortName.padEnd(15)} ${repo.name}`);
4817
+ }
4818
+ }
4819
+ });
4820
+ config.command("repos:add <name>").description("Add a repository to track (owner/repo format)").requiredOption("--project-number <n>", "GitHub project number").requiredOption("--status-field-id <id>", "Project status field ID").requiredOption(
4821
+ "--completion-type <type>",
4822
+ "Completion action: addLabel, updateProjectStatus, closeIssue"
4823
+ ).option("--completion-option-id <id>", "Option ID for updateProjectStatus").option("--completion-label <label>", "Label for addLabel").action((name, opts) => {
4824
+ if (!validateRepoName(name)) {
4825
+ console.error("Invalid repo name. Use owner/repo format (e.g., myorg/myrepo)");
4826
+ process.exit(1);
4827
+ }
4828
+ const cfg = loadFullConfig();
4829
+ if (findRepo(cfg, name)) {
4830
+ console.error(`Repo "${name}" is already configured.`);
4831
+ process.exit(1);
4832
+ }
4833
+ const shortName = name.split("/")[1] ?? name;
4834
+ let completionAction;
4835
+ switch (opts.completionType) {
4836
+ case "addLabel":
4837
+ if (!opts.completionLabel) {
4838
+ console.error("--completion-label required for addLabel type");
4839
+ process.exit(1);
4840
+ }
4841
+ completionAction = { type: "addLabel", label: opts.completionLabel };
4842
+ break;
4843
+ case "updateProjectStatus":
4844
+ if (!opts.completionOptionId) {
4845
+ console.error("--completion-option-id required for updateProjectStatus type");
4846
+ process.exit(1);
4847
+ }
4848
+ completionAction = { type: "updateProjectStatus", optionId: opts.completionOptionId };
4849
+ break;
4850
+ case "closeIssue":
4851
+ completionAction = { type: "closeIssue" };
4852
+ break;
4853
+ default:
4854
+ console.error(
4855
+ `Unknown completion type: ${opts.completionType}. Use: addLabel, updateProjectStatus, closeIssue`
4856
+ );
4857
+ process.exit(1);
4858
+ }
4859
+ const newRepo = {
4860
+ name,
4861
+ shortName,
4862
+ projectNumber: Number.parseInt(opts.projectNumber, 10),
4863
+ statusFieldId: opts.statusFieldId,
4864
+ completionAction
4865
+ };
4866
+ cfg.repos.push(newRepo);
4867
+ saveFullConfig(cfg);
4868
+ if (useJson()) {
4869
+ jsonOut({ ok: true, message: `Added ${name}`, data: newRepo });
4870
+ } else {
4871
+ console.log(`Added ${shortName} \u2192 ${name}`);
4872
+ }
4873
+ });
4874
+ config.command("repos:rm <name>").description("Remove a repository from tracking").action((name) => {
4875
+ const cfg = loadFullConfig();
4876
+ const idx = cfg.repos.findIndex((r) => r.shortName === name || r.name === name);
4877
+ if (idx === -1) {
4878
+ console.error(`Repo "${name}" not found. Run: hog config repos`);
4879
+ process.exit(1);
4880
+ }
4881
+ const [removed] = cfg.repos.splice(idx, 1);
4882
+ if (!removed) {
4883
+ process.exit(1);
4884
+ }
4885
+ saveFullConfig(cfg);
4886
+ if (useJson()) {
4887
+ jsonOut({ ok: true, message: `Removed ${removed.name}`, data: removed });
4888
+ } else {
4889
+ console.log(`Removed ${removed.shortName} \u2192 ${removed.name}`);
4890
+ console.log("Note: Existing sync mappings for this repo remain in sync-state.json.");
4891
+ }
4892
+ });
4893
+ config.command("ticktick:enable").description("Enable TickTick integration in the board").action(() => {
4894
+ const cfg = loadFullConfig();
4895
+ cfg.ticktick = { enabled: true };
4896
+ saveFullConfig(cfg);
4897
+ if (useJson()) {
4898
+ jsonOut({ ok: true, message: "TickTick enabled" });
4899
+ } else {
4900
+ printSuccess("TickTick integration enabled.");
4901
+ }
4902
+ });
4903
+ config.command("ticktick:disable").description("Disable TickTick integration in the board").action(() => {
4904
+ const cfg = loadFullConfig();
4905
+ cfg.ticktick = { enabled: false };
4906
+ saveFullConfig(cfg);
4907
+ if (useJson()) {
4908
+ jsonOut({ ok: true, message: "TickTick disabled" });
4909
+ } else {
4910
+ printSuccess("TickTick integration disabled. Board will no longer show TickTick tasks.");
4911
+ }
4912
+ });
4913
+ config.command("profile:create <name>").description("Create a board profile (copies current top-level config)").action((name) => {
4914
+ const cfg = loadFullConfig();
4915
+ if (cfg.profiles[name]) {
4916
+ console.error(`Profile "${name}" already exists.`);
4917
+ process.exit(1);
4918
+ }
4919
+ cfg.profiles[name] = {
4920
+ repos: [...cfg.repos],
4921
+ board: { ...cfg.board },
4922
+ ticktick: { ...cfg.ticktick }
4923
+ };
4924
+ saveFullConfig(cfg);
4925
+ if (useJson()) {
4926
+ jsonOut({ ok: true, message: `Created profile "${name}"`, data: cfg.profiles[name] });
4927
+ } else {
4928
+ printSuccess(`Created profile "${name}" (copied from current config).`);
4929
+ }
4930
+ });
4931
+ config.command("profile:delete <name>").description("Delete a board profile").action((name) => {
4932
+ const cfg = loadFullConfig();
4933
+ if (!cfg.profiles[name]) {
4934
+ console.error(
4935
+ `Profile "${name}" not found. Available: ${Object.keys(cfg.profiles).join(", ") || "(none)"}`
4936
+ );
4937
+ process.exit(1);
4938
+ }
4939
+ delete cfg.profiles[name];
4940
+ if (cfg.defaultProfile === name) {
4941
+ cfg.defaultProfile = void 0;
4942
+ }
4943
+ saveFullConfig(cfg);
4944
+ if (useJson()) {
4945
+ jsonOut({ ok: true, message: `Deleted profile "${name}"` });
4946
+ } else {
4947
+ printSuccess(`Deleted profile "${name}".`);
4948
+ }
4949
+ });
4950
+ config.command("profile:default [name]").description("Set or show the default board profile").action((name) => {
4951
+ const cfg = loadFullConfig();
4952
+ if (!name) {
4953
+ if (useJson()) {
4954
+ jsonOut({
4955
+ ok: true,
4956
+ data: { defaultProfile: cfg.defaultProfile ?? null, profiles: Object.keys(cfg.profiles) }
4957
+ });
4958
+ } else {
4959
+ console.log("Default profile:", cfg.defaultProfile ?? "(none)");
4960
+ const names = Object.keys(cfg.profiles);
4961
+ if (names.length > 0) {
4962
+ console.log("Available profiles:", names.join(", "));
4963
+ } else {
4964
+ console.log("No profiles configured. Run: hog config profile:create <name>");
4965
+ }
4966
+ }
4967
+ return;
4968
+ }
4969
+ if (!cfg.profiles[name]) {
4970
+ console.error(
4971
+ `Profile "${name}" not found. Available: ${Object.keys(cfg.profiles).join(", ") || "(none)"}`
4972
+ );
4973
+ process.exit(1);
4974
+ }
4975
+ cfg.defaultProfile = name;
4976
+ saveFullConfig(cfg);
4977
+ if (useJson()) {
4978
+ jsonOut({ ok: true, message: `Default profile set to "${name}"` });
4979
+ } else {
4980
+ printSuccess(`Default profile set to "${name}".`);
4981
+ }
4982
+ });
4983
+ program.parseAsync().catch((err) => {
4984
+ const message = err instanceof Error ? err.message : String(err);
4985
+ console.error(`Error: ${message}`);
4986
+ process.exit(1);
4987
+ });
4988
+ //# sourceMappingURL=cli.js.map