@ondrej-svec/hog 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ function migrateConfig(raw) {
20
20
  raw = {
21
21
  ...raw,
22
22
  version: 2,
23
- repos: LEGACY_REPOS,
23
+ repos: [],
24
24
  board: {
25
25
  refreshInterval: 60,
26
26
  backlogLimit: 20,
@@ -135,7 +135,9 @@ function saveConfig(data) {
135
135
  ensureDir();
136
136
  const existing = getConfig();
137
137
  writeFileSync(CONFIG_FILE, `${JSON.stringify({ ...existing, ...data }, null, 2)}
138
- `);
138
+ `, {
139
+ mode: 384
140
+ });
139
141
  }
140
142
  function requireAuth() {
141
143
  const auth = getAuth();
@@ -145,7 +147,7 @@ function requireAuth() {
145
147
  }
146
148
  return auth;
147
149
  }
148
- 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;
150
+ 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;
149
151
  var init_config = __esm({
150
152
  "src/config.ts"() {
151
153
  "use strict";
@@ -191,7 +193,6 @@ var init_config = __esm({
191
193
  profiles: z.record(z.string(), PROFILE_SCHEMA).default({}),
192
194
  defaultProfile: z.string().optional()
193
195
  });
194
- LEGACY_REPOS = [];
195
196
  }
196
197
  });
197
198
 
@@ -335,10 +336,11 @@ async function extractIssueFields(input2, options = {}) {
335
336
  options.onLlmFallback?.("AI parsing unavailable, used keyword matching");
336
337
  return heuristic;
337
338
  }
339
+ const safeLabels = options.validLabels && options.validLabels.length > 0 ? llmResult.labels.filter((l) => (options.validLabels ?? []).includes(l)) : llmResult.labels;
338
340
  const merged = {
339
341
  ...llmResult,
340
342
  // Heuristic explicit tokens always win
341
- labels: heuristic.labels.length > 0 ? heuristic.labels : llmResult.labels,
343
+ labels: heuristic.labels.length > 0 ? heuristic.labels : safeLabels,
342
344
  assignee: heuristic.assignee ?? llmResult.assignee,
343
345
  dueDate: heuristic.dueDate ?? llmResult.due_date,
344
346
  // LLM title is used only if heuristic left explicit tokens
@@ -385,30 +387,41 @@ var init_api = __esm({
385
387
  throw new Error(`TickTick API error ${res.status}: ${text2}`);
386
388
  }
387
389
  const text = await res.text();
388
- if (!text) return void 0;
390
+ if (!text) return null;
389
391
  return JSON.parse(text);
390
392
  }
391
393
  async listProjects() {
392
- return this.request("GET", "/project");
394
+ return await this.request("GET", "/project") ?? [];
393
395
  }
394
396
  async getProject(projectId) {
395
- return this.request("GET", `/project/${projectId}`);
397
+ const result = await this.request("GET", `/project/${projectId}`);
398
+ if (!result) throw new Error(`TickTick API returned empty response for project ${projectId}`);
399
+ return result;
396
400
  }
397
401
  async getProjectData(projectId) {
398
- return this.request("GET", `/project/${projectId}/data`);
402
+ const result = await this.request("GET", `/project/${projectId}/data`);
403
+ if (!result)
404
+ throw new Error(`TickTick API returned empty response for project data ${projectId}`);
405
+ return result;
399
406
  }
400
407
  async listTasks(projectId) {
401
408
  const data = await this.getProjectData(projectId);
402
409
  return data.tasks ?? [];
403
410
  }
404
411
  async getTask(projectId, taskId) {
405
- return this.request("GET", `/project/${projectId}/task/${taskId}`);
412
+ const result = await this.request("GET", `/project/${projectId}/task/${taskId}`);
413
+ if (!result) throw new Error(`TickTick API returned empty response for task ${taskId}`);
414
+ return result;
406
415
  }
407
416
  async createTask(input2) {
408
- return this.request("POST", "/task", input2);
417
+ const result = await this.request("POST", "/task", input2);
418
+ if (!result) throw new Error("TickTick API returned empty response for createTask");
419
+ return result;
409
420
  }
410
421
  async updateTask(input2) {
411
- return this.request("POST", `/task/${input2.id}`, input2);
422
+ const result = await this.request("POST", `/task/${input2.id}`, input2);
423
+ if (!result) throw new Error(`TickTick API returned empty response for updateTask ${input2.id}`);
424
+ return result;
412
425
  }
413
426
  async completeTask(projectId, taskId) {
414
427
  await this.request("POST", `/project/${projectId}/task/${taskId}/complete`);
@@ -480,6 +493,31 @@ var init_types = __esm({
480
493
  }
481
494
  });
482
495
 
496
+ // src/board/constants.ts
497
+ function isTerminalStatus(status) {
498
+ return TERMINAL_STATUS_RE.test(status);
499
+ }
500
+ function isHeaderId(id) {
501
+ return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
502
+ }
503
+ function timeAgo(date) {
504
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
505
+ if (seconds < 10) return "just now";
506
+ if (seconds < 60) return `${seconds}s ago`;
507
+ const minutes = Math.floor(seconds / 60);
508
+ return `${minutes}m ago`;
509
+ }
510
+ function formatError(err) {
511
+ return err instanceof Error ? err.message : String(err);
512
+ }
513
+ var TERMINAL_STATUS_RE;
514
+ var init_constants = __esm({
515
+ "src/board/constants.ts"() {
516
+ "use strict";
517
+ TERMINAL_STATUS_RE = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
518
+ }
519
+ });
520
+
483
521
  // src/github.ts
484
522
  var github_exports = {};
485
523
  __export(github_exports, {
@@ -489,6 +527,11 @@ __export(github_exports, {
489
527
  assignIssue: () => assignIssue,
490
528
  assignIssueAsync: () => assignIssueAsync,
491
529
  assignIssueToAsync: () => assignIssueToAsync,
530
+ clearProjectNodeIdCache: () => clearProjectNodeIdCache,
531
+ closeIssueAsync: () => closeIssueAsync,
532
+ createIssueAsync: () => createIssueAsync,
533
+ editIssueBodyAsync: () => editIssueBodyAsync,
534
+ editIssueTitleAsync: () => editIssueTitleAsync,
492
535
  fetchAssignedIssues: () => fetchAssignedIssues,
493
536
  fetchIssueAsync: () => fetchIssueAsync,
494
537
  fetchIssueCommentsAsync: () => fetchIssueCommentsAsync,
@@ -500,6 +543,7 @@ __export(github_exports, {
500
543
  fetchRepoLabelsAsync: () => fetchRepoLabelsAsync,
501
544
  removeLabelAsync: () => removeLabelAsync,
502
545
  unassignIssueAsync: () => unassignIssueAsync,
546
+ updateLabelsAsync: () => updateLabelsAsync,
503
547
  updateProjectItemDateAsync: () => updateProjectItemDateAsync,
504
548
  updateProjectItemStatus: () => updateProjectItemStatus,
505
549
  updateProjectItemStatusAsync: () => updateProjectItemStatusAsync
@@ -587,6 +631,24 @@ async function fetchIssueAsync(repo, issueNumber) {
587
631
  "number,title,url,state,updatedAt,labels,assignees,body,projectStatus"
588
632
  ]);
589
633
  }
634
+ async function closeIssueAsync(repo, issueNumber) {
635
+ await runGhAsync(["issue", "close", String(issueNumber), "--repo", repo]);
636
+ }
637
+ async function createIssueAsync(repo, title, body, labels) {
638
+ const args = ["issue", "create", "--repo", repo, "--title", title, "--body", body];
639
+ if (labels && labels.length > 0) {
640
+ for (const label of labels) {
641
+ args.push("--label", label);
642
+ }
643
+ }
644
+ return runGhAsync(args);
645
+ }
646
+ async function editIssueTitleAsync(repo, issueNumber, title) {
647
+ await runGhAsync(["issue", "edit", String(issueNumber), "--repo", repo, "--title", title]);
648
+ }
649
+ async function editIssueBodyAsync(repo, issueNumber, body) {
650
+ await runGhAsync(["issue", "edit", String(issueNumber), "--repo", repo, "--body", body]);
651
+ }
590
652
  async function addCommentAsync(repo, issueNumber, body) {
591
653
  await runGhAsync(["issue", "comment", String(issueNumber), "--repo", repo, "--body", body]);
592
654
  }
@@ -596,6 +658,12 @@ async function addLabelAsync(repo, issueNumber, label) {
596
658
  async function removeLabelAsync(repo, issueNumber, label) {
597
659
  await runGhAsync(["issue", "edit", String(issueNumber), "--repo", repo, "--remove-label", label]);
598
660
  }
661
+ async function updateLabelsAsync(repo, issueNumber, addLabels, removeLabels) {
662
+ const args = ["issue", "edit", String(issueNumber), "--repo", repo];
663
+ for (const label of addLabels) args.push("--add-label", label);
664
+ for (const label of removeLabels) args.push("--remove-label", label);
665
+ await runGhAsync(args);
666
+ }
599
667
  async function fetchIssueCommentsAsync(repo, issueNumber) {
600
668
  const result = await runGhJsonAsync([
601
669
  "issue",
@@ -635,6 +703,7 @@ function fetchProjectFields(repo, issueNumber, projectNumber) {
635
703
  }
636
704
  `;
637
705
  const [owner, repoName] = repo.split("/");
706
+ if (!(owner && repoName)) return {};
638
707
  try {
639
708
  const result = runGhJson([
640
709
  "api",
@@ -669,6 +738,7 @@ function fetchProjectFields(repo, issueNumber, projectNumber) {
669
738
  }
670
739
  function fetchProjectEnrichment(repo, projectNumber) {
671
740
  const [owner] = repo.split("/");
741
+ if (!owner) return /* @__PURE__ */ new Map();
672
742
  const query = `
673
743
  query($owner: String!, $projectNumber: Int!) {
674
744
  organization(login: $owner) {
@@ -741,6 +811,7 @@ function fetchProjectTargetDates(repo, projectNumber) {
741
811
  }
742
812
  function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
743
813
  const [owner] = repo.split("/");
814
+ if (!owner) return [];
744
815
  const query = `
745
816
  query($owner: String!, $projectNumber: Int!) {
746
817
  organization(login: $owner) {
@@ -791,8 +862,38 @@ async function fetchRepoLabelsAsync(repo) {
791
862
  return [];
792
863
  }
793
864
  }
865
+ function clearProjectNodeIdCache() {
866
+ projectNodeIdCache.clear();
867
+ }
868
+ async function getProjectNodeId(owner, projectNumber) {
869
+ const key = `${owner}/${String(projectNumber)}`;
870
+ const cached = projectNodeIdCache.get(key);
871
+ if (cached !== void 0) return cached;
872
+ const projectQuery = `
873
+ query($owner: String!) {
874
+ organization(login: $owner) {
875
+ projectV2(number: ${projectNumber}) {
876
+ id
877
+ }
878
+ }
879
+ }
880
+ `;
881
+ const projectResult = await runGhJsonAsync([
882
+ "api",
883
+ "graphql",
884
+ "-f",
885
+ `query=${projectQuery}`,
886
+ "-F",
887
+ `owner=${owner}`
888
+ ]);
889
+ const projectId = projectResult?.data?.organization?.projectV2?.id;
890
+ if (!projectId) return null;
891
+ projectNodeIdCache.set(key, projectId);
892
+ return projectId;
893
+ }
794
894
  function updateProjectItemStatus(repo, issueNumber, projectConfig) {
795
895
  const [owner, repoName] = repo.split("/");
896
+ if (!(owner && repoName)) return;
796
897
  const findItemQuery = `
797
898
  query($owner: String!, $repo: String!, $issueNumber: Int!) {
798
899
  repository(owner: $owner, name: $repo) {
@@ -875,6 +976,7 @@ function updateProjectItemStatus(repo, issueNumber, projectConfig) {
875
976
  }
876
977
  async function updateProjectItemStatusAsync(repo, issueNumber, projectConfig) {
877
978
  const [owner, repoName] = repo.split("/");
979
+ if (!(owner && repoName)) return;
878
980
  const findItemQuery = `
879
981
  query($owner: String!, $repo: String!, $issueNumber: Int!) {
880
982
  repository(owner: $owner, name: $repo) {
@@ -905,24 +1007,7 @@ async function updateProjectItemStatusAsync(repo, issueNumber, projectConfig) {
905
1007
  const projectNumber = projectConfig.projectNumber;
906
1008
  const projectItem = items.find((item) => item?.project?.number === projectNumber);
907
1009
  if (!projectItem?.id) return;
908
- const projectQuery = `
909
- query($owner: String!) {
910
- organization(login: $owner) {
911
- projectV2(number: ${projectNumber}) {
912
- id
913
- }
914
- }
915
- }
916
- `;
917
- const projectResult = await runGhJsonAsync([
918
- "api",
919
- "graphql",
920
- "-f",
921
- `query=${projectQuery}`,
922
- "-F",
923
- `owner=${owner}`
924
- ]);
925
- const projectId = projectResult?.data?.organization?.projectV2?.id;
1010
+ const projectId = await getProjectNodeId(owner, projectNumber);
926
1011
  if (!projectId) return;
927
1012
  const statusFieldId = projectConfig.statusFieldId;
928
1013
  const optionId = projectConfig.optionId;
@@ -957,6 +1042,7 @@ async function updateProjectItemStatusAsync(repo, issueNumber, projectConfig) {
957
1042
  }
958
1043
  async function updateProjectItemDateAsync(repo, issueNumber, projectConfig, dueDate) {
959
1044
  const [owner, repoName] = repo.split("/");
1045
+ if (!(owner && repoName)) return;
960
1046
  const findItemQuery = `
961
1047
  query($owner: String!, $repo: String!, $issueNumber: Int!) {
962
1048
  repository(owner: $owner, name: $repo) {
@@ -986,24 +1072,7 @@ async function updateProjectItemDateAsync(repo, issueNumber, projectConfig, dueD
986
1072
  const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];
987
1073
  const projectItem = items.find((item) => item?.project?.number === projectConfig.projectNumber);
988
1074
  if (!projectItem?.id) return;
989
- const projectQuery = `
990
- query($owner: String!) {
991
- organization(login: $owner) {
992
- projectV2(number: ${projectConfig.projectNumber}) {
993
- id
994
- }
995
- }
996
- }
997
- `;
998
- const projectResult = await runGhJsonAsync([
999
- "api",
1000
- "graphql",
1001
- "-f",
1002
- `query=${projectQuery}`,
1003
- "-F",
1004
- `owner=${owner}`
1005
- ]);
1006
- const projectId = projectResult?.data?.organization?.projectV2?.id;
1075
+ const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);
1007
1076
  if (!projectId) return;
1008
1077
  const mutation = `
1009
1078
  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {
@@ -1034,12 +1103,13 @@ async function updateProjectItemDateAsync(repo, issueNumber, projectConfig, dueD
1034
1103
  `date=${dueDate}`
1035
1104
  ]);
1036
1105
  }
1037
- var execFileAsync, DATE_FIELD_NAME_RE2;
1106
+ var execFileAsync, DATE_FIELD_NAME_RE2, projectNodeIdCache;
1038
1107
  var init_github = __esm({
1039
1108
  "src/github.ts"() {
1040
1109
  "use strict";
1041
1110
  execFileAsync = promisify(execFile);
1042
1111
  DATE_FIELD_NAME_RE2 = /^(target\s*date|due\s*date|due|deadline)$/i;
1112
+ projectNodeIdCache = /* @__PURE__ */ new Map();
1043
1113
  }
1044
1114
  });
1045
1115
 
@@ -1057,7 +1127,7 @@ function loadSyncState() {
1057
1127
  }
1058
1128
  function saveSyncState(state) {
1059
1129
  writeFileSync3(STATE_FILE, `${JSON.stringify(state, null, 2)}
1060
- `);
1130
+ `, { mode: 384 });
1061
1131
  }
1062
1132
  function findMapping(state, githubRepo, issueNumber) {
1063
1133
  return state.mappings.find(
@@ -1281,8 +1351,6 @@ var init_use_action_log = __esm({
1281
1351
  });
1282
1352
 
1283
1353
  // src/board/hooks/use-actions.ts
1284
- import { execFile as execFile2 } from "child_process";
1285
- import { promisify as promisify2 } from "util";
1286
1354
  import { useCallback as useCallback2, useRef as useRef2 } from "react";
1287
1355
  function findIssueContext(repos, selectedId, config2) {
1288
1356
  if (!selectedId?.startsWith("gh:")) {
@@ -1301,17 +1369,10 @@ function findIssueContext(repos, selectedId, config2) {
1301
1369
  async function triggerCompletionActionAsync(action, repoName, issueNumber) {
1302
1370
  switch (action.type) {
1303
1371
  case "closeIssue":
1304
- await execFileAsync2("gh", ["issue", "close", String(issueNumber), "--repo", repoName], {
1305
- encoding: "utf-8",
1306
- timeout: 3e4
1307
- });
1372
+ await closeIssueAsync(repoName, issueNumber);
1308
1373
  break;
1309
1374
  case "addLabel":
1310
- await execFileAsync2(
1311
- "gh",
1312
- ["issue", "edit", String(issueNumber), "--repo", repoName, "--add-label", action.label],
1313
- { encoding: "utf-8", timeout: 3e4 }
1314
- );
1375
+ await addLabelAsync(repoName, issueNumber, action.label);
1315
1376
  break;
1316
1377
  case "updateProjectStatus":
1317
1378
  break;
@@ -1419,11 +1480,7 @@ function useActions({
1419
1480
  }
1420
1481
  const { issue, repoName } = ctx;
1421
1482
  const t = toast.loading("Commenting...");
1422
- execFileAsync2(
1423
- "gh",
1424
- ["issue", "comment", String(issue.number), "--repo", repoName, "--body", body],
1425
- { encoding: "utf-8", timeout: 3e4 }
1426
- ).then(() => {
1483
+ addCommentAsync(repoName, issue.number, body).then(() => {
1427
1484
  t.resolve(`Comment posted on #${issue.number}`);
1428
1485
  pushEntryRef.current?.({
1429
1486
  id: nextEntryId(),
@@ -1432,6 +1489,7 @@ function useActions({
1432
1489
  ago: Date.now()
1433
1490
  });
1434
1491
  refresh();
1492
+ onOverlayDone();
1435
1493
  }).catch((err) => {
1436
1494
  t.reject(`Comment failed: ${err instanceof Error ? err.message : String(err)}`);
1437
1495
  pushEntryRef.current?.({
@@ -1440,8 +1498,6 @@ function useActions({
1440
1498
  status: "error",
1441
1499
  ago: Date.now()
1442
1500
  });
1443
- }).finally(() => {
1444
- onOverlayDone();
1445
1501
  });
1446
1502
  },
1447
1503
  [toast, refresh, onOverlayDone]
@@ -1545,19 +1601,7 @@ function useActions({
1545
1601
  status: "success",
1546
1602
  ago: Date.now(),
1547
1603
  undo: async () => {
1548
- await execFileAsync2(
1549
- "gh",
1550
- [
1551
- "issue",
1552
- "edit",
1553
- String(issue.number),
1554
- "--repo",
1555
- repoName,
1556
- "--remove-assignee",
1557
- "@me"
1558
- ],
1559
- { encoding: "utf-8", timeout: 3e4 }
1560
- );
1604
+ await unassignIssueAsync(repoName, issue.number, "@me");
1561
1605
  }
1562
1606
  });
1563
1607
  refresh();
@@ -1571,33 +1615,6 @@ function useActions({
1571
1615
  });
1572
1616
  });
1573
1617
  }, [toast, refresh]);
1574
- const handleUnassign = useCallback2(() => {
1575
- const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
1576
- if (!(ctx.issue && ctx.repoName)) return;
1577
- const { issue, repoName } = ctx;
1578
- const assignees = issue.assignees ?? [];
1579
- const selfAssigned = assignees.some((a) => a.login === configRef.current.board.assignee);
1580
- if (!selfAssigned) {
1581
- const firstAssignee = assignees[0];
1582
- if (firstAssignee) {
1583
- toast.info(`Assigned to @${firstAssignee.login} \u2014 can only unassign self`);
1584
- } else {
1585
- toast.info("Not assigned");
1586
- }
1587
- return;
1588
- }
1589
- const t = toast.loading("Unassigning...");
1590
- execFileAsync2(
1591
- "gh",
1592
- ["issue", "edit", String(issue.number), "--repo", repoName, "--remove-assignee", "@me"],
1593
- { encoding: "utf-8", timeout: 3e4 }
1594
- ).then(() => {
1595
- t.resolve(`Unassigned #${issue.number} from @${configRef.current.board.assignee}`);
1596
- refresh();
1597
- }).catch((err) => {
1598
- t.reject(`Unassign failed: ${err instanceof Error ? err.message : String(err)}`);
1599
- });
1600
- }, [toast, refresh]);
1601
1618
  const handleCreateIssue = useCallback2(
1602
1619
  async (repo, title, body, dueDate, labels) => {
1603
1620
  const repoConfig = configRef.current.repos.find((r) => r.name === repo);
@@ -1608,16 +1625,9 @@ function useActions({
1608
1625
 
1609
1626
  ${dueLine}` : dueLine;
1610
1627
  }
1611
- const args = ["issue", "create", "--repo", repo, "--title", title, "--body", effectiveBody];
1612
- if (labels && labels.length > 0) {
1613
- for (const label of labels) {
1614
- args.push("--label", label);
1615
- }
1616
- }
1617
1628
  const t = toast.loading("Creating...");
1618
1629
  try {
1619
- const { stdout } = await execFileAsync2("gh", args, { encoding: "utf-8", timeout: 3e4 });
1620
- const output = stdout.trim();
1630
+ const output = await createIssueAsync(repo, title, effectiveBody, labels);
1621
1631
  const match = output.match(/\/(\d+)$/);
1622
1632
  const issueNumber = match?.[1] ? parseInt(match[1], 10) : 0;
1623
1633
  const shortName = repoConfig?.shortName ?? repo;
@@ -1646,11 +1656,8 @@ ${dueLine}` : dueLine;
1646
1656
  const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
1647
1657
  if (!(ctx.issue && ctx.repoName)) return;
1648
1658
  const { issue, repoName } = ctx;
1649
- const args = ["issue", "edit", String(issue.number), "--repo", repoName];
1650
- for (const label of addLabels) args.push("--add-label", label);
1651
- for (const label of removeLabels) args.push("--remove-label", label);
1652
1659
  const t = toast.loading("Updating labels...");
1653
- execFileAsync2("gh", args, { encoding: "utf-8", timeout: 3e4 }).then(() => {
1660
+ updateLabelsAsync(repoName, issue.number, addLabels, removeLabels).then(() => {
1654
1661
  t.resolve(`Labels updated on #${issue.number}`);
1655
1662
  refresh();
1656
1663
  onOverlayDone();
@@ -1706,19 +1713,7 @@ ${dueLine}` : dueLine;
1706
1713
  const assignees = ctx.issue.assignees ?? [];
1707
1714
  if (!assignees.some((a) => a.login === configRef.current.board.assignee)) continue;
1708
1715
  try {
1709
- await execFileAsync2(
1710
- "gh",
1711
- [
1712
- "issue",
1713
- "edit",
1714
- String(ctx.issue.number),
1715
- "--repo",
1716
- ctx.repoName,
1717
- "--remove-assignee",
1718
- "@me"
1719
- ],
1720
- { encoding: "utf-8", timeout: 3e4 }
1721
- );
1716
+ await unassignIssueAsync(ctx.repoName, ctx.issue.number, "@me");
1722
1717
  } catch {
1723
1718
  failed.push(id);
1724
1719
  }
@@ -1783,7 +1778,6 @@ ${dueLine}` : dueLine;
1783
1778
  handleComment,
1784
1779
  handleStatusChange,
1785
1780
  handleAssign,
1786
- handleUnassign,
1787
1781
  handleLabelChange,
1788
1782
  handleCreateIssue,
1789
1783
  handleBulkAssign,
@@ -1791,15 +1785,13 @@ ${dueLine}` : dueLine;
1791
1785
  handleBulkStatusChange
1792
1786
  };
1793
1787
  }
1794
- var execFileAsync2, TERMINAL_STATUS_RE;
1795
1788
  var init_use_actions = __esm({
1796
1789
  "src/board/hooks/use-actions.ts"() {
1797
1790
  "use strict";
1798
1791
  init_github();
1799
1792
  init_pick();
1793
+ init_constants();
1800
1794
  init_use_action_log();
1801
- execFileAsync2 = promisify2(execFile2);
1802
- TERMINAL_STATUS_RE = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
1803
1795
  }
1804
1796
  });
1805
1797
 
@@ -1860,7 +1852,8 @@ function useData(config2, options, refreshIntervalMs) {
1860
1852
  { workerData: { config: configRef.current, options: optionsRef.current } }
1861
1853
  );
1862
1854
  workerRef.current = worker;
1863
- worker.on("message", (msg) => {
1855
+ worker.on("message", (rawMsg) => {
1856
+ const msg = rawMsg;
1864
1857
  if (token.canceled) {
1865
1858
  worker.terminate();
1866
1859
  return;
@@ -1881,13 +1874,13 @@ function useData(config2, options, refreshIntervalMs) {
1881
1874
  consecutiveFailures: 0,
1882
1875
  autoRefreshPaused: false
1883
1876
  });
1884
- } else {
1877
+ } else if (msg.type === "error") {
1885
1878
  setState((prev) => {
1886
1879
  const failures = prev.consecutiveFailures + 1;
1887
1880
  return {
1888
1881
  ...prev,
1889
1882
  status: prev.data ? "success" : "error",
1890
- error: msg.error ?? "Unknown error",
1883
+ error: msg.error,
1891
1884
  isRefreshing: false,
1892
1885
  consecutiveFailures: failures,
1893
1886
  autoRefreshPaused: failures >= MAX_REFRESH_FAILURES
@@ -1998,9 +1991,6 @@ var init_use_data = __esm({
1998
1991
  // src/board/hooks/use-keyboard.ts
1999
1992
  import { useInput } from "ink";
2000
1993
  import { useCallback as useCallback4 } from "react";
2001
- function isHeaderId(id) {
2002
- return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2003
- }
2004
1994
  function useKeyboard({
2005
1995
  ui,
2006
1996
  nav,
@@ -2238,6 +2228,7 @@ function useKeyboard({
2238
2228
  var init_use_keyboard = __esm({
2239
2229
  "src/board/hooks/use-keyboard.ts"() {
2240
2230
  "use strict";
2231
+ init_constants();
2241
2232
  }
2242
2233
  });
2243
2234
 
@@ -2579,19 +2570,6 @@ function useToast() {
2579
2570
  [addToast, removeToast]
2580
2571
  )
2581
2572
  };
2582
- const dismiss = useCallback7(
2583
- (id) => {
2584
- removeToast(id);
2585
- },
2586
- [removeToast]
2587
- );
2588
- const dismissAll = useCallback7(() => {
2589
- for (const timer of timersRef.current.values()) {
2590
- clearTimeout(timer);
2591
- }
2592
- timersRef.current.clear();
2593
- setToasts([]);
2594
- }, []);
2595
2573
  const handleErrorAction = useCallback7(
2596
2574
  (action) => {
2597
2575
  const errorToast = toasts.find((t) => t.type === "error");
@@ -2609,7 +2587,7 @@ function useToast() {
2609
2587
  },
2610
2588
  [toasts, removeToast]
2611
2589
  );
2612
- return { toasts, toast, dismiss, dismissAll, handleErrorAction };
2590
+ return { toasts, toast, handleErrorAction };
2613
2591
  }
2614
2592
  var MAX_VISIBLE, AUTO_DISMISS_MS, nextId;
2615
2593
  var init_use_toast = __esm({
@@ -3002,7 +2980,14 @@ var init_detail_panel = __esm({
3002
2980
  // src/board/components/hint-bar.tsx
3003
2981
  import { Box as Box3, Text as Text3 } from "ink";
3004
2982
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3005
- function HintBar({ uiMode, multiSelectCount, searchQuery, mineOnly, hasUndoable }) {
2983
+ function HintBar({
2984
+ uiMode,
2985
+ multiSelectCount,
2986
+ searchQuery,
2987
+ mineOnly,
2988
+ hasUndoable,
2989
+ onHeader
2990
+ }) {
3006
2991
  if (uiMode === "multiSelect") {
3007
2992
  return /* @__PURE__ */ jsxs3(Box3, { children: [
3008
2993
  /* @__PURE__ */ jsxs3(Text3, { color: "cyan", bold: true, children: [
@@ -3033,6 +3018,17 @@ function HintBar({ uiMode, multiSelectCount, searchQuery, mineOnly, hasUndoable
3033
3018
  if (uiMode.startsWith("overlay:")) {
3034
3019
  return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "j/k:nav Enter:select Esc:cancel" }) });
3035
3020
  }
3021
+ if (onHeader) {
3022
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
3023
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "j/k:nav Enter/Space:expand-collapse Tab:next-section C:collapse-all ?:more q:quit" }),
3024
+ mineOnly ? /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: " filter:@me" }) : null,
3025
+ searchQuery ? /* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
3026
+ ' filter:"',
3027
+ searchQuery,
3028
+ '"'
3029
+ ] }) : null
3030
+ ] });
3031
+ }
3036
3032
  return /* @__PURE__ */ jsxs3(Box3, { children: [
3037
3033
  /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
3038
3034
  "j/k:nav Enter:open m:status c:comment F:find t:@me e:edit L:log",
@@ -3495,11 +3491,10 @@ var init_create_issue_form = __esm({
3495
3491
  });
3496
3492
 
3497
3493
  // src/board/components/edit-issue-overlay.tsx
3498
- import { execFile as execFile3, spawnSync as spawnSync2 } from "child_process";
3494
+ import { spawnSync as spawnSync2 } from "child_process";
3499
3495
  import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3500
3496
  import { tmpdir as tmpdir2 } from "os";
3501
3497
  import { join as join5 } from "path";
3502
- import { promisify as promisify3 } from "util";
3503
3498
  import { Box as Box9, Text as Text9, useStdin as useStdin2 } from "ink";
3504
3499
  import { useEffect as useEffect6, useRef as useRef9, useState as useState10 } from "react";
3505
3500
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -3651,11 +3646,7 @@ function EditIssueOverlay({
3651
3646
  const changedFields = [];
3652
3647
  if (parsed.title !== issue.title) {
3653
3648
  try {
3654
- await execFileAsync3(
3655
- "gh",
3656
- ["issue", "edit", String(issue.number), "--repo", repoName, "--title", parsed.title],
3657
- { encoding: "utf-8", timeout: 3e4 }
3658
- );
3649
+ await editIssueTitleAsync(repoName, issue.number, parsed.title);
3659
3650
  changedFields.push("title");
3660
3651
  } catch {
3661
3652
  onToastError(`Failed to update title on #${issue.number}`);
@@ -3663,11 +3654,7 @@ function EditIssueOverlay({
3663
3654
  }
3664
3655
  if (parsed.body !== (issue.body ?? "").trim()) {
3665
3656
  try {
3666
- await execFileAsync3(
3667
- "gh",
3668
- ["issue", "edit", String(issue.number), "--repo", repoName, "--body", parsed.body],
3669
- { encoding: "utf-8", timeout: 3e4 }
3670
- );
3657
+ await editIssueBodyAsync(repoName, issue.number, parsed.body);
3671
3658
  changedFields.push("body");
3672
3659
  } catch {
3673
3660
  onToastError(`Failed to update body on #${issue.number}`);
@@ -3691,11 +3678,8 @@ function EditIssueOverlay({
3691
3678
  const addLabels = parsed.labels.filter((l) => !origLabels.includes(l));
3692
3679
  const removeLabels = origLabels.filter((l) => !parsed.labels.includes(l));
3693
3680
  if (addLabels.length > 0 || removeLabels.length > 0) {
3694
- const args = ["issue", "edit", String(issue.number), "--repo", repoName];
3695
- for (const l of addLabels) args.push("--add-label", l);
3696
- for (const l of removeLabels) args.push("--remove-label", l);
3697
3681
  try {
3698
- await execFileAsync3("gh", args, { encoding: "utf-8", timeout: 3e4 });
3682
+ await updateLabelsAsync(repoName, issue.number, addLabels, removeLabels);
3699
3683
  changedFields.push("labels");
3700
3684
  } catch {
3701
3685
  onToastError(`Failed to update labels on #${issue.number}`);
@@ -3704,34 +3688,10 @@ function EditIssueOverlay({
3704
3688
  if (parsed.assignee !== origAssignee) {
3705
3689
  try {
3706
3690
  if (parsed.assignee) {
3707
- await execFileAsync3(
3708
- "gh",
3709
- [
3710
- "issue",
3711
- "edit",
3712
- String(issue.number),
3713
- "--repo",
3714
- repoName,
3715
- "--add-assignee",
3716
- parsed.assignee
3717
- ],
3718
- { encoding: "utf-8", timeout: 3e4 }
3719
- );
3691
+ await assignIssueToAsync(repoName, issue.number, parsed.assignee);
3720
3692
  }
3721
3693
  if (origAssignee) {
3722
- await execFileAsync3(
3723
- "gh",
3724
- [
3725
- "issue",
3726
- "edit",
3727
- String(issue.number),
3728
- "--repo",
3729
- repoName,
3730
- "--remove-assignee",
3731
- origAssignee
3732
- ],
3733
- { encoding: "utf-8", timeout: 3e4 }
3734
- );
3694
+ await unassignIssueAsync(repoName, issue.number, origAssignee);
3735
3695
  }
3736
3696
  changedFields.push("assignee");
3737
3697
  } catch {
@@ -3785,14 +3745,12 @@ function EditIssueOverlay({
3785
3745
  "\u2026"
3786
3746
  ] }) });
3787
3747
  }
3788
- var execFileAsync3;
3789
3748
  var init_edit_issue_overlay = __esm({
3790
3749
  "src/board/components/edit-issue-overlay.tsx"() {
3791
3750
  "use strict";
3792
3751
  init_github();
3793
3752
  init_use_action_log();
3794
3753
  init_ink_instance();
3795
- execFileAsync3 = promisify3(execFile3);
3796
3754
  }
3797
3755
  });
3798
3756
 
@@ -4439,7 +4397,7 @@ import { Box as Box15, Text as Text15, useInput as useInput11 } from "ink";
4439
4397
  import { useRef as useRef12, useState as useState14 } from "react";
4440
4398
  import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
4441
4399
  function isTerminal(name) {
4442
- return TERMINAL_STATUS_RE2.test(name);
4400
+ return TERMINAL_STATUS_RE.test(name);
4443
4401
  }
4444
4402
  function handlePickerInput(input2, key, state) {
4445
4403
  if (key.escape) {
@@ -4548,11 +4506,10 @@ function StatusPicker({
4548
4506
  /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
4549
4507
  ] });
4550
4508
  }
4551
- var TERMINAL_STATUS_RE2;
4552
4509
  var init_status_picker = __esm({
4553
4510
  "src/board/components/status-picker.tsx"() {
4554
4511
  "use strict";
4555
- TERMINAL_STATUS_RE2 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
4512
+ init_constants();
4556
4513
  }
4557
4514
  });
4558
4515
 
@@ -4737,7 +4694,7 @@ function formatTargetDate(dateStr) {
4737
4694
  color: "gray"
4738
4695
  };
4739
4696
  }
4740
- function timeAgo(dateStr) {
4697
+ function timeAgo2(dateStr) {
4741
4698
  const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
4742
4699
  if (seconds < 60) return "now";
4743
4700
  const minutes = Math.floor(seconds / 60);
@@ -4781,7 +4738,7 @@ function IssueRow({ issue, selfLogin, isSelected }) {
4781
4738
  /* @__PURE__ */ jsx17(Text16, { children: " " }),
4782
4739
  /* @__PURE__ */ jsx17(Text16, { color: assigneeColor, children: assigneeText.padEnd(14) }),
4783
4740
  /* @__PURE__ */ jsx17(Text16, { children: " " }),
4784
- /* @__PURE__ */ jsx17(Text16, { color: "gray", children: timeAgo(issue.updatedAt).padStart(4) }),
4741
+ /* @__PURE__ */ jsx17(Text16, { color: "gray", children: timeAgo2(issue.updatedAt).padStart(4) }),
4785
4742
  target.text ? /* @__PURE__ */ jsxs17(Fragment3, { children: [
4786
4743
  /* @__PURE__ */ jsx17(Text16, { children: " " }),
4787
4744
  /* @__PURE__ */ jsx17(Text16, { color: target.color, children: target.text })
@@ -4916,7 +4873,7 @@ function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
4916
4873
  ] });
4917
4874
  }
4918
4875
  case "activity": {
4919
- const ago = timeAgo2(row.event.timestamp);
4876
+ const ago = timeAgo(row.event.timestamp);
4920
4877
  return /* @__PURE__ */ jsxs19(Text18, { dimColor: true, children: [
4921
4878
  " ",
4922
4879
  ago,
@@ -4944,16 +4901,10 @@ function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
4944
4901
  return /* @__PURE__ */ jsx19(Text18, { children: "" });
4945
4902
  }
4946
4903
  }
4947
- function timeAgo2(date) {
4948
- const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
4949
- if (seconds < 10) return "just now";
4950
- if (seconds < 60) return `${seconds}s ago`;
4951
- const minutes = Math.floor(seconds / 60);
4952
- return `${minutes}m ago`;
4953
- }
4954
4904
  var init_row_renderer = __esm({
4955
4905
  "src/board/components/row-renderer.tsx"() {
4956
4906
  "use strict";
4907
+ init_constants();
4957
4908
  init_issue_row();
4958
4909
  init_task_row();
4959
4910
  }
@@ -5002,9 +4953,6 @@ import { Spinner as Spinner4 } from "@inkjs/ui";
5002
4953
  import { Box as Box20, Text as Text20, useApp, useStdout } from "ink";
5003
4954
  import { useCallback as useCallback11, useEffect as useEffect9, useMemo as useMemo3, useRef as useRef13, useState as useState15 } from "react";
5004
4955
  import { Fragment as Fragment5, jsx as jsx21, jsxs as jsxs21 } from "react/jsx-runtime";
5005
- function isTerminalStatus(status) {
5006
- return TERMINAL_STATUS_RE3.test(status);
5007
- }
5008
4956
  function resolveStatusGroups(statusOptions, configuredGroups) {
5009
4957
  if (configuredGroups && configuredGroups.length > 0) {
5010
4958
  return configuredGroups.map((entry) => {
@@ -5226,28 +5174,13 @@ function buildFlatRows(repos, tasks, activity, isCollapsed) {
5226
5174
  }
5227
5175
  return rows;
5228
5176
  }
5229
- function timeAgo3(date) {
5230
- const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
5231
- if (seconds < 10) return "just now";
5232
- if (seconds < 60) return `${seconds}s ago`;
5233
- const minutes = Math.floor(seconds / 60);
5234
- return `${minutes}m ago`;
5235
- }
5236
5177
  function openInBrowser(url) {
5178
+ if (!(url.startsWith("https://") || url.startsWith("http://"))) return;
5237
5179
  try {
5238
5180
  execFileSync3("open", [url], { stdio: "ignore" });
5239
5181
  } catch {
5240
5182
  }
5241
5183
  }
5242
- function findSelectedUrl(repos, selectedId) {
5243
- if (!selectedId?.startsWith("gh:")) return null;
5244
- for (const rd of repos) {
5245
- for (const issue of rd.issues) {
5246
- if (`gh:${rd.repo.name}:${issue.number}` === selectedId) return issue.url;
5247
- }
5248
- }
5249
- return null;
5250
- }
5251
5184
  function findSelectedIssueWithRepo(repos, selectedId) {
5252
5185
  if (!selectedId?.startsWith("gh:")) return null;
5253
5186
  for (const rd of repos) {
@@ -5258,8 +5191,17 @@ function findSelectedIssueWithRepo(repos, selectedId) {
5258
5191
  }
5259
5192
  return null;
5260
5193
  }
5261
- function isHeaderId2(id) {
5262
- return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
5194
+ function RefreshAge({ lastRefresh }) {
5195
+ const [, setTick] = useState15(0);
5196
+ useEffect9(() => {
5197
+ const id = setInterval(() => setTick((t) => t + 1), 1e4);
5198
+ return () => clearInterval(id);
5199
+ }, []);
5200
+ if (!lastRefresh) return null;
5201
+ return /* @__PURE__ */ jsxs21(Text20, { color: refreshAgeColor(lastRefresh), children: [
5202
+ "Updated ",
5203
+ timeAgo(lastRefresh)
5204
+ ] });
5263
5205
  }
5264
5206
  function Dashboard({ config: config2, options, activeProfile }) {
5265
5207
  const { exit } = useApp();
@@ -5298,11 +5240,11 @@ function Dashboard({ config: config2, options, activeProfile }) {
5298
5240
  const last = logEntries[logEntries.length - 1];
5299
5241
  if (last?.status === "error") setLogVisible(true);
5300
5242
  }, [logEntries]);
5301
- const [, setTick] = useState15(0);
5302
5243
  useEffect9(() => {
5303
- const id = setInterval(() => setTick((t) => t + 1), 1e4);
5304
- return () => clearInterval(id);
5305
- }, []);
5244
+ if (data?.ticktickError) {
5245
+ toast.error(`TickTick sync failed: ${data.ticktickError}`);
5246
+ }
5247
+ }, [data?.ticktickError, toast.error]);
5306
5248
  const repos = useMemo3(() => {
5307
5249
  let filtered = allRepos;
5308
5250
  if (mineOnly) {
@@ -5405,7 +5347,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
5405
5347
  const [focusLabel, setFocusLabel] = useState15(null);
5406
5348
  const handleEnterFocus = useCallback11(() => {
5407
5349
  const id = nav.selectedId;
5408
- if (!id || isHeaderId2(id)) return;
5350
+ if (!id || isHeaderId(id)) return;
5409
5351
  let label = "";
5410
5352
  if (id.startsWith("gh:")) {
5411
5353
  const found = findSelectedIssueWithRepo(repos, id);
@@ -5480,7 +5422,10 @@ function Dashboard({ config: config2, options, activeProfile }) {
5480
5422
  [repos, tasks, allActivity, nav.isCollapsed]
5481
5423
  );
5482
5424
  const scrollRef = useRef13(0);
5483
- const selectedRowIdx = flatRows.findIndex((r) => r.navId === nav.selectedId);
5425
+ const selectedRowIdx = useMemo3(
5426
+ () => flatRows.findIndex((r) => r.navId === nav.selectedId),
5427
+ [flatRows, nav.selectedId]
5428
+ );
5484
5429
  if (selectedRowIdx >= 0) {
5485
5430
  if (selectedRowIdx < scrollRef.current) {
5486
5431
  scrollRef.current = selectedRowIdx;
@@ -5497,7 +5442,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
5497
5442
  const belowCount = flatRows.length - scrollRef.current - viewportHeight;
5498
5443
  const selectedItem = useMemo3(() => {
5499
5444
  const id = nav.selectedId;
5500
- if (!id || isHeaderId2(id)) return { issue: null, task: null, repoName: null };
5445
+ if (!id || isHeaderId(id)) return { issue: null, task: null, repoName: null };
5501
5446
  if (id.startsWith("gh:")) {
5502
5447
  for (const rd of repos) {
5503
5448
  for (const issue of rd.issues) {
@@ -5528,8 +5473,8 @@ function Dashboard({ config: config2, options, activeProfile }) {
5528
5473
  return rd?.statusOptions ?? [];
5529
5474
  }, [selectedItem.repoName, repos, multiSelect.count, multiSelect.constrainedRepo]);
5530
5475
  const handleOpen = useCallback11(() => {
5531
- const url = findSelectedUrl(repos, nav.selectedId);
5532
- if (url) openInBrowser(url);
5476
+ const found = findSelectedIssueWithRepo(repos, nav.selectedId);
5477
+ if (found) openInBrowser(found.issue.url);
5533
5478
  }, [repos, nav.selectedId]);
5534
5479
  const handleSlack = useCallback11(() => {
5535
5480
  const found = findSelectedIssueWithRepo(repos, nav.selectedId);
@@ -5697,13 +5642,10 @@ function Dashboard({ config: config2, options, activeProfile }) {
5697
5642
  isRefreshing ? /* @__PURE__ */ jsxs21(Fragment5, { children: [
5698
5643
  /* @__PURE__ */ jsx21(Spinner4, { label: "" }),
5699
5644
  /* @__PURE__ */ jsx21(Text20, { color: "cyan", children: " Refreshing..." })
5700
- ] }) : lastRefresh ? /* @__PURE__ */ jsxs21(Fragment5, { children: [
5701
- /* @__PURE__ */ jsxs21(Text20, { color: refreshAgeColor(lastRefresh), children: [
5702
- "Updated ",
5703
- timeAgo3(lastRefresh)
5704
- ] }),
5645
+ ] }) : /* @__PURE__ */ jsxs21(Fragment5, { children: [
5646
+ /* @__PURE__ */ jsx21(RefreshAge, { lastRefresh }),
5705
5647
  consecutiveFailures > 0 ? /* @__PURE__ */ jsx21(Text20, { color: "red", children: " (!)" }) : null
5706
- ] }) : null,
5648
+ ] }),
5707
5649
  autoRefreshPaused ? /* @__PURE__ */ jsx21(Text20, { color: "yellow", children: " Auto-refresh paused \u2014 press r to retry" }) : null
5708
5650
  ] }),
5709
5651
  error ? /* @__PURE__ */ jsxs21(Text20, { color: "red", children: [
@@ -5800,17 +5742,19 @@ function Dashboard({ config: config2, options, activeProfile }) {
5800
5742
  multiSelectCount: multiSelect.count,
5801
5743
  searchQuery,
5802
5744
  mineOnly,
5803
- hasUndoable
5745
+ hasUndoable,
5746
+ onHeader: isHeaderId(nav.selectedId)
5804
5747
  }
5805
5748
  )
5806
5749
  ] });
5807
5750
  }
5808
- var TERMINAL_STATUS_RE3, PRIORITY_RANK, CHROME_ROWS;
5751
+ var PRIORITY_RANK, CHROME_ROWS;
5809
5752
  var init_dashboard = __esm({
5810
5753
  "src/board/components/dashboard.tsx"() {
5811
5754
  "use strict";
5812
5755
  init_clipboard();
5813
5756
  init_github();
5757
+ init_constants();
5814
5758
  init_use_action_log();
5815
5759
  init_use_actions();
5816
5760
  init_use_data();
@@ -5825,7 +5769,6 @@ var init_dashboard = __esm({
5825
5769
  init_overlay_renderer();
5826
5770
  init_row_renderer();
5827
5771
  init_toast_container();
5828
- TERMINAL_STATUS_RE3 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
5829
5772
  PRIORITY_RANK = {
5830
5773
  "priority:critical": 0,
5831
5774
  "priority:high": 1,
@@ -5872,9 +5815,6 @@ function extractSlackUrl(body) {
5872
5815
  const match = body.match(SLACK_URL_RE2);
5873
5816
  return match?.[0];
5874
5817
  }
5875
- function formatError2(err) {
5876
- return err instanceof Error ? err.message : String(err);
5877
- }
5878
5818
  function fetchRecentActivity(repoName, shortName) {
5879
5819
  try {
5880
5820
  const output = execFileSync4(
@@ -5954,28 +5894,34 @@ async function fetchDashboard(config2, options = {}) {
5954
5894
  fetchOpts.assignee = config2.board.assignee;
5955
5895
  }
5956
5896
  const issues = fetchRepoIssues(repo.name, fetchOpts);
5897
+ let enrichedIssues = issues;
5957
5898
  let statusOptions = [];
5958
5899
  try {
5959
5900
  const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);
5960
- for (const issue of issues) {
5901
+ enrichedIssues = issues.map((issue) => {
5961
5902
  const e = enrichMap.get(issue.number);
5962
- if (e?.targetDate) issue.targetDate = e.targetDate;
5963
- if (e?.projectStatus) issue.projectStatus = e.projectStatus;
5964
- }
5903
+ const slackUrl = extractSlackUrl(issue.body ?? "");
5904
+ return {
5905
+ ...issue,
5906
+ ...e?.targetDate !== void 0 ? { targetDate: e.targetDate } : {},
5907
+ ...e?.projectStatus !== void 0 ? { projectStatus: e.projectStatus } : {},
5908
+ ...slackUrl ? { slackThreadUrl: slackUrl } : {}
5909
+ };
5910
+ });
5965
5911
  statusOptions = fetchProjectStatusOptions(
5966
5912
  repo.name,
5967
5913
  repo.projectNumber,
5968
5914
  repo.statusFieldId
5969
5915
  );
5970
5916
  } catch {
5917
+ enrichedIssues = issues.map((issue) => {
5918
+ const slackUrl = extractSlackUrl(issue.body ?? "");
5919
+ return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;
5920
+ });
5971
5921
  }
5972
- for (const issue of issues) {
5973
- const slackUrl = extractSlackUrl(issue.body);
5974
- if (slackUrl) issue.slackThreadUrl = slackUrl;
5975
- }
5976
- return { repo, issues, statusOptions, error: null };
5922
+ return { repo, issues: enrichedIssues, statusOptions, error: null };
5977
5923
  } catch (err) {
5978
- return { repo, issues: [], statusOptions: [], error: formatError2(err) };
5924
+ return { repo, issues: [], statusOptions: [], error: formatError(err) };
5979
5925
  }
5980
5926
  });
5981
5927
  let ticktick = [];
@@ -5990,7 +5936,7 @@ async function fetchDashboard(config2, options = {}) {
5990
5936
  ticktick = tasks.filter((t) => t.status !== 2 /* Completed */);
5991
5937
  }
5992
5938
  } catch (err) {
5993
- ticktickError = formatError2(err);
5939
+ ticktickError = formatError(err);
5994
5940
  }
5995
5941
  }
5996
5942
  const activity = [];
@@ -6015,6 +5961,7 @@ var init_fetch = __esm({
6015
5961
  init_config();
6016
5962
  init_github();
6017
5963
  init_types();
5964
+ init_constants();
6018
5965
  SLACK_URL_RE2 = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/i;
6019
5966
  }
6020
5967
  });
@@ -6193,7 +6140,9 @@ function renderBoardJson(data, selfLogin) {
6193
6140
  labels: i.labels.map((l) => l.name),
6194
6141
  updatedAt: i.updatedAt,
6195
6142
  isMine: (i.assignees ?? []).some((a) => a.login === selfLogin),
6196
- slackThreadUrl: i.slackThreadUrl ?? null
6143
+ slackThreadUrl: i.slackThreadUrl ?? null,
6144
+ projectStatus: i.projectStatus ?? null,
6145
+ targetDate: i.targetDate ?? null
6197
6146
  }))
6198
6147
  })),
6199
6148
  ticktick: {
@@ -6206,6 +6155,7 @@ function renderBoardJson(data, selfLogin) {
6206
6155
  tags: t.tags
6207
6156
  }))
6208
6157
  },
6158
+ activity: data.activity,
6209
6159
  fetchedAt: data.fetchedAt.toISOString()
6210
6160
  }
6211
6161
  };
@@ -6224,8 +6174,8 @@ var init_format_static = __esm({
6224
6174
  init_ai();
6225
6175
  init_api();
6226
6176
  init_config();
6227
- import { execFile as execFile4, execFileSync as execFileSync5 } from "child_process";
6228
- import { promisify as promisify4 } from "util";
6177
+ import { execFile as execFile2, execFileSync as execFileSync5 } from "child_process";
6178
+ import { promisify as promisify2 } from "util";
6229
6179
  import { Command } from "commander";
6230
6180
 
6231
6181
  // src/init.ts
@@ -6710,6 +6660,7 @@ function printSyncStatus(state, repos) {
6710
6660
 
6711
6661
  // src/sync.ts
6712
6662
  init_api();
6663
+ init_constants();
6713
6664
  init_config();
6714
6665
  init_github();
6715
6666
  init_sync_state();
@@ -6717,9 +6668,6 @@ init_types();
6717
6668
  function emptySyncResult() {
6718
6669
  return { created: [], updated: [], completed: [], ghUpdated: [], errors: [] };
6719
6670
  }
6720
- function formatError(err) {
6721
- return err instanceof Error ? err.message : String(err);
6722
- }
6723
6671
  function repoShortName(repo) {
6724
6672
  return repo.split("/")[1] ?? repo;
6725
6673
  }
@@ -6778,23 +6726,29 @@ async function syncGitHubToTickTick(config2, state, api, result, dryRun) {
6778
6726
  failedRepos.add(repoConfig.name);
6779
6727
  continue;
6780
6728
  }
6729
+ let enrichMap;
6730
+ try {
6731
+ enrichMap = fetchProjectEnrichment(repoConfig.name, repoConfig.projectNumber);
6732
+ } catch {
6733
+ enrichMap = /* @__PURE__ */ new Map();
6734
+ }
6781
6735
  for (const issue of issues) {
6782
6736
  const key = `${repoConfig.name}#${issue.number}`;
6783
6737
  openIssueKeys.add(key);
6784
- await syncSingleIssue(state, api, result, dryRun, repoConfig, issue, key);
6738
+ await syncSingleIssue(state, api, result, dryRun, repoConfig, issue, key, enrichMap);
6785
6739
  }
6786
6740
  }
6787
6741
  return { openIssueKeys, failedRepos };
6788
6742
  }
6789
- async function syncSingleIssue(state, api, result, dryRun, repoConfig, issue, key) {
6743
+ async function syncSingleIssue(state, api, result, dryRun, repoConfig, issue, key, enrichMap) {
6790
6744
  try {
6791
6745
  const existing = findMapping(state, repoConfig.name, issue.number);
6792
6746
  if (existing && existing.githubUpdatedAt === issue.updatedAt) return;
6793
- const projectFields = fetchProjectFields(
6794
- repoConfig.name,
6795
- issue.number,
6796
- repoConfig.projectNumber
6797
- );
6747
+ const enrichment = enrichMap.get(issue.number);
6748
+ const projectFields = {
6749
+ ...enrichment?.targetDate !== void 0 && { targetDate: enrichment.targetDate },
6750
+ ...enrichment?.projectStatus !== void 0 && { status: enrichment.projectStatus }
6751
+ };
6798
6752
  if (!existing) {
6799
6753
  await createTickTickTask(
6800
6754
  state,
@@ -6953,7 +6907,7 @@ if (major < 22) {
6953
6907
  );
6954
6908
  process.exit(1);
6955
6909
  }
6956
- var execFileAsync4 = promisify4(execFile4);
6910
+ var execFileAsync2 = promisify2(execFile2);
6957
6911
  async function resolveRef(ref, config2) {
6958
6912
  const { parseIssueRef: parseIssueRef2 } = await Promise.resolve().then(() => (init_pick(), pick_exports));
6959
6913
  try {
@@ -6981,8 +6935,7 @@ var PRIORITY_MAP = {
6981
6935
  function parsePriority(value) {
6982
6936
  const p = PRIORITY_MAP[value.toLowerCase()];
6983
6937
  if (p === void 0) {
6984
- console.error(`Invalid priority: ${value}. Use: none, low, medium, high`);
6985
- process.exit(1);
6938
+ errorOut(`Invalid priority: "${value}". Valid values: none, low, medium, high`);
6986
6939
  }
6987
6940
  return p;
6988
6941
  }
@@ -6998,7 +6951,7 @@ function resolveProjectId(projectId) {
6998
6951
  process.exit(1);
6999
6952
  }
7000
6953
  var program = new Command();
7001
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.7.2").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
6954
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.8.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7002
6955
  const opts = thisCommand.opts();
7003
6956
  if (opts.json) setFormat("json");
7004
6957
  if (opts.human) setFormat("human");
@@ -7020,9 +6973,7 @@ task.command("add <title>").description("Create a new task").option("-p, --prior
7020
6973
  if (opts.tags) input2.tags = opts.tags.split(",").map((t) => t.trim());
7021
6974
  if (opts.allDay) input2.isAllDay = true;
7022
6975
  const created = await api.createTask(input2);
7023
- printSuccess(`Created: ${created.title}`, {
7024
- task: created
7025
- });
6976
+ printSuccess(`Created: ${created.title}`, { task: created });
7026
6977
  });
7027
6978
  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) => {
7028
6979
  const api = createClient();
@@ -7063,9 +7014,7 @@ task.command("update <taskId>").description("Update a task").option("--title <ti
7063
7014
  if (opts.content) input2.content = opts.content;
7064
7015
  if (opts.tags) input2.tags = opts.tags.split(",").map((t) => t.trim());
7065
7016
  const updated = await api.updateTask(input2);
7066
- printSuccess(`Updated: ${updated.title}`, {
7067
- task: updated
7068
- });
7017
+ printSuccess(`Updated: ${updated.title}`, { task: updated });
7069
7018
  });
7070
7019
  task.command("delete <taskId>").description("Delete a task").option("--project <id>", "Project ID (overrides default)").action(async (taskId, opts) => {
7071
7020
  const api = createClient();
@@ -7442,7 +7391,7 @@ issueCommand.command("create <text>").description("Create a GitHub issue from na
7442
7391
  const repoArg = repo;
7443
7392
  try {
7444
7393
  if (useJson()) {
7445
- const output = await execFileAsync4("gh", args, { encoding: "utf-8", timeout: 6e4 });
7394
+ const output = await execFileAsync2("gh", args, { encoding: "utf-8", timeout: 6e4 });
7446
7395
  const url = output.stdout.trim();
7447
7396
  const issueNumber = Number.parseInt(url.split("/").pop() ?? "0", 10);
7448
7397
  jsonOut({ ok: true, data: { url, issueNumber, repo: repoArg } });
@@ -7494,7 +7443,20 @@ issueCommand.command("move <issueRef> <status>").description("Change project sta
7494
7443
  errorOut(`Invalid status "${status}". Valid: ${valid}`, { status, validStatuses: valid });
7495
7444
  }
7496
7445
  if (opts.dryRun) {
7497
- console.log(`[dry-run] Would move ${rc.shortName}#${ref.issueNumber} \u2192 "${target.name}"`);
7446
+ if (useJson()) {
7447
+ jsonOut({
7448
+ ok: true,
7449
+ dryRun: true,
7450
+ would: {
7451
+ action: "move",
7452
+ issue: ref.issueNumber,
7453
+ repo: rc.shortName,
7454
+ status: target.name
7455
+ }
7456
+ });
7457
+ } else {
7458
+ console.log(`[dry-run] Would move ${rc.shortName}#${ref.issueNumber} \u2192 "${target.name}"`);
7459
+ }
7498
7460
  return;
7499
7461
  }
7500
7462
  await updateProjectItemStatusAsync2(rc.name, ref.issueNumber, {
@@ -7517,7 +7479,15 @@ issueCommand.command("assign <issueRef>").description("Assign issue to self or a
7517
7479
  process.exit(1);
7518
7480
  }
7519
7481
  if (opts.dryRun) {
7520
- console.log(`[dry-run] Would assign ${ref.repo.shortName}#${ref.issueNumber} to @${user}`);
7482
+ if (useJson()) {
7483
+ jsonOut({
7484
+ ok: true,
7485
+ dryRun: true,
7486
+ would: { action: "assign", issue: ref.issueNumber, repo: ref.repo.shortName, user }
7487
+ });
7488
+ } else {
7489
+ console.log(`[dry-run] Would assign ${ref.repo.shortName}#${ref.issueNumber} to @${user}`);
7490
+ }
7521
7491
  return;
7522
7492
  }
7523
7493
  const { assignIssueToAsync: assignIssueToAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
@@ -7537,7 +7507,17 @@ issueCommand.command("unassign <issueRef>").description("Remove assignee from is
7537
7507
  process.exit(1);
7538
7508
  }
7539
7509
  if (opts.dryRun) {
7540
- console.log(`[dry-run] Would remove @${user} from ${ref.repo.shortName}#${ref.issueNumber}`);
7510
+ if (useJson()) {
7511
+ jsonOut({
7512
+ ok: true,
7513
+ dryRun: true,
7514
+ would: { action: "unassign", issue: ref.issueNumber, repo: ref.repo.shortName, user }
7515
+ });
7516
+ } else {
7517
+ console.log(
7518
+ `[dry-run] Would remove @${user} from ${ref.repo.shortName}#${ref.issueNumber}`
7519
+ );
7520
+ }
7541
7521
  return;
7542
7522
  }
7543
7523
  const { unassignIssueAsync: unassignIssueAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
@@ -7552,7 +7532,17 @@ issueCommand.command("comment <issueRef> <text>").description("Post a comment on
7552
7532
  const cfg = loadFullConfig();
7553
7533
  const ref = await resolveRef(issueRef, cfg);
7554
7534
  if (opts.dryRun) {
7555
- console.log(`[dry-run] Would comment on ${ref.repo.shortName}#${ref.issueNumber}: "${text}"`);
7535
+ if (useJson()) {
7536
+ jsonOut({
7537
+ ok: true,
7538
+ dryRun: true,
7539
+ would: { action: "comment", issue: ref.issueNumber, repo: ref.repo.shortName, text }
7540
+ });
7541
+ } else {
7542
+ console.log(
7543
+ `[dry-run] Would comment on ${ref.repo.shortName}#${ref.issueNumber}: "${text}"`
7544
+ );
7545
+ }
7556
7546
  return;
7557
7547
  }
7558
7548
  const { addCommentAsync: addCommentAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
@@ -7588,9 +7578,17 @@ issueCommand.command("edit <issueRef>").description("Edit issue fields (title, b
7588
7578
  process.exit(1);
7589
7579
  }
7590
7580
  if (opts.dryRun) {
7591
- console.log(
7592
- `[dry-run] Would edit ${ref.repo.shortName}#${ref.issueNumber}: ${changes.join("; ")}`
7593
- );
7581
+ if (useJson()) {
7582
+ jsonOut({
7583
+ ok: true,
7584
+ dryRun: true,
7585
+ would: { action: "edit", issue: ref.issueNumber, repo: ref.repo.shortName, changes }
7586
+ });
7587
+ } else {
7588
+ console.log(
7589
+ `[dry-run] Would edit ${ref.repo.shortName}#${ref.issueNumber}: ${changes.join("; ")}`
7590
+ );
7591
+ }
7594
7592
  return;
7595
7593
  }
7596
7594
  const ghArgs = ["issue", "edit", String(ref.issueNumber), "--repo", ref.repo.name];
@@ -7601,7 +7599,7 @@ issueCommand.command("edit <issueRef>").description("Edit issue fields (title, b
7601
7599
  if (opts.assignee) ghArgs.push("--add-assignee", opts.assignee);
7602
7600
  if (opts.removeAssignee) ghArgs.push("--remove-assignee", opts.removeAssignee);
7603
7601
  if (useJson()) {
7604
- await execFileAsync4("gh", ghArgs, { encoding: "utf-8", timeout: 3e4 });
7602
+ await execFileAsync2("gh", ghArgs, { encoding: "utf-8", timeout: 3e4 });
7605
7603
  jsonOut({ ok: true, data: { issue: ref.issueNumber, changes } });
7606
7604
  } else {
7607
7605
  execFileSync5("gh", ghArgs, { stdio: "inherit" });
@@ -7613,9 +7611,22 @@ issueCommand.command("label <issueRef> <label>").description("Add or remove a la
7613
7611
  const ref = await resolveRef(issueRef, cfg);
7614
7612
  const verb = opts.remove ? "remove" : "add";
7615
7613
  if (opts.dryRun) {
7616
- console.log(
7617
- `[dry-run] Would ${verb} label "${label}" on ${ref.repo.shortName}#${ref.issueNumber}`
7618
- );
7614
+ if (useJson()) {
7615
+ jsonOut({
7616
+ ok: true,
7617
+ dryRun: true,
7618
+ would: {
7619
+ action: `${verb}Label`,
7620
+ issue: ref.issueNumber,
7621
+ repo: ref.repo.shortName,
7622
+ label
7623
+ }
7624
+ });
7625
+ } else {
7626
+ console.log(
7627
+ `[dry-run] Would ${verb} label "${label}" on ${ref.repo.shortName}#${ref.issueNumber}`
7628
+ );
7629
+ }
7619
7630
  return;
7620
7631
  }
7621
7632
  if (opts.remove) {
@@ -7651,6 +7662,140 @@ issueCommand.command("statuses").description("List available project statuses fo
7651
7662
  console.log(`Available statuses for ${repo}: ${statuses.map((s) => s.name).join(", ")}`);
7652
7663
  }
7653
7664
  });
7665
+ async function moveSingleIssue(r, status, cfg) {
7666
+ try {
7667
+ const ref = await resolveRef(r, cfg);
7668
+ const rc = ref.repo;
7669
+ if (!(rc.statusFieldId && rc.projectNumber)) {
7670
+ throw new Error(`${rc.name} is not configured with a project board. Run: hog init`);
7671
+ }
7672
+ const { fetchProjectStatusOptions: fetchProjectStatusOptions2, updateProjectItemStatusAsync: updateProjectItemStatusAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
7673
+ const options = fetchProjectStatusOptions2(rc.name, rc.projectNumber, rc.statusFieldId);
7674
+ const target = options.find((o) => o.name.toLowerCase() === status.toLowerCase());
7675
+ if (!target) {
7676
+ const valid = options.map((o) => o.name).join(", ");
7677
+ throw new Error(`Invalid status "${status}". Valid: ${valid}`);
7678
+ }
7679
+ await updateProjectItemStatusAsync2(rc.name, ref.issueNumber, {
7680
+ projectNumber: rc.projectNumber,
7681
+ statusFieldId: rc.statusFieldId,
7682
+ optionId: target.id
7683
+ });
7684
+ return { ref: r, success: true };
7685
+ } catch (err) {
7686
+ return { ref: r, success: false, error: err instanceof Error ? err.message : String(err) };
7687
+ }
7688
+ }
7689
+ function outputBulkResults(results) {
7690
+ const allOk = results.every((r) => r.success);
7691
+ if (useJson()) {
7692
+ jsonOut({ ok: allOk, results });
7693
+ } else {
7694
+ for (const r of results) {
7695
+ if (!r.success) {
7696
+ console.error(
7697
+ `Failed ${r.ref}: ${r.error}`
7698
+ );
7699
+ }
7700
+ }
7701
+ }
7702
+ }
7703
+ issueCommand.command("bulk-assign <refs...>").description(
7704
+ "Assign multiple issues to self or a specific user (e.g., hog issue bulk-assign myrepo/42 myrepo/43)"
7705
+ ).option("--user <username>", "GitHub username to assign (default: configured assignee)").option("--dry-run", "Print what would change without mutating").action(async (refs, opts) => {
7706
+ const cfg = loadFullConfig();
7707
+ const user = opts.user ?? cfg.board.assignee;
7708
+ if (!user) {
7709
+ errorOut("no user specified. Use --user or configure board.assignee in hog init");
7710
+ }
7711
+ if (opts.dryRun) {
7712
+ if (useJson()) {
7713
+ jsonOut({ ok: true, dryRun: true, would: { action: "bulk-assign", refs, user } });
7714
+ } else {
7715
+ for (const r of refs) {
7716
+ console.log(`[dry-run] Would assign ${r} to @${user}`);
7717
+ }
7718
+ }
7719
+ return;
7720
+ }
7721
+ const { assignIssueToAsync: assignIssueToAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
7722
+ const results = [];
7723
+ for (const r of refs) {
7724
+ try {
7725
+ const ref = await resolveRef(r, cfg);
7726
+ await assignIssueToAsync2(ref.repo.name, ref.issueNumber, user);
7727
+ results.push({ ref: r, success: true });
7728
+ if (!useJson()) console.log(`Assigned ${r} to @${user}`);
7729
+ } catch (err) {
7730
+ results.push({
7731
+ ref: r,
7732
+ success: false,
7733
+ error: err instanceof Error ? err.message : String(err)
7734
+ });
7735
+ }
7736
+ }
7737
+ outputBulkResults(results);
7738
+ });
7739
+ issueCommand.command("bulk-unassign <refs...>").description(
7740
+ "Remove assignee from multiple issues (e.g., hog issue bulk-unassign myrepo/42 myrepo/43)"
7741
+ ).option("--user <username>", "GitHub username to remove (default: configured assignee)").option("--dry-run", "Print what would change without mutating").action(async (refs, opts) => {
7742
+ const cfg = loadFullConfig();
7743
+ const user = opts.user ?? cfg.board.assignee;
7744
+ if (!user) {
7745
+ errorOut("no user specified. Use --user or configure board.assignee in hog init");
7746
+ }
7747
+ if (opts.dryRun) {
7748
+ if (useJson()) {
7749
+ jsonOut({ ok: true, dryRun: true, would: { action: "bulk-unassign", refs, user } });
7750
+ } else {
7751
+ for (const r of refs) {
7752
+ console.log(`[dry-run] Would remove @${user} from ${r}`);
7753
+ }
7754
+ }
7755
+ return;
7756
+ }
7757
+ const { unassignIssueAsync: unassignIssueAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
7758
+ const results = [];
7759
+ for (const r of refs) {
7760
+ try {
7761
+ const ref = await resolveRef(r, cfg);
7762
+ await unassignIssueAsync2(ref.repo.name, ref.issueNumber, user);
7763
+ results.push({ ref: r, success: true });
7764
+ if (!useJson()) console.log(`Removed @${user} from ${r}`);
7765
+ } catch (err) {
7766
+ results.push({
7767
+ ref: r,
7768
+ success: false,
7769
+ error: err instanceof Error ? err.message : String(err)
7770
+ });
7771
+ }
7772
+ }
7773
+ outputBulkResults(results);
7774
+ });
7775
+ issueCommand.command("bulk-move <status> <refs...>").description(
7776
+ "Move multiple issues to a project status (e.g., hog issue bulk-move 'In Review' myrepo/42 myrepo/43)"
7777
+ ).option("--dry-run", "Print what would change without mutating").action(async (status, refs, opts) => {
7778
+ const cfg = loadFullConfig();
7779
+ if (opts.dryRun) {
7780
+ if (useJson()) {
7781
+ jsonOut({ ok: true, dryRun: true, would: { action: "bulk-move", refs, status } });
7782
+ } else {
7783
+ for (const r of refs) {
7784
+ console.log(`[dry-run] Would move ${r} \u2192 "${status}"`);
7785
+ }
7786
+ }
7787
+ return;
7788
+ }
7789
+ const results = await Promise.all(
7790
+ refs.map((r) => moveSingleIssue(r, status, cfg))
7791
+ );
7792
+ if (!useJson()) {
7793
+ for (const r of results) {
7794
+ if (r.success) console.log(`Moved ${r.ref} \u2192 ${status}`);
7795
+ }
7796
+ }
7797
+ outputBulkResults(results);
7798
+ });
7654
7799
  program.addCommand(issueCommand);
7655
7800
  var logCommand = program.command("log").description("Action log commands");
7656
7801
  logCommand.command("show").description("Show recent action log entries").option("--limit <n>", "number of entries to show", "50").action((opts) => {