@markmdev/pebble 0.1.0 → 0.1.2

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/index.js CHANGED
@@ -7,6 +7,7 @@ import { Command } from "commander";
7
7
  var ISSUE_TYPES = ["task", "bug", "epic"];
8
8
  var PRIORITIES = [0, 1, 2, 3, 4];
9
9
  var STATUSES = ["open", "in_progress", "blocked", "closed"];
10
+ var EVENT_TYPES = ["create", "update", "close", "reopen", "comment"];
10
11
  var PRIORITY_LABELS = {
11
12
  0: "critical",
12
13
  1: "high",
@@ -886,6 +887,7 @@ function listCommand(program2) {
886
887
  program2.command("list").description("List issues").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--priority <priority>", "Filter by priority").option("--parent <id>", "Filter by parent epic").action(async (options) => {
887
888
  const pretty = program2.opts().pretty ?? false;
888
889
  try {
890
+ getOrCreatePebbleDir();
889
891
  const filters = {};
890
892
  if (options.status !== void 0) {
891
893
  const status = options.status;
@@ -924,6 +926,7 @@ function showCommand(program2) {
924
926
  program2.command("show <id>").description("Show issue details").action(async (id) => {
925
927
  const pretty = program2.opts().pretty ?? false;
926
928
  try {
929
+ getOrCreatePebbleDir();
927
930
  const resolvedId = resolveId(id);
928
931
  const issue = getIssue(resolvedId);
929
932
  if (!issue) {
@@ -941,6 +944,7 @@ function readyCommand(program2) {
941
944
  program2.command("ready").description("Show issues ready for work (no open blockers)").action(async () => {
942
945
  const pretty = program2.opts().pretty ?? false;
943
946
  try {
947
+ getOrCreatePebbleDir();
944
948
  const issues = getReady();
945
949
  outputIssueList(issues, pretty);
946
950
  } catch (error) {
@@ -954,6 +958,7 @@ function blockedCommand(program2) {
954
958
  program2.command("blocked").description("Show blocked issues (have open blockers)").action(async () => {
955
959
  const pretty = program2.opts().pretty ?? false;
956
960
  try {
961
+ getOrCreatePebbleDir();
957
962
  const issues = getBlocked();
958
963
  outputIssueList(issues, pretty);
959
964
  } catch (error) {
@@ -1157,6 +1162,7 @@ function graphCommand(program2) {
1157
1162
  program2.command("graph").description("Show dependency graph").option("--root <id>", "Filter to subtree rooted at issue").action(async (options) => {
1158
1163
  const pretty = program2.opts().pretty ?? false;
1159
1164
  try {
1165
+ getOrCreatePebbleDir();
1160
1166
  let issues;
1161
1167
  if (options.root) {
1162
1168
  const rootId = resolveId(options.root);
@@ -1342,6 +1348,27 @@ function mergeIssuesFromFiles(filePaths) {
1342
1348
  _sources: Array.from(sources)
1343
1349
  }));
1344
1350
  }
1351
+ function findIssueInSources(issueId, filePaths) {
1352
+ const allIssues = mergeIssuesFromFiles(filePaths);
1353
+ let found = allIssues.find((i) => i.id === issueId);
1354
+ if (!found) {
1355
+ const matches = allIssues.filter((i) => i.id.startsWith(issueId));
1356
+ if (matches.length === 1) {
1357
+ found = matches[0];
1358
+ } else if (matches.length > 1) {
1359
+ return null;
1360
+ }
1361
+ }
1362
+ if (!found) {
1363
+ return null;
1364
+ }
1365
+ const targetFile = found._sources[0];
1366
+ return { issue: found, targetFile };
1367
+ }
1368
+ function appendEventToFile(event, filePath) {
1369
+ const line = JSON.stringify(event) + "\n";
1370
+ fs2.appendFileSync(filePath, line, "utf-8");
1371
+ }
1345
1372
  function uiCommand(program2) {
1346
1373
  const defaultPort = process.env.PEBBLE_UI_PORT || "3333";
1347
1374
  program2.command("ui").description("Serve the React UI").option("--port <port>", "Port to serve on", defaultPort).option("--no-open", "Do not open browser automatically").option("--files <paths>", "Comma-separated paths to issues.jsonl files for multi-worktree view").action(async (options) => {
@@ -1422,26 +1449,16 @@ function uiCommand(program2) {
1422
1449
  });
1423
1450
  app.get("/api/issues", (_req, res) => {
1424
1451
  try {
1425
- if (isMultiWorktree()) {
1426
- const issues = mergeIssuesFromFiles(issueFiles);
1427
- res.json(issues);
1428
- } else {
1429
- const issues = getIssues({});
1430
- res.json(issues);
1431
- }
1452
+ const issues = mergeIssuesFromFiles(issueFiles);
1453
+ res.json(issues);
1432
1454
  } catch (error) {
1433
1455
  res.status(500).json({ error: error.message });
1434
1456
  }
1435
1457
  });
1436
1458
  app.get("/api/events", (_req, res) => {
1437
1459
  try {
1438
- if (isMultiWorktree()) {
1439
- const events = readEventsFromFiles(issueFiles);
1440
- res.json(events);
1441
- } else {
1442
- const events = readEvents();
1443
- res.json(events);
1444
- }
1460
+ const events = readEventsFromFiles(issueFiles);
1461
+ res.json(events);
1445
1462
  } catch (error) {
1446
1463
  res.status(500).json({ error: error.message });
1447
1464
  }
@@ -1653,12 +1670,28 @@ data: ${message}
1653
1670
  });
1654
1671
  app.put("/api/issues/:id", (req, res) => {
1655
1672
  try {
1656
- const pebbleDir = getOrCreatePebbleDir();
1657
- const issueId = resolveId(req.params.id);
1658
- const issue = getIssue(issueId);
1659
- if (!issue) {
1660
- res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1661
- return;
1673
+ let issue;
1674
+ let issueId;
1675
+ let targetFile;
1676
+ if (isMultiWorktree()) {
1677
+ const found = findIssueInSources(req.params.id, issueFiles);
1678
+ if (!found) {
1679
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1680
+ return;
1681
+ }
1682
+ issue = found.issue;
1683
+ issueId = issue.id;
1684
+ targetFile = found.targetFile;
1685
+ } else {
1686
+ const pebbleDir = getOrCreatePebbleDir();
1687
+ issueId = resolveId(req.params.id);
1688
+ const localIssue = getIssue(issueId);
1689
+ if (!localIssue) {
1690
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1691
+ return;
1692
+ }
1693
+ issue = localIssue;
1694
+ targetFile = path2.join(pebbleDir, "issues.jsonl");
1662
1695
  }
1663
1696
  const { title, type, priority, status, description, parent } = req.body;
1664
1697
  const updates = {};
@@ -1695,14 +1728,26 @@ data: ${message}
1695
1728
  }
1696
1729
  if (parent !== void 0) {
1697
1730
  if (parent !== null) {
1698
- const parentIssue = getIssue(parent);
1699
- if (!parentIssue) {
1700
- res.status(400).json({ error: `Parent issue not found: ${parent}` });
1701
- return;
1702
- }
1703
- if (parentIssue.type !== "epic") {
1704
- res.status(400).json({ error: "Parent must be an epic" });
1705
- return;
1731
+ if (isMultiWorktree()) {
1732
+ const parentFound = findIssueInSources(parent, issueFiles);
1733
+ if (!parentFound) {
1734
+ res.status(400).json({ error: `Parent issue not found: ${parent}` });
1735
+ return;
1736
+ }
1737
+ if (parentFound.issue.type !== "epic") {
1738
+ res.status(400).json({ error: "Parent must be an epic" });
1739
+ return;
1740
+ }
1741
+ } else {
1742
+ const parentIssue = getIssue(parent);
1743
+ if (!parentIssue) {
1744
+ res.status(400).json({ error: `Parent issue not found: ${parent}` });
1745
+ return;
1746
+ }
1747
+ if (parentIssue.type !== "epic") {
1748
+ res.status(400).json({ error: "Parent must be an epic" });
1749
+ return;
1750
+ }
1706
1751
  }
1707
1752
  }
1708
1753
  updates.parent = parent;
@@ -1718,27 +1763,47 @@ data: ${message}
1718
1763
  timestamp,
1719
1764
  data: updates
1720
1765
  };
1721
- appendEvent(event, pebbleDir);
1722
- const updatedIssue = getIssue(issueId);
1723
- res.json(updatedIssue);
1766
+ appendEventToFile(event, targetFile);
1767
+ if (isMultiWorktree()) {
1768
+ const updated = findIssueInSources(issueId, issueFiles);
1769
+ res.json(updated?.issue || { ...issue, ...updates, updatedAt: timestamp });
1770
+ } else {
1771
+ res.json(getIssue(issueId));
1772
+ }
1724
1773
  } catch (error) {
1725
1774
  res.status(500).json({ error: error.message });
1726
1775
  }
1727
1776
  });
1728
1777
  app.post("/api/issues/:id/close", (req, res) => {
1729
1778
  try {
1730
- const pebbleDir = getOrCreatePebbleDir();
1731
- const issueId = resolveId(req.params.id);
1732
- const issue = getIssue(issueId);
1733
- if (!issue) {
1734
- res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1735
- return;
1779
+ let issue;
1780
+ let issueId;
1781
+ let targetFile;
1782
+ if (isMultiWorktree()) {
1783
+ const found = findIssueInSources(req.params.id, issueFiles);
1784
+ if (!found) {
1785
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1786
+ return;
1787
+ }
1788
+ issue = found.issue;
1789
+ issueId = issue.id;
1790
+ targetFile = found.targetFile;
1791
+ } else {
1792
+ const pebbleDir = getOrCreatePebbleDir();
1793
+ issueId = resolveId(req.params.id);
1794
+ const localIssue = getIssue(issueId);
1795
+ if (!localIssue) {
1796
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1797
+ return;
1798
+ }
1799
+ issue = localIssue;
1800
+ targetFile = path2.join(pebbleDir, "issues.jsonl");
1736
1801
  }
1737
1802
  if (issue.status === "closed") {
1738
1803
  res.status(400).json({ error: "Issue is already closed" });
1739
1804
  return;
1740
1805
  }
1741
- if (issue.type === "epic" && hasOpenChildren(issueId)) {
1806
+ if (!isMultiWorktree() && issue.type === "epic" && hasOpenChildren(issueId)) {
1742
1807
  res.status(400).json({ error: "Cannot close epic with open children" });
1743
1808
  return;
1744
1809
  }
@@ -1750,21 +1815,41 @@ data: ${message}
1750
1815
  timestamp,
1751
1816
  data: { reason }
1752
1817
  };
1753
- appendEvent(event, pebbleDir);
1754
- const closedIssue = getIssue(issueId);
1755
- res.json(closedIssue);
1818
+ appendEventToFile(event, targetFile);
1819
+ if (isMultiWorktree()) {
1820
+ const updated = findIssueInSources(issueId, issueFiles);
1821
+ res.json(updated?.issue || { ...issue, status: "closed", updatedAt: timestamp });
1822
+ } else {
1823
+ res.json(getIssue(issueId));
1824
+ }
1756
1825
  } catch (error) {
1757
1826
  res.status(500).json({ error: error.message });
1758
1827
  }
1759
1828
  });
1760
1829
  app.post("/api/issues/:id/reopen", (req, res) => {
1761
1830
  try {
1762
- const pebbleDir = getOrCreatePebbleDir();
1763
- const issueId = resolveId(req.params.id);
1764
- const issue = getIssue(issueId);
1765
- if (!issue) {
1766
- res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1767
- return;
1831
+ let issue;
1832
+ let issueId;
1833
+ let targetFile;
1834
+ if (isMultiWorktree()) {
1835
+ const found = findIssueInSources(req.params.id, issueFiles);
1836
+ if (!found) {
1837
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1838
+ return;
1839
+ }
1840
+ issue = found.issue;
1841
+ issueId = issue.id;
1842
+ targetFile = found.targetFile;
1843
+ } else {
1844
+ const pebbleDir = getOrCreatePebbleDir();
1845
+ issueId = resolveId(req.params.id);
1846
+ const localIssue = getIssue(issueId);
1847
+ if (!localIssue) {
1848
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1849
+ return;
1850
+ }
1851
+ issue = localIssue;
1852
+ targetFile = path2.join(pebbleDir, "issues.jsonl");
1768
1853
  }
1769
1854
  if (issue.status !== "closed") {
1770
1855
  res.status(400).json({ error: "Issue is not closed" });
@@ -1778,21 +1863,41 @@ data: ${message}
1778
1863
  timestamp,
1779
1864
  data: { reason }
1780
1865
  };
1781
- appendEvent(event, pebbleDir);
1782
- const reopenedIssue = getIssue(issueId);
1783
- res.json(reopenedIssue);
1866
+ appendEventToFile(event, targetFile);
1867
+ if (isMultiWorktree()) {
1868
+ const updated = findIssueInSources(issueId, issueFiles);
1869
+ res.json(updated?.issue || { ...issue, status: "open", updatedAt: timestamp });
1870
+ } else {
1871
+ res.json(getIssue(issueId));
1872
+ }
1784
1873
  } catch (error) {
1785
1874
  res.status(500).json({ error: error.message });
1786
1875
  }
1787
1876
  });
1788
1877
  app.post("/api/issues/:id/comments", (req, res) => {
1789
1878
  try {
1790
- const pebbleDir = getOrCreatePebbleDir();
1791
- const issueId = resolveId(req.params.id);
1792
- const issue = getIssue(issueId);
1793
- if (!issue) {
1794
- res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1795
- return;
1879
+ let issue;
1880
+ let issueId;
1881
+ let targetFile;
1882
+ if (isMultiWorktree()) {
1883
+ const found = findIssueInSources(req.params.id, issueFiles);
1884
+ if (!found) {
1885
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1886
+ return;
1887
+ }
1888
+ issue = found.issue;
1889
+ issueId = issue.id;
1890
+ targetFile = found.targetFile;
1891
+ } else {
1892
+ const pebbleDir = getOrCreatePebbleDir();
1893
+ issueId = resolveId(req.params.id);
1894
+ const localIssue = getIssue(issueId);
1895
+ if (!localIssue) {
1896
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1897
+ return;
1898
+ }
1899
+ issue = localIssue;
1900
+ targetFile = path2.join(pebbleDir, "issues.jsonl");
1796
1901
  }
1797
1902
  const { text, author } = req.body;
1798
1903
  if (!text || typeof text !== "string" || text.trim() === "") {
@@ -1810,38 +1915,68 @@ data: ${message}
1810
1915
  author
1811
1916
  }
1812
1917
  };
1813
- appendEvent(event, pebbleDir);
1814
- const updatedIssue = getIssue(issueId);
1815
- res.json(updatedIssue);
1918
+ appendEventToFile(event, targetFile);
1919
+ if (isMultiWorktree()) {
1920
+ const updated = findIssueInSources(issueId, issueFiles);
1921
+ res.json(updated?.issue || issue);
1922
+ } else {
1923
+ res.json(getIssue(issueId));
1924
+ }
1816
1925
  } catch (error) {
1817
1926
  res.status(500).json({ error: error.message });
1818
1927
  }
1819
1928
  });
1820
1929
  app.post("/api/issues/:id/deps", (req, res) => {
1821
1930
  try {
1822
- const pebbleDir = getOrCreatePebbleDir();
1823
- const issueId = resolveId(req.params.id);
1824
- const issue = getIssue(issueId);
1825
- if (!issue) {
1826
- res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1827
- return;
1931
+ let issue;
1932
+ let issueId;
1933
+ let targetFile;
1934
+ if (isMultiWorktree()) {
1935
+ const found = findIssueInSources(req.params.id, issueFiles);
1936
+ if (!found) {
1937
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1938
+ return;
1939
+ }
1940
+ issue = found.issue;
1941
+ issueId = issue.id;
1942
+ targetFile = found.targetFile;
1943
+ } else {
1944
+ const pebbleDir = getOrCreatePebbleDir();
1945
+ issueId = resolveId(req.params.id);
1946
+ const localIssue = getIssue(issueId);
1947
+ if (!localIssue) {
1948
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1949
+ return;
1950
+ }
1951
+ issue = localIssue;
1952
+ targetFile = path2.join(pebbleDir, "issues.jsonl");
1828
1953
  }
1829
1954
  const { blockerId } = req.body;
1830
1955
  if (!blockerId) {
1831
1956
  res.status(400).json({ error: "blockerId is required" });
1832
1957
  return;
1833
1958
  }
1834
- const resolvedBlockerId = resolveId(blockerId);
1835
- const blockerIssue = getIssue(resolvedBlockerId);
1836
- if (!blockerIssue) {
1837
- res.status(404).json({ error: `Blocker issue not found: ${blockerId}` });
1838
- return;
1959
+ let resolvedBlockerId;
1960
+ if (isMultiWorktree()) {
1961
+ const blockerFound = findIssueInSources(blockerId, issueFiles);
1962
+ if (!blockerFound) {
1963
+ res.status(404).json({ error: `Blocker issue not found: ${blockerId}` });
1964
+ return;
1965
+ }
1966
+ resolvedBlockerId = blockerFound.issue.id;
1967
+ } else {
1968
+ resolvedBlockerId = resolveId(blockerId);
1969
+ const blockerIssue = getIssue(resolvedBlockerId);
1970
+ if (!blockerIssue) {
1971
+ res.status(404).json({ error: `Blocker issue not found: ${blockerId}` });
1972
+ return;
1973
+ }
1839
1974
  }
1840
1975
  if (issue.blockedBy.includes(resolvedBlockerId)) {
1841
1976
  res.status(400).json({ error: "Dependency already exists" });
1842
1977
  return;
1843
1978
  }
1844
- if (detectCycle(issueId, resolvedBlockerId)) {
1979
+ if (!isMultiWorktree() && detectCycle(issueId, resolvedBlockerId)) {
1845
1980
  res.status(400).json({ error: "Adding this dependency would create a cycle" });
1846
1981
  return;
1847
1982
  }
@@ -1854,39 +1989,74 @@ data: ${message}
1854
1989
  blockedBy: [...issue.blockedBy, resolvedBlockerId]
1855
1990
  }
1856
1991
  };
1857
- appendEvent(event, pebbleDir);
1858
- const updatedIssue = getIssue(issueId);
1859
- res.json(updatedIssue);
1992
+ appendEventToFile(event, targetFile);
1993
+ if (isMultiWorktree()) {
1994
+ const updated = findIssueInSources(issueId, issueFiles);
1995
+ res.json(updated?.issue || { ...issue, blockedBy: [...issue.blockedBy, resolvedBlockerId], updatedAt: timestamp });
1996
+ } else {
1997
+ res.json(getIssue(issueId));
1998
+ }
1860
1999
  } catch (error) {
1861
2000
  res.status(500).json({ error: error.message });
1862
2001
  }
1863
2002
  });
1864
2003
  app.delete("/api/issues/:id/deps/:blockerId", (req, res) => {
1865
2004
  try {
1866
- const pebbleDir = getOrCreatePebbleDir();
1867
- const issueId = resolveId(req.params.id);
1868
- const issue = getIssue(issueId);
1869
- if (!issue) {
1870
- res.status(404).json({ error: `Issue not found: ${req.params.id}` });
1871
- return;
2005
+ let issue;
2006
+ let issueId;
2007
+ let targetFile;
2008
+ if (isMultiWorktree()) {
2009
+ const found = findIssueInSources(req.params.id, issueFiles);
2010
+ if (!found) {
2011
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
2012
+ return;
2013
+ }
2014
+ issue = found.issue;
2015
+ issueId = issue.id;
2016
+ targetFile = found.targetFile;
2017
+ } else {
2018
+ const pebbleDir = getOrCreatePebbleDir();
2019
+ issueId = resolveId(req.params.id);
2020
+ const localIssue = getIssue(issueId);
2021
+ if (!localIssue) {
2022
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
2023
+ return;
2024
+ }
2025
+ issue = localIssue;
2026
+ targetFile = path2.join(pebbleDir, "issues.jsonl");
2027
+ }
2028
+ let resolvedBlockerId;
2029
+ if (isMultiWorktree()) {
2030
+ const blockerFound = findIssueInSources(req.params.blockerId, issueFiles);
2031
+ if (blockerFound) {
2032
+ resolvedBlockerId = blockerFound.issue.id;
2033
+ } else {
2034
+ resolvedBlockerId = req.params.blockerId;
2035
+ }
2036
+ } else {
2037
+ resolvedBlockerId = resolveId(req.params.blockerId);
1872
2038
  }
1873
- const resolvedBlockerId = resolveId(req.params.blockerId);
1874
2039
  if (!issue.blockedBy.includes(resolvedBlockerId)) {
1875
2040
  res.status(400).json({ error: "Dependency does not exist" });
1876
2041
  return;
1877
2042
  }
1878
2043
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2044
+ const newBlockedBy = issue.blockedBy.filter((id) => id !== resolvedBlockerId);
1879
2045
  const event = {
1880
2046
  type: "update",
1881
2047
  issueId,
1882
2048
  timestamp,
1883
2049
  data: {
1884
- blockedBy: issue.blockedBy.filter((id) => id !== resolvedBlockerId)
2050
+ blockedBy: newBlockedBy
1885
2051
  }
1886
2052
  };
1887
- appendEvent(event, pebbleDir);
1888
- const updatedIssue = getIssue(issueId);
1889
- res.json(updatedIssue);
2053
+ appendEventToFile(event, targetFile);
2054
+ if (isMultiWorktree()) {
2055
+ const updated = findIssueInSources(issueId, issueFiles);
2056
+ res.json(updated?.issue || { ...issue, blockedBy: newBlockedBy, updatedAt: timestamp });
2057
+ } else {
2058
+ res.json(getIssue(issueId));
2059
+ }
1890
2060
  } catch (error) {
1891
2061
  res.status(500).json({ error: error.message });
1892
2062
  }
@@ -2146,7 +2316,7 @@ function mergeIssues(filePaths) {
2146
2316
  }));
2147
2317
  }
2148
2318
  function mergeCommand(program2) {
2149
- program2.command("merge <files...>").description("Merge multiple issues.jsonl files into one").option("-o, --output <file>", "Output file (default: stdout)").option("--events", "Output raw events instead of computed state").option("--show-sources", "Include _sources field showing which files contained each issue").action((files, options) => {
2319
+ program2.command("merge <files...>").description("Merge multiple issues.jsonl files into one").option("-o, --output <file>", "Output file (default: stdout)").option("--state", "Output computed state instead of raw events").option("--show-sources", "Include _sources field (only with --state)").action((files, options) => {
2150
2320
  const pretty = program2.opts().pretty ?? false;
2151
2321
  const filePaths = [];
2152
2322
  for (const file of files) {
@@ -2163,13 +2333,13 @@ function mergeCommand(program2) {
2163
2333
  }
2164
2334
  try {
2165
2335
  let output;
2166
- if (options.events) {
2167
- const events = mergeEvents(filePaths);
2168
- output = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
2169
- } else {
2336
+ if (options.state) {
2170
2337
  const issues = mergeIssues(filePaths);
2171
2338
  const outputIssues = options.showSources ? issues : issues.map(({ _sources, ...issue }) => issue);
2172
2339
  output = pretty ? JSON.stringify(outputIssues, null, 2) : JSON.stringify(outputIssues);
2340
+ } else {
2341
+ const events = mergeEvents(filePaths);
2342
+ output = events.map((e) => JSON.stringify(e)).join("\n");
2173
2343
  }
2174
2344
  if (options.output) {
2175
2345
  fs4.writeFileSync(options.output, output + "\n", "utf-8");
@@ -2184,6 +2354,246 @@ function mergeCommand(program2) {
2184
2354
  });
2185
2355
  }
2186
2356
 
2357
+ // src/cli/commands/summary.ts
2358
+ function countChildren(epicId) {
2359
+ const children = getChildren(epicId);
2360
+ return {
2361
+ total: children.length,
2362
+ done: children.filter((c) => c.status === "closed").length,
2363
+ in_progress: children.filter((c) => c.status === "in_progress").length,
2364
+ open: children.filter((c) => c.status === "open").length,
2365
+ blocked: children.filter((c) => c.status === "blocked").length
2366
+ };
2367
+ }
2368
+ function formatSummaryPretty(summaries) {
2369
+ if (summaries.length === 0) {
2370
+ return "No epics found.";
2371
+ }
2372
+ const lines = [];
2373
+ for (const summary of summaries) {
2374
+ const { children } = summary;
2375
+ const progress = children.total > 0 ? `(${children.done}/${children.total} done)` : "(no children)";
2376
+ lines.push(`${summary.id} ${summary.title} ${progress}`);
2377
+ if (summary.description) {
2378
+ const desc = summary.description.split("\n")[0];
2379
+ const truncated = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
2380
+ lines.push(` ${truncated}`);
2381
+ }
2382
+ }
2383
+ return lines.join("\n");
2384
+ }
2385
+ function summaryCommand(program2) {
2386
+ program2.command("summary").description("Show epic summary with child completion status").option("--status <status>", "Filter epics by status (default: open)").option("--limit <n>", "Max epics to return", "10").option("--include-closed", "Include closed epics").action(async (options) => {
2387
+ const pretty = program2.opts().pretty ?? false;
2388
+ try {
2389
+ getOrCreatePebbleDir();
2390
+ let epics = getIssues({ type: "epic" });
2391
+ if (options.includeClosed) {
2392
+ } else if (options.status !== void 0) {
2393
+ const status = options.status;
2394
+ if (!STATUSES.includes(status)) {
2395
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2396
+ }
2397
+ epics = epics.filter((e) => e.status === status);
2398
+ } else {
2399
+ epics = epics.filter((e) => e.status !== "closed");
2400
+ }
2401
+ epics.sort(
2402
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
2403
+ );
2404
+ const limit = parseInt(options.limit, 10);
2405
+ if (limit > 0) {
2406
+ epics = epics.slice(0, limit);
2407
+ }
2408
+ const summaries = epics.map((epic) => ({
2409
+ id: epic.id,
2410
+ title: epic.title,
2411
+ description: epic.description,
2412
+ status: epic.status,
2413
+ children: countChildren(epic.id)
2414
+ }));
2415
+ if (pretty) {
2416
+ console.log(formatSummaryPretty(summaries));
2417
+ } else {
2418
+ console.log(formatJson(summaries));
2419
+ }
2420
+ } catch (error) {
2421
+ outputError(error, pretty);
2422
+ }
2423
+ });
2424
+ }
2425
+
2426
+ // src/cli/commands/history.ts
2427
+ function parseDuration(duration) {
2428
+ const match = duration.match(/^(\d+)([dhms])$/);
2429
+ if (!match) {
2430
+ throw new Error(`Invalid duration: ${duration}. Use format like "7d", "24h", "30m", "60s"`);
2431
+ }
2432
+ const value = parseInt(match[1], 10);
2433
+ const unit = match[2];
2434
+ const multipliers = {
2435
+ s: 1e3,
2436
+ m: 60 * 1e3,
2437
+ h: 60 * 60 * 1e3,
2438
+ d: 24 * 60 * 60 * 1e3
2439
+ };
2440
+ return value * multipliers[unit];
2441
+ }
2442
+ function formatRelativeTime(timestamp) {
2443
+ const now = Date.now();
2444
+ const then = new Date(timestamp).getTime();
2445
+ const diff = now - then;
2446
+ const seconds = Math.floor(diff / 1e3);
2447
+ const minutes = Math.floor(seconds / 60);
2448
+ const hours = Math.floor(minutes / 60);
2449
+ const days = Math.floor(hours / 24);
2450
+ if (days > 0) return `${days}d ago`;
2451
+ if (hours > 0) return `${hours}h ago`;
2452
+ if (minutes > 0) return `${minutes}m ago`;
2453
+ return `${seconds}s ago`;
2454
+ }
2455
+ function formatHistoryPretty(entries) {
2456
+ if (entries.length === 0) {
2457
+ return "No events found.";
2458
+ }
2459
+ const lines = [];
2460
+ for (const entry of entries) {
2461
+ const time = formatRelativeTime(entry.timestamp);
2462
+ const eventLabel = entry.event.charAt(0).toUpperCase() + entry.event.slice(1);
2463
+ let line = `[${time}] ${eventLabel} ${entry.issue.id} "${entry.issue.title}" (${entry.issue.type})`;
2464
+ if (entry.parent) {
2465
+ line += ` under ${entry.parent.id}`;
2466
+ }
2467
+ lines.push(line);
2468
+ }
2469
+ return lines.join("\n");
2470
+ }
2471
+ function historyCommand(program2) {
2472
+ program2.command("history").description("Show recent activity log").option("--limit <n>", "Max events to return", "20").option("--type <type>", "Filter by event type (create/close/reopen/update/comment)").option("--since <duration>", 'Only show events since (e.g., "7d", "24h")').action(async (options) => {
2473
+ const pretty = program2.opts().pretty ?? false;
2474
+ try {
2475
+ getOrCreatePebbleDir();
2476
+ const events = readEvents();
2477
+ const state = computeState(events);
2478
+ let filteredEvents = events;
2479
+ if (options.type !== void 0) {
2480
+ const eventType = options.type;
2481
+ if (!EVENT_TYPES.includes(eventType)) {
2482
+ throw new Error(`Invalid event type: ${eventType}. Must be one of: ${EVENT_TYPES.join(", ")}`);
2483
+ }
2484
+ filteredEvents = filteredEvents.filter((e) => e.type === eventType);
2485
+ }
2486
+ if (options.since !== void 0) {
2487
+ const sinceMs = parseDuration(options.since);
2488
+ const cutoff = Date.now() - sinceMs;
2489
+ filteredEvents = filteredEvents.filter(
2490
+ (e) => new Date(e.timestamp).getTime() >= cutoff
2491
+ );
2492
+ }
2493
+ filteredEvents.sort(
2494
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
2495
+ );
2496
+ const limit = parseInt(options.limit, 10);
2497
+ if (limit > 0) {
2498
+ filteredEvents = filteredEvents.slice(0, limit);
2499
+ }
2500
+ const entries = filteredEvents.map((event) => {
2501
+ const issue = state.get(event.issueId);
2502
+ const entry = {
2503
+ timestamp: event.timestamp,
2504
+ event: event.type,
2505
+ issue: {
2506
+ id: event.issueId,
2507
+ title: issue?.title ?? "(unknown)",
2508
+ type: issue?.type ?? "task"
2509
+ }
2510
+ };
2511
+ if (issue?.parent) {
2512
+ const parent = state.get(issue.parent);
2513
+ if (parent) {
2514
+ entry.parent = {
2515
+ id: parent.id,
2516
+ title: parent.title
2517
+ };
2518
+ }
2519
+ }
2520
+ if (event.type === "close" && event.data.reason) {
2521
+ entry.details = { reason: event.data.reason };
2522
+ } else if (event.type === "comment") {
2523
+ entry.details = { text: event.data.text };
2524
+ }
2525
+ return entry;
2526
+ });
2527
+ if (pretty) {
2528
+ console.log(formatHistoryPretty(entries));
2529
+ } else {
2530
+ console.log(formatJson(entries));
2531
+ }
2532
+ } catch (error) {
2533
+ outputError(error, pretty);
2534
+ }
2535
+ });
2536
+ }
2537
+
2538
+ // src/cli/commands/search.ts
2539
+ function searchIssues(issues, query) {
2540
+ const lowerQuery = query.toLowerCase();
2541
+ return issues.filter((issue) => {
2542
+ if (issue.id.toLowerCase().includes(lowerQuery)) {
2543
+ return true;
2544
+ }
2545
+ if (issue.title.toLowerCase().includes(lowerQuery)) {
2546
+ return true;
2547
+ }
2548
+ if (issue.description?.toLowerCase().includes(lowerQuery)) {
2549
+ return true;
2550
+ }
2551
+ if (issue.comments.some((c) => c.text.toLowerCase().includes(lowerQuery))) {
2552
+ return true;
2553
+ }
2554
+ return false;
2555
+ });
2556
+ }
2557
+ function searchCommand(program2) {
2558
+ program2.command("search <query>").description("Search issues by text in title, description, and comments").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--limit <n>", "Max results", "20").action(async (query, options) => {
2559
+ const pretty = program2.opts().pretty ?? false;
2560
+ try {
2561
+ getOrCreatePebbleDir();
2562
+ let issues = getIssues();
2563
+ if (options.status !== void 0) {
2564
+ const status = options.status;
2565
+ if (!STATUSES.includes(status)) {
2566
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2567
+ }
2568
+ issues = issues.filter((i) => i.status === status);
2569
+ }
2570
+ if (options.type !== void 0) {
2571
+ const type = options.type;
2572
+ if (!ISSUE_TYPES.includes(type)) {
2573
+ throw new Error(`Invalid type: ${type}. Must be one of: ${ISSUE_TYPES.join(", ")}`);
2574
+ }
2575
+ issues = issues.filter((i) => i.type === type);
2576
+ }
2577
+ let results = searchIssues(issues, query);
2578
+ const lowerQuery = query.toLowerCase();
2579
+ results.sort((a, b) => {
2580
+ const aInTitle = a.title.toLowerCase().includes(lowerQuery);
2581
+ const bInTitle = b.title.toLowerCase().includes(lowerQuery);
2582
+ if (aInTitle && !bInTitle) return -1;
2583
+ if (!aInTitle && bInTitle) return 1;
2584
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
2585
+ });
2586
+ const limit = parseInt(options.limit, 10);
2587
+ if (limit > 0) {
2588
+ results = results.slice(0, limit);
2589
+ }
2590
+ outputIssueList(results, pretty);
2591
+ } catch (error) {
2592
+ outputError(error, pretty);
2593
+ }
2594
+ });
2595
+ }
2596
+
2187
2597
  // src/cli/index.ts
2188
2598
  var program = new Command();
2189
2599
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
@@ -2203,5 +2613,8 @@ graphCommand(program);
2203
2613
  uiCommand(program);
2204
2614
  importCommand(program);
2205
2615
  mergeCommand(program);
2616
+ summaryCommand(program);
2617
+ historyCommand(program);
2618
+ searchCommand(program);
2206
2619
  program.parse();
2207
2620
  //# sourceMappingURL=index.js.map