@obsfx/trekker 1.8.0 → 1.10.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.
Files changed (2) hide show
  1. package/dist/index.js +512 -295
  2. package/package.json +13 -2
package/dist/index.js CHANGED
@@ -493,8 +493,8 @@ var epics = sqliteTable("epics", {
493
493
  projectId: text("project_id").notNull(),
494
494
  title: text("title").notNull(),
495
495
  description: text("description"),
496
- status: text("status").notNull().default("todo"),
497
- priority: integer("priority").notNull().default(2),
496
+ status: text("status").notNull().default("todo").$type(),
497
+ priority: integer("priority").notNull().default(2).$type(),
498
498
  createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
499
499
  updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
500
500
  });
@@ -505,8 +505,8 @@ var tasks = sqliteTable("tasks", {
505
505
  parentTaskId: text("parent_task_id"),
506
506
  title: text("title").notNull(),
507
507
  description: text("description"),
508
- priority: integer("priority").notNull().default(2),
509
- status: text("status").notNull().default("todo"),
508
+ priority: integer("priority").notNull().default(2).$type(),
509
+ status: text("status").notNull().default("todo").$type(),
510
510
  tags: text("tags"),
511
511
  createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
512
512
  updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
@@ -628,14 +628,15 @@ function createDb(cwd = process.cwd()) {
628
628
  const dbPath = getDbPath(cwd);
629
629
  sqliteInstance = new Database(dbPath);
630
630
  dbInstance = drizzle(sqliteInstance, { schema: exports_schema });
631
- sqliteInstance.exec(`
631
+ sqliteInstance.run(`
632
632
  CREATE TABLE IF NOT EXISTS projects (
633
633
  id TEXT PRIMARY KEY,
634
634
  name TEXT NOT NULL UNIQUE,
635
635
  created_at INTEGER NOT NULL,
636
636
  updated_at INTEGER NOT NULL
637
- );
638
-
637
+ )
638
+ `);
639
+ sqliteInstance.run(`
639
640
  CREATE TABLE IF NOT EXISTS epics (
640
641
  id TEXT PRIMARY KEY,
641
642
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
@@ -645,8 +646,9 @@ function createDb(cwd = process.cwd()) {
645
646
  priority INTEGER NOT NULL DEFAULT 2,
646
647
  created_at INTEGER NOT NULL,
647
648
  updated_at INTEGER NOT NULL
648
- );
649
-
649
+ )
650
+ `);
651
+ sqliteInstance.run(`
650
652
  CREATE TABLE IF NOT EXISTS tasks (
651
653
  id TEXT PRIMARY KEY,
652
654
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
@@ -659,8 +661,9 @@ function createDb(cwd = process.cwd()) {
659
661
  tags TEXT,
660
662
  created_at INTEGER NOT NULL,
661
663
  updated_at INTEGER NOT NULL
662
- );
663
-
664
+ )
665
+ `);
666
+ sqliteInstance.run(`
664
667
  CREATE TABLE IF NOT EXISTS comments (
665
668
  id TEXT PRIMARY KEY,
666
669
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -668,26 +671,26 @@ function createDb(cwd = process.cwd()) {
668
671
  content TEXT NOT NULL,
669
672
  created_at INTEGER NOT NULL,
670
673
  updated_at INTEGER NOT NULL
671
- );
672
-
674
+ )
675
+ `);
676
+ sqliteInstance.run(`
673
677
  CREATE TABLE IF NOT EXISTS dependencies (
674
678
  id TEXT PRIMARY KEY,
675
679
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
676
680
  depends_on_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
677
681
  created_at INTEGER NOT NULL
678
- );
679
-
682
+ )
683
+ `);
684
+ sqliteInstance.run(`
680
685
  CREATE TABLE IF NOT EXISTS id_counters (
681
686
  entity_type TEXT PRIMARY KEY,
682
687
  counter INTEGER NOT NULL DEFAULT 0
683
- );
684
-
685
- -- Initialize counters
686
- INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('task', 0);
687
- INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('epic', 0);
688
- INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('comment', 0);
689
-
690
- -- Events table for history/logbook
688
+ )
689
+ `);
690
+ sqliteInstance.run("INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('task', 0)");
691
+ sqliteInstance.run("INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('epic', 0)");
692
+ sqliteInstance.run("INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('comment', 0)");
693
+ sqliteInstance.run(`
691
694
  CREATE TABLE IF NOT EXISTS events (
692
695
  id INTEGER PRIMARY KEY AUTOINCREMENT,
693
696
  action TEXT NOT NULL,
@@ -696,12 +699,11 @@ function createDb(cwd = process.cwd()) {
696
699
  snapshot TEXT,
697
700
  changes TEXT,
698
701
  created_at INTEGER NOT NULL
699
- );
700
-
701
- CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_id);
702
- CREATE INDEX IF NOT EXISTS idx_events_type_action ON events(entity_type, action);
703
- CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
702
+ )
704
703
  `);
704
+ sqliteInstance.run("CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_id)");
705
+ sqliteInstance.run("CREATE INDEX IF NOT EXISTS idx_events_type_action ON events(entity_type, action)");
706
+ sqliteInstance.run("CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at)");
705
707
  createSearchIndex(sqliteInstance);
706
708
  createHistoryTriggers(sqliteInstance);
707
709
  return dbInstance;
@@ -989,19 +991,8 @@ function deleteDb(cwd = process.cwd()) {
989
991
  import { eq, sql } from "drizzle-orm";
990
992
 
991
993
  // src/types/index.ts
992
- var TASK_STATUSES = [
993
- "todo",
994
- "in_progress",
995
- "completed",
996
- "wont_fix",
997
- "archived"
998
- ];
999
- var EPIC_STATUSES = [
1000
- "todo",
1001
- "in_progress",
1002
- "completed",
1003
- "archived"
1004
- ];
994
+ var TASK_STATUSES = ["todo", "in_progress", "completed", "wont_fix", "archived"];
995
+ var EPIC_STATUSES = ["todo", "in_progress", "completed", "archived"];
1005
996
  var DEFAULT_PRIORITY = 2;
1006
997
  var DEFAULT_TASK_STATUS = "todo";
1007
998
  var DEFAULT_EPIC_STATUS = "todo";
@@ -1011,13 +1002,7 @@ var PAGINATION_DEFAULTS = {
1011
1002
  HISTORY_PAGE_SIZE: 50,
1012
1003
  DEFAULT_PAGE: 1
1013
1004
  };
1014
- var VALID_SORT_FIELDS = [
1015
- "created",
1016
- "updated",
1017
- "title",
1018
- "priority",
1019
- "status"
1020
- ];
1005
+ var VALID_SORT_FIELDS = ["created", "updated", "title", "priority", "status"];
1021
1006
  var LIST_ENTITY_TYPES = ["epic", "task", "subtask"];
1022
1007
  var SEARCH_ENTITY_TYPES = ["epic", "task", "subtask", "comment"];
1023
1008
  var PREFIX_MAP = {
@@ -1545,6 +1530,19 @@ function resolveOptions(options) {
1545
1530
  };
1546
1531
  }
1547
1532
 
1533
+ // src/utils/constants.ts
1534
+ var STATUS_PAD_WIDTH = 11;
1535
+ var TYPE_PAD_WIDTH = 7;
1536
+ var JSON_INDENT = 2;
1537
+ var MS_PER_SECOND = 1000;
1538
+ var MAX_PRIORITY = 5;
1539
+ var ACTION_PAD_WIDTH = 6;
1540
+ var RADIX_DECIMAL = 10;
1541
+ var TRUNCATE_OFFSET = 3;
1542
+ var TIMESTAMP_SLICE_END = 19;
1543
+ var TRUNCATE_DEFAULT = 40;
1544
+ var TRUNCATE_CONTENT = 60;
1545
+
1548
1546
  // src/utils/output.ts
1549
1547
  var toonMode = false;
1550
1548
  function setToonMode(enabled) {
@@ -1559,7 +1557,7 @@ function output(data) {
1559
1557
  } else if (typeof data === "string") {
1560
1558
  console.log(data);
1561
1559
  } else {
1562
- console.log(JSON.stringify(data, null, 2));
1560
+ console.log(JSON.stringify(data, null, JSON_INDENT));
1563
1561
  }
1564
1562
  }
1565
1563
  function success(message, data) {
@@ -1583,7 +1581,11 @@ function error(message, details) {
1583
1581
  }
1584
1582
  }
1585
1583
  function handleCommandError(err) {
1586
- error(err instanceof Error ? err.message : String(err));
1584
+ if (err instanceof Error) {
1585
+ error(err.message);
1586
+ } else {
1587
+ error(String(err));
1588
+ }
1587
1589
  process.exit(1);
1588
1590
  }
1589
1591
  function handleNotFound(entityType, id) {
@@ -1593,12 +1595,12 @@ function handleNotFound(entityType, id) {
1593
1595
  function outputResult(data, formatter, successMessage) {
1594
1596
  if (isToonMode()) {
1595
1597
  output(data);
1596
- } else {
1597
- if (successMessage) {
1598
- success(successMessage);
1599
- }
1600
- console.log(formatter(data));
1598
+ return;
1599
+ }
1600
+ if (successMessage) {
1601
+ success(successMessage);
1601
1602
  }
1603
+ console.log(formatter(data));
1602
1604
  }
1603
1605
  function info(message) {
1604
1606
  if (!toonMode) {
@@ -1654,46 +1656,83 @@ function formatComment(comment) {
1654
1656
  return lines.join(`
1655
1657
  `);
1656
1658
  }
1657
- function formatTaskList(tasks2) {
1658
- if (tasks2.length === 0) {
1659
- return "No tasks found.";
1659
+ function formatDependencyList(dependencies2, direction) {
1660
+ if (dependencies2.length === 0) {
1661
+ if (direction === "depends_on") {
1662
+ return "No dependencies.";
1663
+ }
1664
+ return "Does not block any tasks.";
1660
1665
  }
1661
- const lines = tasks2.map((task) => {
1662
- const tags = task.tags ? ` [${task.tags}]` : "";
1663
- const parent = task.parentTaskId ? ` (subtask of ${task.parentTaskId})` : "";
1664
- return `${task.id} | ${task.status.padEnd(11)} | P${task.priority} | ${task.title}${tags}${parent}`;
1665
- });
1666
- return lines.join(`
1666
+ if (direction === "depends_on") {
1667
+ return dependencies2.map((d) => ` \u2192 depends on ${d.dependsOnId}`).join(`
1668
+ `);
1669
+ }
1670
+ return dependencies2.map((d) => ` \u2192 blocks ${d.taskId}`).join(`
1667
1671
  `);
1668
1672
  }
1669
- function formatEpicList(epics2) {
1670
- if (epics2.length === 0) {
1671
- return "No epics found.";
1673
+ function formatPaginationFooter(total, page, limit) {
1674
+ const totalPages = Math.ceil(total / limit);
1675
+ if (totalPages > 1) {
1676
+ return `
1677
+ Page ${page} of ${totalPages}`;
1672
1678
  }
1673
- const lines = epics2.map((epic) => {
1674
- return `${epic.id} | ${epic.status.padEnd(11)} | P${epic.priority} | ${epic.title}`;
1675
- });
1679
+ return "";
1680
+ }
1681
+ function formatPaginatedTaskList(result) {
1682
+ const lines = [];
1683
+ lines.push(`${result.total} task(s) (page ${result.page}, ${result.limit} per page)
1684
+ `);
1685
+ if (result.items.length === 0) {
1686
+ lines.push("No tasks found.");
1687
+ return lines.join(`
1688
+ `);
1689
+ }
1690
+ for (const task of result.items) {
1691
+ let tags = "";
1692
+ if (task.tags) {
1693
+ tags = ` [${task.tags}]`;
1694
+ }
1695
+ let parent = "";
1696
+ if (task.parentTaskId) {
1697
+ parent = ` (subtask of ${task.parentTaskId})`;
1698
+ }
1699
+ lines.push(`${task.id} | ${task.status.padEnd(STATUS_PAD_WIDTH)} | P${task.priority} | ${task.title}${tags}${parent}`);
1700
+ }
1701
+ lines.push(formatPaginationFooter(result.total, result.page, result.limit));
1676
1702
  return lines.join(`
1677
1703
  `);
1678
1704
  }
1679
- function formatCommentList(comments2) {
1680
- if (comments2.length === 0) {
1681
- return "No comments found.";
1705
+ function formatPaginatedEpicList(result) {
1706
+ const lines = [];
1707
+ lines.push(`${result.total} epic(s) (page ${result.page}, ${result.limit} per page)
1708
+ `);
1709
+ if (result.items.length === 0) {
1710
+ lines.push("No epics found.");
1711
+ return lines.join(`
1712
+ `);
1682
1713
  }
1683
- return comments2.map((c) => `[${c.id}] ${c.author}: ${c.content}`).join(`
1714
+ for (const epic of result.items) {
1715
+ lines.push(`${epic.id} | ${epic.status.padEnd(STATUS_PAD_WIDTH)} | P${epic.priority} | ${epic.title}`);
1716
+ }
1717
+ lines.push(formatPaginationFooter(result.total, result.page, result.limit));
1718
+ return lines.join(`
1684
1719
  `);
1685
1720
  }
1686
- function formatDependencyList(dependencies2, direction) {
1687
- if (dependencies2.length === 0) {
1688
- return direction === "depends_on" ? "No dependencies." : "Does not block any tasks.";
1689
- }
1690
- if (direction === "depends_on") {
1691
- return dependencies2.map((d) => ` \u2192 depends on ${d.dependsOnId}`).join(`
1721
+ function formatPaginatedCommentList(result) {
1722
+ const lines = [];
1723
+ lines.push(`${result.total} comment(s) (page ${result.page}, ${result.limit} per page)
1692
1724
  `);
1693
- } else {
1694
- return dependencies2.map((d) => ` \u2192 blocks ${d.taskId}`).join(`
1725
+ if (result.items.length === 0) {
1726
+ lines.push("No comments found.");
1727
+ return lines.join(`
1695
1728
  `);
1696
1729
  }
1730
+ for (const c of result.items) {
1731
+ lines.push(`[${c.id}] ${c.author}: ${c.content}`);
1732
+ }
1733
+ lines.push(formatPaginationFooter(result.total, result.page, result.limit));
1734
+ return lines.join(`
1735
+ `);
1697
1736
  }
1698
1737
 
1699
1738
  // src/commands/init.ts
@@ -1706,8 +1745,7 @@ var initCommand = new Command("init").description("Initialize Trekker in the cur
1706
1745
  initProject();
1707
1746
  success("Trekker initialized successfully.");
1708
1747
  } catch (err) {
1709
- error(err instanceof Error ? err.message : String(err));
1710
- process.exit(1);
1748
+ handleCommandError(err);
1711
1749
  }
1712
1750
  });
1713
1751
 
@@ -1730,8 +1768,7 @@ var wipeCommand = new Command2("wipe").description("Delete all Trekker data in t
1730
1768
  wipeProject();
1731
1769
  success("Trekker data deleted successfully.");
1732
1770
  } catch (err) {
1733
- error(err instanceof Error ? err.message : String(err));
1734
- process.exit(1);
1771
+ handleCommandError(err);
1735
1772
  }
1736
1773
  });
1737
1774
  function confirm(prompt) {
@@ -1751,7 +1788,7 @@ function confirm(prompt) {
1751
1788
  import { Command as Command3 } from "commander";
1752
1789
 
1753
1790
  // src/services/epic.ts
1754
- import { eq as eq2, and, isNull } from "drizzle-orm";
1791
+ import { eq as eq2, and, isNull, desc, sql as sql2 } from "drizzle-orm";
1755
1792
  function createEpic(input) {
1756
1793
  const db = getDb();
1757
1794
  const project = db.select().from(projects).get();
@@ -1775,15 +1812,21 @@ function createEpic(input) {
1775
1812
  }
1776
1813
  function getEpic(id) {
1777
1814
  const db = getDb();
1778
- const result = db.select().from(epics).where(eq2(epics.id, id)).get();
1779
- return result;
1815
+ return db.select().from(epics).where(eq2(epics.id, id)).get();
1780
1816
  }
1781
- function listEpics(status) {
1817
+ function listEpics(options) {
1782
1818
  const db = getDb();
1783
- if (status) {
1784
- return db.select().from(epics).where(eq2(epics.status, status)).all();
1819
+ const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
1820
+ const page = options?.page ?? PAGINATION_DEFAULTS.DEFAULT_PAGE;
1821
+ const offset = (page - 1) * limit;
1822
+ let where;
1823
+ if (options?.status) {
1824
+ where = eq2(epics.status, options.status);
1785
1825
  }
1786
- return db.select().from(epics).all();
1826
+ const countRow = db.select({ count: sql2`count(*)` }).from(epics).where(where).get();
1827
+ const total = countRow?.count ?? 0;
1828
+ const items = db.select().from(epics).where(where).orderBy(desc(epics.createdAt)).limit(limit).offset(offset).all();
1829
+ return { total, page, limit, items };
1787
1830
  }
1788
1831
  function updateEpic(id, input) {
1789
1832
  const db = getDb();
@@ -1794,16 +1837,24 @@ function updateEpic(id, input) {
1794
1837
  const updates = {
1795
1838
  updatedAt: new Date
1796
1839
  };
1797
- if (input.title !== undefined)
1840
+ if (input.title !== undefined) {
1798
1841
  updates.title = input.title;
1799
- if (input.description !== undefined)
1842
+ }
1843
+ if (input.description !== undefined) {
1800
1844
  updates.description = input.description;
1801
- if (input.status !== undefined)
1845
+ }
1846
+ if (input.status !== undefined) {
1802
1847
  updates.status = input.status;
1803
- if (input.priority !== undefined)
1848
+ }
1849
+ if (input.priority !== undefined) {
1804
1850
  updates.priority = input.priority;
1851
+ }
1805
1852
  db.update(epics).set(updates).where(eq2(epics.id, id)).run();
1806
- return getEpic(id);
1853
+ const updated = getEpic(id);
1854
+ if (!updated) {
1855
+ throw new Error(`Epic not found after update: ${id}`);
1856
+ }
1857
+ return updated;
1807
1858
  }
1808
1859
  function deleteEpic(id) {
1809
1860
  const db = getDb();
@@ -1847,36 +1898,41 @@ function completeEpic(id) {
1847
1898
  }
1848
1899
 
1849
1900
  // src/utils/validator.ts
1901
+ var TASK_STATUS_SET = new Set(TASK_STATUSES);
1902
+ var EPIC_STATUS_SET = new Set(EPIC_STATUSES);
1903
+ var LIST_ENTITY_TYPE_SET = new Set(LIST_ENTITY_TYPES);
1904
+ var SEARCH_ENTITY_TYPE_SET = new Set(SEARCH_ENTITY_TYPES);
1850
1905
  function isValidTaskStatus(status) {
1851
- return TASK_STATUSES.includes(status);
1906
+ return TASK_STATUS_SET.has(status);
1852
1907
  }
1853
1908
  function isValidEpicStatus(status) {
1854
- return EPIC_STATUSES.includes(status);
1909
+ return EPIC_STATUS_SET.has(status);
1855
1910
  }
1856
1911
  function isValidPriority(priority) {
1857
- return Number.isInteger(priority) && priority >= 0 && priority <= 5;
1912
+ return Number.isInteger(priority) && priority >= 0 && priority <= MAX_PRIORITY;
1858
1913
  }
1859
1914
  function parseStatus(status, type) {
1860
- if (!status)
1915
+ if (!status) {
1861
1916
  return;
1917
+ }
1862
1918
  const normalizedStatus = status.toLowerCase().replace(/-/g, "_");
1863
1919
  if (type === "task") {
1864
1920
  if (!isValidTaskStatus(normalizedStatus)) {
1865
1921
  throw new Error(`Invalid task status: ${status}. Valid values: ${TASK_STATUSES.join(", ")}`);
1866
1922
  }
1867
1923
  return normalizedStatus;
1868
- } else {
1869
- if (!isValidEpicStatus(normalizedStatus)) {
1870
- throw new Error(`Invalid epic status: ${status}. Valid values: ${EPIC_STATUSES.join(", ")}`);
1871
- }
1872
- return normalizedStatus;
1873
1924
  }
1925
+ if (!isValidEpicStatus(normalizedStatus)) {
1926
+ throw new Error(`Invalid epic status: ${status}. Valid values: ${EPIC_STATUSES.join(", ")}`);
1927
+ }
1928
+ return normalizedStatus;
1874
1929
  }
1875
1930
  function parsePriority(priority) {
1876
- if (priority === undefined)
1931
+ if (priority === undefined) {
1877
1932
  return;
1878
- const num = parseInt(priority, 10);
1879
- if (isNaN(num) || !isValidPriority(num)) {
1933
+ }
1934
+ const num = Number.parseInt(priority, RADIX_DECIMAL);
1935
+ if (Number.isNaN(num) || !isValidPriority(num)) {
1880
1936
  throw new Error(`Invalid priority: ${priority}. Must be a number between 0 and 5.`);
1881
1937
  }
1882
1938
  return num;
@@ -1887,34 +1943,68 @@ function validateRequired(value, fieldName) {
1887
1943
  }
1888
1944
  }
1889
1945
  function validatePagination(limit, page) {
1890
- if (isNaN(limit) || limit < 1) {
1946
+ if (Number.isNaN(limit) || limit < 1) {
1891
1947
  throw new Error("Invalid limit value");
1892
1948
  }
1893
- if (isNaN(page) || page < 1) {
1949
+ if (Number.isNaN(page) || page < 1) {
1894
1950
  throw new Error("Invalid page value");
1895
1951
  }
1896
1952
  }
1953
+ function parsePaginationOptions(opts) {
1954
+ const limit = Number.parseInt(opts.limit, RADIX_DECIMAL);
1955
+ const page = Number.parseInt(opts.page, RADIX_DECIMAL);
1956
+ validatePagination(limit, page);
1957
+ return { limit, page };
1958
+ }
1897
1959
  function validateListEntityTypes(types) {
1898
1960
  for (const t of types) {
1899
- if (!LIST_ENTITY_TYPES.includes(t)) {
1961
+ if (!LIST_ENTITY_TYPE_SET.has(t)) {
1900
1962
  throw new Error(`Invalid type: ${t}. Valid types: ${LIST_ENTITY_TYPES.join(", ")}`);
1901
1963
  }
1902
1964
  }
1903
1965
  }
1904
1966
  function validateSearchEntityTypes(types) {
1905
1967
  for (const t of types) {
1906
- if (!SEARCH_ENTITY_TYPES.includes(t)) {
1968
+ if (!SEARCH_ENTITY_TYPE_SET.has(t)) {
1907
1969
  throw new Error(`Invalid type: ${t}. Valid types: ${SEARCH_ENTITY_TYPES.join(", ")}`);
1908
1970
  }
1909
1971
  }
1910
1972
  }
1911
1973
  function validatePriorities(priorities) {
1912
1974
  for (const p of priorities) {
1913
- if (isNaN(p) || p < 0 || p > 5) {
1975
+ if (Number.isNaN(p) || p < 0 || p > MAX_PRIORITY) {
1914
1976
  throw new Error(`Invalid priority: ${p}. Valid priorities: 0-5`);
1915
1977
  }
1916
1978
  }
1917
1979
  }
1980
+ var VALID_HISTORY_TYPES = new Set([
1981
+ "epic",
1982
+ "task",
1983
+ "subtask",
1984
+ "comment",
1985
+ "dependency"
1986
+ ]);
1987
+ var VALID_HISTORY_ACTIONS = new Set(["create", "update", "delete"]);
1988
+ function validateHistoryTypes(types) {
1989
+ for (const t of types) {
1990
+ if (!VALID_HISTORY_TYPES.has(t)) {
1991
+ throw new Error(`Invalid type: ${t}. Valid types: ${[...VALID_HISTORY_TYPES].join(", ")}`);
1992
+ }
1993
+ }
1994
+ }
1995
+ function validateHistoryActions(actions) {
1996
+ for (const a of actions) {
1997
+ if (!VALID_HISTORY_ACTIONS.has(a)) {
1998
+ throw new Error(`Invalid action: ${a}. Valid actions: ${[...VALID_HISTORY_ACTIONS].join(", ")}`);
1999
+ }
2000
+ }
2001
+ }
2002
+ function parseCommaSeparated(input) {
2003
+ if (!input) {
2004
+ return;
2005
+ }
2006
+ return input.split(",").map((s) => s.trim());
2007
+ }
1918
2008
 
1919
2009
  // src/commands/epic.ts
1920
2010
  var epicCommand = new Command3("epic").description("Manage epics");
@@ -1932,11 +2022,12 @@ epicCommand.command("create").description("Create a new epic").requiredOption("-
1932
2022
  handleCommandError(err);
1933
2023
  }
1934
2024
  });
1935
- epicCommand.command("list").description("List all epics").option("-s, --status <status>", "Filter by status").action((options) => {
2025
+ epicCommand.command("list").description("List all epics").option("-s, --status <status>", "Filter by status").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((options) => {
1936
2026
  try {
1937
2027
  const status = parseStatus(options.status, "epic");
1938
- const epics2 = listEpics(status);
1939
- outputResult(epics2, formatEpicList);
2028
+ const { limit, page } = parsePaginationOptions(options);
2029
+ const result = listEpics({ status, limit, page });
2030
+ outputResult(result, formatPaginatedEpicList);
1940
2031
  } catch (err) {
1941
2032
  handleCommandError(err);
1942
2033
  }
@@ -1944,8 +2035,9 @@ epicCommand.command("list").description("List all epics").option("-s, --status <
1944
2035
  epicCommand.command("show <epic-id>").description("Show epic details").action((epicId) => {
1945
2036
  try {
1946
2037
  const epic = getEpic(epicId);
1947
- if (!epic)
2038
+ if (!epic) {
1948
2039
  return handleNotFound("Epic", epicId);
2040
+ }
1949
2041
  outputResult(epic, formatEpic);
1950
2042
  } catch (err) {
1951
2043
  handleCommandError(err);
@@ -1990,7 +2082,7 @@ epicCommand.command("complete <epic-id>").description("Complete an epic and arch
1990
2082
  import { Command as Command4 } from "commander";
1991
2083
 
1992
2084
  // src/services/task.ts
1993
- import { eq as eq3, and as and2, isNull as isNull2 } from "drizzle-orm";
2085
+ import { eq as eq3, and as and2, isNull as isNull2, desc as desc2, sql as sql3 } from "drizzle-orm";
1994
2086
  function createTask(input) {
1995
2087
  const db = getDb();
1996
2088
  const project = db.select().from(projects).get();
@@ -2029,11 +2121,13 @@ function createTask(input) {
2029
2121
  }
2030
2122
  function getTask(id) {
2031
2123
  const db = getDb();
2032
- const result = db.select().from(tasks).where(eq3(tasks.id, id)).get();
2033
- return result;
2124
+ return db.select().from(tasks).where(eq3(tasks.id, id)).get();
2034
2125
  }
2035
2126
  function listTasks(options) {
2036
2127
  const db = getDb();
2128
+ const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
2129
+ const page = options?.page ?? PAGINATION_DEFAULTS.DEFAULT_PAGE;
2130
+ const offset = (page - 1) * limit;
2037
2131
  const conditions = [];
2038
2132
  if (options?.status) {
2039
2133
  conditions.push(eq3(tasks.status, options.status));
@@ -2046,14 +2140,25 @@ function listTasks(options) {
2046
2140
  } else if (options?.parentTaskId) {
2047
2141
  conditions.push(eq3(tasks.parentTaskId, options.parentTaskId));
2048
2142
  }
2143
+ let where;
2049
2144
  if (conditions.length > 0) {
2050
- return db.select().from(tasks).where(and2(...conditions)).all();
2145
+ where = and2(...conditions);
2051
2146
  }
2052
- return db.select().from(tasks).all();
2147
+ const countRow = db.select({ count: sql3`count(*)` }).from(tasks).where(where).get();
2148
+ const total = countRow?.count ?? 0;
2149
+ const items = db.select().from(tasks).where(where).orderBy(desc2(tasks.createdAt)).limit(limit).offset(offset).all();
2150
+ return { total, page, limit, items };
2053
2151
  }
2054
- function listSubtasks(parentTaskId) {
2152
+ function listSubtasks(parentTaskId, options) {
2055
2153
  const db = getDb();
2056
- return db.select().from(tasks).where(eq3(tasks.parentTaskId, parentTaskId)).all();
2154
+ const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
2155
+ const page = options?.page ?? PAGINATION_DEFAULTS.DEFAULT_PAGE;
2156
+ const offset = (page - 1) * limit;
2157
+ const where = eq3(tasks.parentTaskId, parentTaskId);
2158
+ const countRow = db.select({ count: sql3`count(*)` }).from(tasks).where(where).get();
2159
+ const total = countRow?.count ?? 0;
2160
+ const items = db.select().from(tasks).where(where).orderBy(desc2(tasks.createdAt)).limit(limit).offset(offset).all();
2161
+ return { total, page, limit, items };
2057
2162
  }
2058
2163
  function updateTask(id, input) {
2059
2164
  const db = getDb();
@@ -2070,20 +2175,30 @@ function updateTask(id, input) {
2070
2175
  const updates = {
2071
2176
  updatedAt: new Date
2072
2177
  };
2073
- if (input.title !== undefined)
2178
+ if (input.title !== undefined) {
2074
2179
  updates.title = input.title;
2075
- if (input.description !== undefined)
2180
+ }
2181
+ if (input.description !== undefined) {
2076
2182
  updates.description = input.description;
2077
- if (input.priority !== undefined)
2183
+ }
2184
+ if (input.priority !== undefined) {
2078
2185
  updates.priority = input.priority;
2079
- if (input.status !== undefined)
2186
+ }
2187
+ if (input.status !== undefined) {
2080
2188
  updates.status = input.status;
2081
- if (input.tags !== undefined)
2189
+ }
2190
+ if (input.tags !== undefined) {
2082
2191
  updates.tags = input.tags;
2083
- if (input.epicId !== undefined)
2192
+ }
2193
+ if (input.epicId !== undefined) {
2084
2194
  updates.epicId = input.epicId;
2195
+ }
2085
2196
  db.update(tasks).set(updates).where(eq3(tasks.id, id)).run();
2086
- return getTask(id);
2197
+ const updated = getTask(id);
2198
+ if (!updated) {
2199
+ throw new Error(`Task not found after update: ${id}`);
2200
+ }
2201
+ return updated;
2087
2202
  }
2088
2203
  function deleteTask(id) {
2089
2204
  const db = getDb();
@@ -2112,15 +2227,18 @@ taskCommand.command("create").description("Create a new task").requiredOption("-
2112
2227
  handleCommandError(err);
2113
2228
  }
2114
2229
  });
2115
- taskCommand.command("list").description("List all tasks").option("-s, --status <status>", "Filter by status").option("-e, --epic <epic-id>", "Filter by epic").action((options) => {
2230
+ taskCommand.command("list").description("List all tasks").option("-s, --status <status>", "Filter by status").option("-e, --epic <epic-id>", "Filter by epic").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((options) => {
2116
2231
  try {
2117
2232
  const status = parseStatus(options.status, "task");
2118
- const tasks2 = listTasks({
2233
+ const { limit, page } = parsePaginationOptions(options);
2234
+ const result = listTasks({
2119
2235
  status,
2120
2236
  epicId: options.epic,
2121
- parentTaskId: null
2237
+ parentTaskId: null,
2238
+ limit,
2239
+ page
2122
2240
  });
2123
- outputResult(tasks2, formatTaskList);
2241
+ outputResult(result, formatPaginatedTaskList);
2124
2242
  } catch (err) {
2125
2243
  handleCommandError(err);
2126
2244
  }
@@ -2128,8 +2246,9 @@ taskCommand.command("list").description("List all tasks").option("-s, --status <
2128
2246
  taskCommand.command("show <task-id>").description("Show task details").action((taskId) => {
2129
2247
  try {
2130
2248
  const task = getTask(taskId);
2131
- if (!task)
2249
+ if (!task) {
2132
2250
  return handleNotFound("Task", taskId);
2251
+ }
2133
2252
  outputResult(task, formatTask);
2134
2253
  } catch (err) {
2135
2254
  handleCommandError(err);
@@ -2138,16 +2257,21 @@ taskCommand.command("show <task-id>").description("Show task details").action((t
2138
2257
  taskCommand.command("update <task-id>").description("Update a task").option("-t, --title <title>", "New title").option("-d, --description <description>", "New description").option("-p, --priority <priority>", "New priority (0-5)").option("-s, --status <status>", "New status").option("--tags <tags>", "New tags (comma-separated)").option("-e, --epic <epic-id>", "New epic ID").option("--no-epic", "Remove from epic").action((taskId, options) => {
2139
2258
  try {
2140
2259
  const updateInput = {};
2141
- if (options.title !== undefined)
2260
+ if (options.title !== undefined) {
2142
2261
  updateInput.title = options.title;
2143
- if (options.description !== undefined)
2262
+ }
2263
+ if (options.description !== undefined) {
2144
2264
  updateInput.description = options.description;
2145
- if (options.priority !== undefined)
2265
+ }
2266
+ if (options.priority !== undefined) {
2146
2267
  updateInput.priority = parsePriority(options.priority);
2147
- if (options.status !== undefined)
2268
+ }
2269
+ if (options.status !== undefined) {
2148
2270
  updateInput.status = parseStatus(options.status, "task");
2149
- if (options.tags !== undefined)
2271
+ }
2272
+ if (options.tags !== undefined) {
2150
2273
  updateInput.tags = options.tags;
2274
+ }
2151
2275
  if (options.epic === false) {
2152
2276
  updateInput.epicId = null;
2153
2277
  } else if (options.epic !== undefined) {
@@ -2175,8 +2299,9 @@ subtaskCommand.command("create <parent-task-id>").description("Create a new subt
2175
2299
  try {
2176
2300
  validateRequired(options.title, "Title");
2177
2301
  const parent = getTask(parentTaskId);
2178
- if (!parent)
2302
+ if (!parent) {
2179
2303
  return handleNotFound("Parent task", parentTaskId);
2304
+ }
2180
2305
  const subtask = createTask({
2181
2306
  title: options.title,
2182
2307
  description: options.description,
@@ -2190,22 +2315,15 @@ subtaskCommand.command("create <parent-task-id>").description("Create a new subt
2190
2315
  handleCommandError(err);
2191
2316
  }
2192
2317
  });
2193
- subtaskCommand.command("list <parent-task-id>").description("List all subtasks of a task").action((parentTaskId) => {
2318
+ subtaskCommand.command("list <parent-task-id>").description("List all subtasks of a task").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((parentTaskId, options) => {
2194
2319
  try {
2195
2320
  const parent = getTask(parentTaskId);
2196
- if (!parent)
2321
+ if (!parent) {
2197
2322
  return handleNotFound("Parent task", parentTaskId);
2198
- const subtasks = listSubtasks(parentTaskId);
2199
- if (isToonMode()) {
2200
- output(subtasks);
2201
- } else {
2202
- if (subtasks.length === 0) {
2203
- console.log(`No subtasks for ${parentTaskId}`);
2204
- } else {
2205
- console.log(`Subtasks of ${parentTaskId}:`);
2206
- console.log(formatTaskList(subtasks));
2207
- }
2208
2323
  }
2324
+ const { limit, page } = parsePaginationOptions(options);
2325
+ const result = listSubtasks(parentTaskId, { limit, page });
2326
+ outputResult(result, formatPaginatedTaskList);
2209
2327
  } catch (err) {
2210
2328
  handleCommandError(err);
2211
2329
  }
@@ -2213,21 +2331,26 @@ subtaskCommand.command("list <parent-task-id>").description("List all subtasks o
2213
2331
  subtaskCommand.command("update <subtask-id>").description("Update a subtask").option("-t, --title <title>", "New title").option("-d, --description <description>", "New description").option("-p, --priority <priority>", "New priority (0-5)").option("-s, --status <status>", "New status").action((subtaskId, options) => {
2214
2332
  try {
2215
2333
  const subtask = getTask(subtaskId);
2216
- if (!subtask)
2334
+ if (!subtask) {
2217
2335
  return handleNotFound("Subtask", subtaskId);
2336
+ }
2218
2337
  if (!subtask.parentTaskId) {
2219
2338
  error(`${subtaskId} is not a subtask. Use 'trekker task update' instead.`);
2220
2339
  process.exit(1);
2221
2340
  }
2222
2341
  const updateInput = {};
2223
- if (options.title !== undefined)
2342
+ if (options.title !== undefined) {
2224
2343
  updateInput.title = options.title;
2225
- if (options.description !== undefined)
2344
+ }
2345
+ if (options.description !== undefined) {
2226
2346
  updateInput.description = options.description;
2227
- if (options.priority !== undefined)
2347
+ }
2348
+ if (options.priority !== undefined) {
2228
2349
  updateInput.priority = parsePriority(options.priority);
2229
- if (options.status !== undefined)
2350
+ }
2351
+ if (options.status !== undefined) {
2230
2352
  updateInput.status = parseStatus(options.status, "task");
2353
+ }
2231
2354
  const updated = updateTask(subtaskId, updateInput);
2232
2355
  outputResult(updated, formatTask, `Subtask updated: ${updated.id}`);
2233
2356
  } catch (err) {
@@ -2237,8 +2360,9 @@ subtaskCommand.command("update <subtask-id>").description("Update a subtask").op
2237
2360
  subtaskCommand.command("delete <subtask-id>").description("Delete a subtask").action((subtaskId) => {
2238
2361
  try {
2239
2362
  const subtask = getTask(subtaskId);
2240
- if (!subtask)
2363
+ if (!subtask) {
2241
2364
  return handleNotFound("Subtask", subtaskId);
2365
+ }
2242
2366
  if (!subtask.parentTaskId) {
2243
2367
  error(`${subtaskId} is not a subtask. Use 'trekker task delete' instead.`);
2244
2368
  process.exit(1);
@@ -2254,7 +2378,7 @@ subtaskCommand.command("delete <subtask-id>").description("Delete a subtask").ac
2254
2378
  import { Command as Command6 } from "commander";
2255
2379
 
2256
2380
  // src/services/comment.ts
2257
- import { eq as eq4 } from "drizzle-orm";
2381
+ import { eq as eq4, desc as desc3, sql as sql4 } from "drizzle-orm";
2258
2382
  function createComment(input) {
2259
2383
  const db = getDb();
2260
2384
  const task = db.select().from(tasks).where(eq4(tasks.id, input.taskId)).get();
@@ -2276,16 +2400,22 @@ function createComment(input) {
2276
2400
  }
2277
2401
  function getComment(id) {
2278
2402
  const db = getDb();
2279
- const result = db.select().from(comments).where(eq4(comments.id, id)).get();
2280
- return result;
2403
+ return db.select().from(comments).where(eq4(comments.id, id)).get();
2281
2404
  }
2282
- function listComments(taskId) {
2405
+ function listComments(taskId, options) {
2283
2406
  const db = getDb();
2407
+ const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
2408
+ const page = options?.page ?? PAGINATION_DEFAULTS.DEFAULT_PAGE;
2409
+ const offset = (page - 1) * limit;
2284
2410
  const task = db.select().from(tasks).where(eq4(tasks.id, taskId)).get();
2285
2411
  if (!task) {
2286
2412
  throw new Error(`Task not found: ${taskId}`);
2287
2413
  }
2288
- return db.select().from(comments).where(eq4(comments.taskId, taskId)).all();
2414
+ const where = eq4(comments.taskId, taskId);
2415
+ const countRow = db.select({ count: sql4`count(*)` }).from(comments).where(where).get();
2416
+ const total = countRow?.count ?? 0;
2417
+ const items = db.select().from(comments).where(where).orderBy(desc3(comments.createdAt)).limit(limit).offset(offset).all();
2418
+ return { total, page, limit, items };
2289
2419
  }
2290
2420
  function updateComment(id, input) {
2291
2421
  const db = getDb();
@@ -2297,7 +2427,11 @@ function updateComment(id, input) {
2297
2427
  content: input.content,
2298
2428
  updatedAt: new Date
2299
2429
  }).where(eq4(comments.id, id)).run();
2300
- return getComment(id);
2430
+ const updated = getComment(id);
2431
+ if (!updated) {
2432
+ throw new Error(`Comment not found after update: ${id}`);
2433
+ }
2434
+ return updated;
2301
2435
  }
2302
2436
  function deleteComment(id) {
2303
2437
  const db = getDb();
@@ -2324,19 +2458,11 @@ commentCommand.command("add <task-id>").description("Add a comment to a task").r
2324
2458
  handleCommandError(err);
2325
2459
  }
2326
2460
  });
2327
- commentCommand.command("list <task-id>").description("List all comments on a task").action((taskId) => {
2461
+ commentCommand.command("list <task-id>").description("List all comments on a task").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((taskId, options) => {
2328
2462
  try {
2329
- const comments2 = listComments(taskId);
2330
- if (isToonMode()) {
2331
- output(comments2);
2332
- } else {
2333
- if (comments2.length === 0) {
2334
- console.log(`No comments on ${taskId}`);
2335
- } else {
2336
- console.log(`Comments on ${taskId}:`);
2337
- console.log(formatCommentList(comments2));
2338
- }
2339
- }
2463
+ const { limit, page } = parsePaginationOptions(options);
2464
+ const result = listComments(taskId, { limit, page });
2465
+ outputResult(result, formatPaginatedCommentList);
2340
2466
  } catch (err) {
2341
2467
  handleCommandError(err);
2342
2468
  }
@@ -2423,6 +2549,9 @@ function wouldCreateCycle(taskId, dependsOnId) {
2423
2549
  const stack = [dependsOnId];
2424
2550
  while (stack.length > 0) {
2425
2551
  const current = stack.pop();
2552
+ if (current === undefined) {
2553
+ continue;
2554
+ }
2426
2555
  if (current === taskId) {
2427
2556
  return true;
2428
2557
  }
@@ -2812,14 +2941,25 @@ var SAMPLE_SUBTASKS = [
2812
2941
  priority: 3
2813
2942
  }
2814
2943
  ];
2944
+ var TASK_JWT = 1;
2945
+ var TASK_LOGIN = 2;
2946
+ var TASK_PASSWORD_RESET = 3;
2947
+ var TASK_DASHBOARD_LAYOUT = 4;
2948
+ var TASK_CHART_COMPONENTS = 5;
2949
+ var TASK_REALTIME = 6;
2950
+ var TASK_USER_ENDPOINTS = 8;
2951
+ var TASK_RATE_LIMITING = 9;
2952
+ var TASK_JEST_SETUP = 11;
2953
+ var TASK_UNIT_TESTS_AUTH = 12;
2954
+ var TASK_E2E_TESTING = 13;
2815
2955
  var SAMPLE_DEPENDENCIES = [
2816
- [2, 1],
2817
- [3, 1],
2818
- [6, 5],
2819
- [6, 4],
2820
- [9, 8],
2821
- [12, 11],
2822
- [13, 11]
2956
+ [TASK_LOGIN, TASK_JWT],
2957
+ [TASK_PASSWORD_RESET, TASK_JWT],
2958
+ [TASK_REALTIME, TASK_CHART_COMPONENTS],
2959
+ [TASK_REALTIME, TASK_DASHBOARD_LAYOUT],
2960
+ [TASK_RATE_LIMITING, TASK_USER_ENDPOINTS],
2961
+ [TASK_UNIT_TESTS_AUTH, TASK_JEST_SETUP],
2962
+ [TASK_E2E_TESTING, TASK_JEST_SETUP]
2823
2963
  ];
2824
2964
  var seedCommand = new Command9("seed").description("Seed the database with sample data (development only)").option("--force", "Skip confirmation prompt").action((options) => {
2825
2965
  try {
@@ -2848,13 +2988,17 @@ var seedCommand = new Command9("seed").description("Seed the database with sampl
2848
2988
  info(`
2849
2989
  Creating tasks...`);
2850
2990
  for (const taskData of SAMPLE_TASKS) {
2991
+ let epicId;
2992
+ if (taskData.epicIndex !== null) {
2993
+ epicId = epicIds[taskData.epicIndex];
2994
+ }
2851
2995
  const task = createTask({
2852
2996
  title: taskData.title,
2853
2997
  description: taskData.description,
2854
2998
  priority: taskData.priority,
2855
2999
  status: taskData.status,
2856
3000
  tags: taskData.tags,
2857
- epicId: taskData.epicIndex !== null ? epicIds[taskData.epicIndex] : undefined
3001
+ epicId
2858
3002
  });
2859
3003
  taskIds.push(task.id);
2860
3004
  info(` Created ${task.id}: ${task.title}`);
@@ -2881,8 +3025,7 @@ Creating dependencies...`);
2881
3025
  success(`
2882
3026
  Seed complete! Created ${epicIds.length} epics, ${taskIds.length} tasks, ${SAMPLE_SUBTASKS.length} subtasks, and ${SAMPLE_DEPENDENCIES.length} dependencies.`);
2883
3027
  } catch (err) {
2884
- error(err instanceof Error ? err.message : String(err));
2885
- process.exit(1);
3028
+ handleCommandError(err);
2886
3029
  }
2887
3030
  });
2888
3031
 
@@ -2940,8 +3083,8 @@ function search(query, options) {
2940
3083
  title: row.title,
2941
3084
  snippet: row.snippet,
2942
3085
  score: Math.abs(row.score),
2943
- status: row.status || null,
2944
- parentId: row.parent_id || null
3086
+ status: row.status ?? null,
3087
+ parentId: row.parent_id ?? null
2945
3088
  }))
2946
3089
  };
2947
3090
  }
@@ -2952,12 +3095,13 @@ var searchCommand = new Command10("search").description("Search across epics, ta
2952
3095
  if (options.rebuildIndex) {
2953
3096
  rebuildSearchIndex();
2954
3097
  }
2955
- const limit = parseInt(options.limit, 10);
2956
- const page = parseInt(options.page, 10);
2957
- validatePagination(limit, page);
2958
- const types = options.type ? options.type.split(",").map((t) => t.trim()) : undefined;
2959
- if (types)
2960
- validateSearchEntityTypes(types);
3098
+ const { limit, page } = parsePaginationOptions(options);
3099
+ const rawTypes = parseCommaSeparated(options.type);
3100
+ let types;
3101
+ if (rawTypes) {
3102
+ validateSearchEntityTypes(rawTypes);
3103
+ types = rawTypes;
3104
+ }
2961
3105
  const result = search(query, {
2962
3106
  types,
2963
3107
  status: options.status,
@@ -2980,9 +3124,15 @@ function formatSearchResults(result) {
2980
3124
  `);
2981
3125
  }
2982
3126
  for (const r of result.results) {
2983
- const typeLabel = r.type.toUpperCase().padEnd(7);
2984
- const statusLabel = r.status ? ` [${r.status}]` : "";
2985
- const parentLabel = r.parentId ? ` (parent: ${r.parentId})` : "";
3127
+ const typeLabel = r.type.toUpperCase().padEnd(TYPE_PAD_WIDTH);
3128
+ let statusLabel = "";
3129
+ if (r.status) {
3130
+ statusLabel = ` [${r.status}]`;
3131
+ }
3132
+ let parentLabel = "";
3133
+ if (r.parentId) {
3134
+ parentLabel = ` (parent: ${r.parentId})`;
3135
+ }
2986
3136
  lines.push(`${typeLabel} ${r.id}${statusLabel}${parentLabel}`);
2987
3137
  if (r.title) {
2988
3138
  lines.push(` Title: ${r.title}`);
@@ -3004,6 +3154,8 @@ var import_customParseFormat = __toESM(require_customParseFormat(), 1);
3004
3154
  import { Command as Command11 } from "commander";
3005
3155
 
3006
3156
  // src/services/history.ts
3157
+ var parseJsonRecord = JSON.parse;
3158
+ var parseJsonChanges = JSON.parse;
3007
3159
  function getHistory(options) {
3008
3160
  const sqlite = requireSqliteInstance();
3009
3161
  const limit = options?.limit ?? PAGINATION_DEFAULTS.HISTORY_PAGE_SIZE;
@@ -3033,7 +3185,10 @@ function getHistory(options) {
3033
3185
  conditions.push("created_at <= ?");
3034
3186
  params.push(options.until.getTime());
3035
3187
  }
3036
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3188
+ let whereClause = "";
3189
+ if (conditions.length > 0) {
3190
+ whereClause = `WHERE ${conditions.join(" AND ")}`;
3191
+ }
3037
3192
  const countQuery = `SELECT COUNT(*) as total FROM events ${whereClause}`;
3038
3193
  const countResult = sqlite.query(countQuery).get(...params);
3039
3194
  const total = countResult?.total ?? 0;
@@ -3049,15 +3204,25 @@ function getHistory(options) {
3049
3204
  total,
3050
3205
  page,
3051
3206
  limit,
3052
- events: results.map((row) => ({
3053
- id: row.id,
3054
- action: row.action,
3055
- entityType: row.entity_type,
3056
- entityId: row.entity_id,
3057
- snapshot: row.snapshot ? JSON.parse(row.snapshot) : null,
3058
- changes: row.changes ? JSON.parse(row.changes) : null,
3059
- timestamp: new Date(row.created_at)
3060
- }))
3207
+ events: results.map((row) => {
3208
+ let snapshot = null;
3209
+ if (row.snapshot) {
3210
+ snapshot = parseJsonRecord(row.snapshot);
3211
+ }
3212
+ let changes = null;
3213
+ if (row.changes) {
3214
+ changes = parseJsonChanges(row.changes);
3215
+ }
3216
+ return {
3217
+ id: row.id,
3218
+ action: row.action,
3219
+ entityType: row.entity_type,
3220
+ entityId: row.entity_id,
3221
+ snapshot,
3222
+ changes,
3223
+ timestamp: new Date(row.created_at)
3224
+ };
3225
+ })
3061
3226
  };
3062
3227
  }
3063
3228
 
@@ -3065,31 +3230,14 @@ function getHistory(options) {
3065
3230
  import_dayjs.default.extend(import_customParseFormat.default);
3066
3231
  var historyCommand = new Command11("history").description("View history of all changes (creates, updates, deletes)").option("--entity <id>", "Filter by entity ID (e.g., TREK-1, EPIC-1)").option("--type <types>", "Filter by type: epic,task,subtask,comment,dependency (comma-separated)").option("--action <actions>", "Filter by action: create,update,delete (comma-separated)").option("--since <date>", "Events after date (YYYY-MM-DD)").option("--until <date>", "Events before date (YYYY-MM-DD)").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((options) => {
3067
3232
  try {
3068
- const types = options.type ? options.type.split(",").map((t) => t.trim()) : undefined;
3069
- const actions = options.action ? options.action.split(",").map((a) => a.trim()) : undefined;
3070
- const limit = parseInt(options.limit, 10);
3071
- const page = parseInt(options.page, 10);
3072
- if (isNaN(limit) || limit < 1) {
3073
- throw new Error("Invalid limit value");
3074
- }
3075
- if (isNaN(page) || page < 1) {
3076
- throw new Error("Invalid page value");
3077
- }
3078
- const validTypes = ["epic", "task", "subtask", "comment", "dependency"];
3233
+ const types = parseCommaSeparated(options.type);
3234
+ const actions = parseCommaSeparated(options.action);
3235
+ const { limit, page } = parsePaginationOptions(options);
3079
3236
  if (types) {
3080
- for (const t of types) {
3081
- if (!validTypes.includes(t)) {
3082
- throw new Error(`Invalid type: ${t}. Valid types: ${validTypes.join(", ")}`);
3083
- }
3084
- }
3237
+ validateHistoryTypes(types);
3085
3238
  }
3086
- const validActions = ["create", "update", "delete"];
3087
3239
  if (actions) {
3088
- for (const a of actions) {
3089
- if (!validActions.includes(a)) {
3090
- throw new Error(`Invalid action: ${a}. Valid actions: ${validActions.join(", ")}`);
3091
- }
3092
- }
3240
+ validateHistoryActions(actions);
3093
3241
  }
3094
3242
  let since;
3095
3243
  let until;
@@ -3121,8 +3269,7 @@ var historyCommand = new Command11("history").description("View history of all c
3121
3269
  console.log(formatHistoryResults(result));
3122
3270
  }
3123
3271
  } catch (err) {
3124
- error(err instanceof Error ? err.message : String(err));
3125
- process.exit(1);
3272
+ handleCommandError(err);
3126
3273
  }
3127
3274
  });
3128
3275
  function parseDate(dateStr) {
@@ -3154,8 +3301,8 @@ function formatHistoryResults(result) {
3154
3301
  }
3155
3302
  function formatEvent(event) {
3156
3303
  const lines = [];
3157
- const timestamp = event.timestamp.toISOString().replace("T", " ").substring(0, 19);
3158
- const actionLabel = event.action.toUpperCase().padEnd(6);
3304
+ const timestamp = event.timestamp.toISOString().replace("T", " ").slice(0, TIMESTAMP_SLICE_END);
3305
+ const actionLabel = event.action.toUpperCase().padEnd(ACTION_PAD_WIDTH);
3159
3306
  const typeLabel = event.entityType.toUpperCase();
3160
3307
  lines.push(`[${timestamp}] ${actionLabel} ${typeLabel} ${event.entityId}`);
3161
3308
  if (event.action === "update" && event.changes) {
@@ -3166,13 +3313,13 @@ function formatEvent(event) {
3166
3313
  }
3167
3314
  } else if (event.snapshot) {
3168
3315
  const snap = event.snapshot;
3169
- if (snap.title) {
3316
+ if (typeof snap.title === "string") {
3170
3317
  lines.push(` title: ${snap.title}`);
3171
3318
  }
3172
- if (snap.content) {
3173
- lines.push(` content: ${truncate(String(snap.content), 60)}`);
3319
+ if (typeof snap.content === "string") {
3320
+ lines.push(` content: ${truncate(snap.content, TRUNCATE_CONTENT)}`);
3174
3321
  }
3175
- if (snap.status) {
3322
+ if (typeof snap.status === "string") {
3176
3323
  lines.push(` status: ${snap.status}`);
3177
3324
  }
3178
3325
  }
@@ -3184,15 +3331,21 @@ function formatValue(value) {
3184
3331
  return "(none)";
3185
3332
  }
3186
3333
  if (typeof value === "string") {
3187
- return truncate(value, 40);
3334
+ return truncate(value, TRUNCATE_DEFAULT);
3188
3335
  }
3189
- return String(value);
3336
+ if (typeof value === "number") {
3337
+ return `${value}`;
3338
+ }
3339
+ if (typeof value === "boolean") {
3340
+ return String(value);
3341
+ }
3342
+ return JSON.stringify(value);
3190
3343
  }
3191
3344
  function truncate(str, maxLen) {
3192
3345
  if (str.length <= maxLen) {
3193
3346
  return str;
3194
3347
  }
3195
- return str.substring(0, maxLen - 3) + "...";
3348
+ return `${str.slice(0, Math.max(0, maxLen - TRUNCATE_OFFSET))}...`;
3196
3349
  }
3197
3350
 
3198
3351
  // src/commands/list.ts
@@ -3201,6 +3354,7 @@ var import_customParseFormat2 = __toESM(require_customParseFormat(), 1);
3201
3354
  import { Command as Command12 } from "commander";
3202
3355
 
3203
3356
  // src/services/list.ts
3357
+ var VALID_SORT_FIELD_SET = new Set(VALID_SORT_FIELDS);
3204
3358
  function listAll(options) {
3205
3359
  const sqlite = requireSqliteInstance();
3206
3360
  const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
@@ -3225,17 +3379,25 @@ function listAll(options) {
3225
3379
  }
3226
3380
  if (options?.since) {
3227
3381
  conditions.push("created_at >= ?");
3228
- params.push(Math.floor(options.since.getTime() / 1000));
3382
+ params.push(Math.floor(options.since.getTime() / MS_PER_SECOND));
3229
3383
  }
3230
3384
  if (options?.until) {
3231
3385
  conditions.push("created_at <= ?");
3232
- params.push(Math.floor(options.until.getTime() / 1000));
3386
+ params.push(Math.floor(options.until.getTime() / MS_PER_SECOND));
3387
+ }
3388
+ let whereClause = "";
3389
+ if (conditions.length > 0) {
3390
+ whereClause = `WHERE ${conditions.join(" AND ")}`;
3233
3391
  }
3234
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3235
3392
  let orderClause = "ORDER BY created_at DESC";
3236
3393
  if (options?.sort && options.sort.length > 0) {
3237
3394
  const sortParts = options.sort.map((s) => {
3238
- const field = s.field === "created" ? "created_at" : s.field === "updated" ? "updated_at" : s.field;
3395
+ let field = s.field;
3396
+ if (s.field === "created") {
3397
+ field = "created_at";
3398
+ } else if (s.field === "updated") {
3399
+ field = "updated_at";
3400
+ }
3239
3401
  return `${field} ${s.direction.toUpperCase()}`;
3240
3402
  });
3241
3403
  orderClause = `ORDER BY ${sortParts.join(", ")}`;
@@ -3268,8 +3430,8 @@ function listAll(options) {
3268
3430
  status: row.status,
3269
3431
  priority: row.priority,
3270
3432
  parentId: row.parent_id,
3271
- createdAt: new Date(row.created_at * 1000),
3272
- updatedAt: new Date(row.updated_at * 1000)
3433
+ createdAt: new Date(row.created_at * MS_PER_SECOND),
3434
+ updatedAt: new Date(row.updated_at * MS_PER_SECOND)
3273
3435
  }))
3274
3436
  };
3275
3437
  }
@@ -3278,11 +3440,14 @@ function parseSort(sortStr) {
3278
3440
  const result = [];
3279
3441
  for (const part of parts) {
3280
3442
  const [field, dir] = part.split(":").map((s) => s.trim().toLowerCase());
3281
- if (!VALID_SORT_FIELDS.includes(field)) {
3443
+ if (!VALID_SORT_FIELD_SET.has(field)) {
3282
3444
  throw new Error(`Invalid sort field: ${field}. Valid fields: ${VALID_SORT_FIELDS.join(", ")}`);
3283
3445
  }
3284
- const direction = dir === "asc" ? "asc" : "desc";
3285
- result.push({ field, direction });
3446
+ if (dir === "asc") {
3447
+ result.push({ field, direction: "asc" });
3448
+ } else {
3449
+ result.push({ field, direction: "desc" });
3450
+ }
3286
3451
  }
3287
3452
  return result;
3288
3453
  }
@@ -3291,21 +3456,29 @@ function parseSort(sortStr) {
3291
3456
  import_dayjs2.default.extend(import_customParseFormat2.default);
3292
3457
  var listCommand = new Command12("list").description("List all epics, tasks, and subtasks").option("--type <types>", "Filter by type: epic,task,subtask (comma-separated)").option("--status <statuses>", "Filter by status (comma-separated)").option("--priority <levels>", "Filter by priority: 0-5 (comma-separated)").option("--since <date>", "Created after date (YYYY-MM-DD)").option("--until <date>", "Created before date (YYYY-MM-DD)").option("--sort <fields>", "Sort by fields (field:direction, comma-separated)", "created:desc").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((options) => {
3293
3458
  try {
3294
- const limit = parseInt(options.limit, 10);
3295
- const page = parseInt(options.page, 10);
3296
- validatePagination(limit, page);
3297
- const types = options.type ? options.type.split(",").map((t) => t.trim()) : undefined;
3298
- if (types)
3299
- validateListEntityTypes(types);
3300
- const statuses = options.status ? options.status.split(",").map((s) => s.trim()) : undefined;
3301
- const priorities = options.priority ? options.priority.split(",").map((p) => parseInt(p.trim(), 10)) : undefined;
3302
- if (priorities)
3459
+ const { limit, page } = parsePaginationOptions(options);
3460
+ const rawTypes = parseCommaSeparated(options.type);
3461
+ let types;
3462
+ if (rawTypes) {
3463
+ validateListEntityTypes(rawTypes);
3464
+ types = rawTypes;
3465
+ }
3466
+ const statuses = parseCommaSeparated(options.status);
3467
+ let priorities;
3468
+ if (options.priority) {
3469
+ priorities = options.priority.split(",").map((p) => Number.parseInt(p.trim(), RADIX_DECIMAL));
3470
+ }
3471
+ if (priorities) {
3303
3472
  validatePriorities(priorities);
3473
+ }
3304
3474
  let sort;
3305
3475
  try {
3306
3476
  sort = parseSort(options.sort);
3307
3477
  } catch (err) {
3308
- throw new Error(`Invalid sort: ${err instanceof Error ? err.message : String(err)}`);
3478
+ if (err instanceof Error) {
3479
+ throw new Error(`Invalid sort: ${err.message}`);
3480
+ }
3481
+ throw new Error(`Invalid sort: ${String(err)}`);
3309
3482
  }
3310
3483
  const since = parseDate2(options.since);
3311
3484
  if (options.since && !since) {
@@ -3331,16 +3504,24 @@ var listCommand = new Command12("list").description("List all epics, tasks, and
3331
3504
  }
3332
3505
  });
3333
3506
  function parseDate2(dateStr) {
3334
- if (!dateStr)
3507
+ if (!dateStr) {
3335
3508
  return;
3509
+ }
3336
3510
  const parsed = import_dayjs2.default(dateStr, "YYYY-MM-DD", true);
3337
- return parsed.isValid() ? parsed.toDate() : undefined;
3511
+ if (parsed.isValid()) {
3512
+ return parsed.toDate();
3513
+ }
3514
+ return;
3338
3515
  }
3339
3516
  function parseUntilDate(dateStr) {
3340
- if (!dateStr)
3517
+ if (!dateStr) {
3341
3518
  return;
3519
+ }
3342
3520
  const parsed = import_dayjs2.default(dateStr, "YYYY-MM-DD", true);
3343
- return parsed.isValid() ? parsed.endOf("day").toDate() : undefined;
3521
+ if (parsed.isValid()) {
3522
+ return parsed.endOf("day").toDate();
3523
+ }
3524
+ return;
3344
3525
  }
3345
3526
  function formatListResults(result) {
3346
3527
  const lines = [];
@@ -3363,10 +3544,13 @@ function formatListResults(result) {
3363
3544
  `);
3364
3545
  }
3365
3546
  function formatItem(item) {
3366
- const typeLabel = item.type.toUpperCase().padEnd(7);
3367
- const statusLabel = item.status.padEnd(11);
3547
+ const typeLabel = item.type.toUpperCase().padEnd(TYPE_PAD_WIDTH);
3548
+ const statusLabel = item.status.padEnd(STATUS_PAD_WIDTH);
3368
3549
  const priorityLabel = `P${item.priority}`;
3369
- const parentLabel = item.parentId ? ` (${item.parentId})` : "";
3550
+ let parentLabel = "";
3551
+ if (item.parentId) {
3552
+ parentLabel = ` (${item.parentId})`;
3553
+ }
3370
3554
  return `${typeLabel} ${item.id} | ${statusLabel} | ${priorityLabel} | ${item.title}${parentLabel}`;
3371
3555
  }
3372
3556
 
@@ -3374,22 +3558,31 @@ function formatItem(item) {
3374
3558
  import { Command as Command13 } from "commander";
3375
3559
 
3376
3560
  // src/services/ready.ts
3377
- function getReadyTasks() {
3561
+ function getReadyTasks(options) {
3378
3562
  const sqlite = requireSqliteInstance();
3563
+ const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
3564
+ const page = options?.page ?? PAGINATION_DEFAULTS.DEFAULT_PAGE;
3565
+ const offset = (page - 1) * limit;
3566
+ const baseWhere = `
3567
+ WHERE t.status = 'todo'
3568
+ AND t.parent_task_id IS NULL
3569
+ AND NOT EXISTS (
3570
+ SELECT 1 FROM dependencies d
3571
+ JOIN tasks dt ON dt.id = d.depends_on_id
3572
+ WHERE d.task_id = t.id
3573
+ AND dt.status NOT IN ('completed', 'wont_fix', 'archived')
3574
+ )
3575
+ `;
3576
+ const countResult = sqlite.query(`SELECT COUNT(*) as total FROM tasks t ${baseWhere}`).get();
3577
+ const total = countResult?.total ?? 0;
3379
3578
  const readyRows = sqlite.query(`
3380
3579
  SELECT t.id, t.title, t.description, t.priority, t.status,
3381
3580
  t.epic_id, t.tags, t.created_at, t.updated_at
3382
3581
  FROM tasks t
3383
- WHERE t.status = 'todo'
3384
- AND t.parent_task_id IS NULL
3385
- AND NOT EXISTS (
3386
- SELECT 1 FROM dependencies d
3387
- JOIN tasks dt ON dt.id = d.depends_on_id
3388
- WHERE d.task_id = t.id
3389
- AND dt.status NOT IN ('completed', 'wont_fix', 'archived')
3390
- )
3582
+ ${baseWhere}
3391
3583
  ORDER BY t.priority ASC, t.created_at ASC
3392
- `).all();
3584
+ LIMIT ? OFFSET ?
3585
+ `).all(limit, offset);
3393
3586
  const dependentsQuery = sqlite.query(`
3394
3587
  SELECT t.id, t.title, t.status, t.priority
3395
3588
  FROM dependencies d
@@ -3397,7 +3590,7 @@ function getReadyTasks() {
3397
3590
  WHERE d.depends_on_id = ?
3398
3591
  ORDER BY t.priority ASC
3399
3592
  `);
3400
- return readyRows.map((row) => ({
3593
+ const items = readyRows.map((row) => ({
3401
3594
  id: row.id,
3402
3595
  title: row.title,
3403
3596
  description: row.description,
@@ -3405,8 +3598,8 @@ function getReadyTasks() {
3405
3598
  status: row.status,
3406
3599
  epicId: row.epic_id,
3407
3600
  tags: row.tags,
3408
- createdAt: new Date(row.created_at * 1000),
3409
- updatedAt: new Date(row.updated_at * 1000),
3601
+ createdAt: new Date(row.created_at * MS_PER_SECOND),
3602
+ updatedAt: new Date(row.updated_at * MS_PER_SECOND),
3410
3603
  dependents: dependentsQuery.all(row.id).map((d) => ({
3411
3604
  id: d.id,
3412
3605
  title: d.title,
@@ -3414,41 +3607,54 @@ function getReadyTasks() {
3414
3607
  priority: d.priority
3415
3608
  }))
3416
3609
  }));
3610
+ return { total, page, limit, items };
3417
3611
  }
3418
3612
 
3419
3613
  // src/commands/ready.ts
3420
- var readyCommand = new Command13("ready").description("Show tasks that are ready to work on (unblocked, todo)").action(() => {
3614
+ var readyCommand = new Command13("ready").description("Show tasks that are ready to work on (unblocked, todo)").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((options) => {
3421
3615
  try {
3422
- const readyTasks = getReadyTasks();
3423
- outputResult(readyTasks, formatReadyTasks);
3616
+ const { limit, page } = parsePaginationOptions(options);
3617
+ const result = getReadyTasks({ limit, page });
3618
+ outputResult(result, formatReadyTasks);
3424
3619
  } catch (err) {
3425
3620
  handleCommandError(err);
3426
3621
  }
3427
3622
  });
3428
- function formatReadyTasks(tasks2) {
3429
- if (tasks2.length === 0) {
3623
+ function formatReadyTasks(result) {
3624
+ if (result.items.length === 0) {
3430
3625
  return "No ready tasks found.";
3431
3626
  }
3432
3627
  const lines = [];
3433
- lines.push(`${tasks2.length} ready task(s):
3628
+ lines.push(`${result.total} ready task(s) (page ${result.page}, ${result.limit} per page)
3434
3629
  `);
3435
- for (const task of tasks2) {
3436
- const epic = task.epicId ? ` (${task.epicId})` : "";
3437
- const tags = task.tags ? ` [${task.tags}]` : "";
3630
+ for (const task of result.items) {
3631
+ let epic = "";
3632
+ if (task.epicId) {
3633
+ epic = ` (${task.epicId})`;
3634
+ }
3635
+ let tags = "";
3636
+ if (task.tags) {
3637
+ tags = ` [${task.tags}]`;
3638
+ }
3438
3639
  lines.push(`${task.id} | P${task.priority} | ${task.title}${epic}${tags}`);
3439
3640
  if (task.dependents.length > 0) {
3440
3641
  for (const dep of task.dependents) {
3441
- lines.push(` -> unblocks ${dep.id} | ${dep.status.padEnd(11)} | P${dep.priority} | ${dep.title}`);
3642
+ lines.push(` -> unblocks ${dep.id} | ${dep.status.padEnd(STATUS_PAD_WIDTH)} | P${dep.priority} | ${dep.title}`);
3442
3643
  }
3443
3644
  }
3444
3645
  }
3646
+ const totalPages = Math.ceil(result.total / result.limit);
3647
+ if (totalPages > 1) {
3648
+ lines.push(`
3649
+ Page ${result.page} of ${totalPages}`);
3650
+ }
3445
3651
  return lines.join(`
3446
3652
  `);
3447
3653
  }
3448
3654
  // package.json
3449
3655
  var package_default = {
3450
3656
  name: "@obsfx/trekker",
3451
- version: "1.8.0",
3657
+ version: "1.10.0",
3452
3658
  description: "A CLI-based issue tracker built for AI coding agents. Stores tasks, epics, and dependencies in a local SQLite database.",
3453
3659
  type: "module",
3454
3660
  main: "dist/index.js",
@@ -3464,6 +3670,11 @@ var package_default = {
3464
3670
  dev: "bun run src/index.ts",
3465
3671
  test: "bun test",
3466
3672
  "test:watch": "bun test --watch",
3673
+ lint: "eslint . --cache",
3674
+ "lint:fix": "eslint . --fix --cache",
3675
+ format: 'prettier --write "src/**/*.ts" "tests/**/*.ts"',
3676
+ "format:check": 'prettier --check "src/**/*.ts" "tests/**/*.ts"',
3677
+ check: "bun run lint && bun run format:check",
3467
3678
  "db:generate": "drizzle-kit generate",
3468
3679
  "db:migrate": "drizzle-kit migrate",
3469
3680
  "release:patch": "npm version patch && npm run build && npm publish --access public",
@@ -3505,7 +3716,13 @@ var package_default = {
3505
3716
  devDependencies: {
3506
3717
  "@types/bun": "^1.2.2",
3507
3718
  "drizzle-kit": "^0.30.4",
3508
- typescript: "^5.7.3"
3719
+ eslint: "^10.0.2",
3720
+ "eslint-config-prettier": "^10.1.8",
3721
+ "eslint-plugin-unicorn": "^63.0.0",
3722
+ knip: "^5.85.0",
3723
+ prettier: "^3.8.1",
3724
+ typescript: "^5.7.3",
3725
+ "typescript-eslint": "^8.56.1"
3509
3726
  }
3510
3727
  };
3511
3728